Skip to content

luci-theme-footstrap: add new theme - #8903

Merged
hnyman merged 1 commit into
openwrt:masterfrom
VizzleTF:luci-theme-footstrap
Aug 20, 2026
Merged

luci-theme-footstrap: add new theme#8903
hnyman merged 1 commit into
openwrt:masterfrom
VizzleTF:luci-theme-footstrap

Conversation

@VizzleTF

@VizzleTF VizzleTF commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Pull request details

Description

Add Footstrap, a standalone theme for OpenWrt 24.10 and newer. It depends on luci-base and nothing else and ships no CSS framework; pages are rendered by luci-base's own view JS.

  • Sidebar or top bar, switched by the user and remembered per browser.
  • Light and dark modes, palettes, density and colour axes, applied before the first paint.
  • Search over the menu tree.
  • Client-side navigation that swaps the view instead of reloading the page. It falls back to a full load when a page's stylesheet would otherwise carry over.
  • Settings page built from .cbi-value rows, ui.Select and ui.RangeSlider.

Developed at https://github.com/VizzleTF/luci-theme-footstrap

Navigation is client-side: a click swaps the view rather than reloading the document, so a page opens without the full-load round trip. Measured against bootstrap on a Netcore N60 Pro running 25.12.2, five runs, medians:

bootstrap footstrap
38-page tour 11 306 ms 4933 ms
router CPU time 37.3 s 18.4 s
median page 3.03× faster

Method and per-page results:
https://github.com/VizzleTF/luci-theme-footstrap/blob/main/docs/benchmark.md

Discussed on the forum since July:
https://forum.openwrt.org/t/footstrap-new-theme-for-luci-2x-faster-than-bootstrap/251930

Screenshot or video of changes (if applicable)

Desktop

overview-desktop-dark appearance-desktop-light

Mobile

network-mobile-dark

Maintainer (preferred)

@VizzleTF


Tested on

OpenWrt version: OpenWrt 25.12.4 and OpenWrt 24.10.8 (aarch64_generic containers), plus OpenWrt 25.12.2 (r32802-f505120278) on a Netcore N60 Pro
LuCI version: this branch, built with the x86_64 master and 24.10 SDKs
Web browser(s): Chromium 151, Firefox 145, Safari 26
And a single release is downloaded 200–240 times

@openwrt openwrt Bot added the add package Introduces a new package Makefile build script label Aug 3, 2026
@jow-

jow- commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

I like it. Could be a spiritual successor of the OpenWrt 2020 theme. Ideally I'd like to apply the same improvements to the bootstrap default theme too, while keeping its current style.

@VizzleTF

VizzleTF commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for a feedback @jow- !

'd like to apply the same improvements to the bootstrap

Palettes are templated by design: adding one is a matter of filling in a set of variables, not touching the chrome. So a palette matching bootstrap's current look is a small amount of work.

image

That might cover the "same improvements, current style" case without backporting client-side navigation logic into bootstrap itself.

Also theme can be much faster if this will be merged

@VizzleTF

VizzleTF commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Like this)
image

@skramstad

Copy link
Copy Markdown
Contributor

I love this @VizzleTF . 👍 Just out of curiosity, should/could we minify some files? The ones that are more generic (never changes).

@VizzleTF

VizzleTF commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

I love this @VizzleTF . 👍 Just out of curiosity, should/could we minify some files? The ones that are more generic (never changes).

Thank you for feedback!

I minified it in my repo, its saves around 15% (65kb instead of 75kb)

i can do it here also, if it doesn’t need to be readable 😄

@skramstad

Copy link
Copy Markdown
Contributor

I love this @VizzleTF . 👍 Just out of curiosity, should/could we minify some files? The ones that are more generic (never changes).

Thank you for feedback!

I minified it in my repo, its saves around 15% (65kb instead of 75kb)

i can do it here also, if it doesn’t need to be readable 😄

Ah okey. :) I'll let @jow- make that decision 👍

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit (3985204). The commit message matches the diff, so no commit-check notes.

Two functional findings (login-form parity with the base sysauth.ut, and a layout-default that is stated three times and disagrees once) plus four nits are inline. CI on 39852045 is green across all three FormalityCheck runs, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js Outdated
Comment thread themes/luci-theme-footstrap/ucode/template/themes/footstrap/header.ut Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap.js Outdated
Comment thread themes/luci-theme-footstrap/Makefile Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 3985204 to 4251198 Compare August 3, 2026 18:02
VizzleTF added a commit to VizzleTF/luci-theme-footstrap that referenced this pull request Aug 3, 2026
…default once

The theme-local sysauth.ut dropped auth_fields, auth_message, auth_html and
auth_assets, which the dispatcher hands every sysauth template. With
luci-plugin-2fa installed the OTP input was never drawn, so luci_otp was never
posted and a router with 2FA enabled could not be logged into while this theme
was active. The template exists only to pass blank_page: true and now stays a
superset of the generic one; each conditional attribute carries a leading space
because ucode emits none of its own.

_resolvedDefault() still answered 'sidebar' after the built-in layout flipped to
the top bar, so on a fresh install matchesSavedDefault() was false before the
user touched anything and Reset-to-saved landed on the wrong layout.

Three comments described an Appearance page at admin/system/appearance that the
package never shipped, counted fourteen axes against AXIS_KEYS' twenty-one, and
listed a po/ that luci.mk's Build/Prepare does not copy.

Found by the review on openwrt/luci#8903; verified on all four owlab routers.
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 4251198 to b7402d3 Compare August 3, 2026 18:42
@unisequence

Copy link
Copy Markdown

this theme is awesome, much much better than others like proton2025/aurora.

@jow-

jow- commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The three more or less official themes are:

  • Bootstrap (current default)
  • OpenWrt 2020 (attempt for modern theme based on OpenWrt's logo refresh design)
  • OpenWrt (legacy design)

I'm inclined to take this but given that maintaining non-default themes is effort no one is willing to spend, one of the other themes likely has to be deprecated. I'd lean towards deprecating 2020 in favor to this but maybe let's hear the takes from @feckert @systemcrash @hnyman

@VizzleTF

VizzleTF commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

given that maintaining non-default themes is effort no one is willing to spend

I'll be glad to keep maintaining the theme. If something changes in luci-base and it breaks,
no problem — I'll fix it.

I also have a few other pet projects I've kept going for a couple of years now, like
CommitSage (a VS Code extension I've been shipping since August 2024). I still fix issues
there when they come up and answer users, so this isn't something I'd start and walk away from.

@hnyman

hnyman commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

If a new theme is added, I would drop at least the 2020, but maybe also the legacy theme, which is likely not used by many.

(Personally I use bootstrap, which works well)

@skramstad

Copy link
Copy Markdown
Contributor

Been testing this since PR, and I am a fan. Clean, fast, simple and refreshing.
@VizzleTF : This is a personal preference, but is it easy to change the sans font? I really like the Inter font, but understands it add Kb :) . It has some font-features that I prefer.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 39852045b7402d3. The commit message is unchanged and still matches the diff, so no commit-check notes.

All six threads from the last round are addressed: sysauth.ut now carries auth_message / auth_fields / auth_html / auth_assets and is a proper superset of the generic template; _resolvedDefault().layout is top; the Makefile, header.ut and fs-prefs.js header comments and the menu-footstrap.js indentation are all corrected.

The new work in this revision is the third palette. The list is consistently extended across all five places that need it — cascade.css (light + dark blocks, same token set as the other palettes), head.ut's _sd_pal whitelist, head.ut's pre-paint, fs-prefs.js's new PALETTES array, and the fs-appearance.js select — and swapping enumAxis for the wallpaper/density validate-against-a-list shape is the right call for a three-valued axis.

One functional finding inline, in the factory that palette just moved out of: enumAxis derives its window.__fsSD field name as key.slice(3), which is wrong for its one remaining instance (fs-pattern-inkpattern-ink, but the field is pattern_ink). Plus three nits. CI on b7402d3 is green across all three FormalityCheck runs, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js Outdated
Comment thread themes/luci-theme-footstrap/root/etc/uci-defaults/30_luci-theme-footstrap Outdated
Comment thread themes/luci-theme-footstrap/ucode/template/themes/footstrap/sysauth.ut Outdated
VizzleTF added a commit to VizzleTF/luci-theme-footstrap that referenced this pull request Aug 4, 2026
…rver sends

enumAxis derived its window.__fsSD field name as the localStorage key minus
'fs-', giving 'pattern-ink' where head.ut emits 'pattern_ink'. sd() answered
undefined for good, so the axis reported the built-in 'theme' whatever the
router had saved: the page was pre-painted data-pattern-ink="original" while
the Ink control read Theme, and pressing Save-as-default wrote 'theme' over the
stored value. Reproduced and fixed on 25.12/apk and 24.10/opkg. colorAxis folds
the hyphen too, where every key is one word today and the failure would be as
silent tomorrow.

Also corrected the comments that had stopped describing the code: the
uci-defaults fall-through called the built-in layout "the sidebar default", the
axis-shape inventory named a hueAxis that does not exist, omitted surfaceAxis,
counted five instances against fifteen, and still listed updateCheck and a
wallpaper that persists to the router; four CSS comments and the file tree in
docs/css.md pointed at styles/01-fonts.css, deleted with the webfonts in 0.12.1.
The sysauth whitespace rationale now states what ucode actually does.

Found by the review on openwrt/luci#8903.
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch 2 times, most recently from 7273c92 to 98201ff Compare August 4, 2026 06:26

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push b7402d398201ff. The commit message is unchanged and still matches the diff, so no commit-check notes.

First, my ucode whitespace nit from the last round was wrong and your measurement is right — I checked the source rather than my recollection. lstrip_blocks/trim_blocks are not off by default the way I assumed: both the ucode CLI (main.c:531-532) and, more to the point, uhttpd's ucode plugin (ucode.c:197-201) set both to true, and loadfile() inherits the running VM's config and only overrides the keys it is handed (lib.c:5522) — so LuCI's loadfile(path, { raw_mode: false }) keeps them on. The lexer then strips \t\v\f\r before {% and the newline after %} with plain tags (lexer.c:992, lexer.c:1076), which reproduces your A\nBCD exactly. The leading spaces are load-bearing and the comment now states the rule correctly.

The enumAxis/colorAxis sdKey fold, the uci-defaults layout comment and the keep.d inventory are all correct against the tree — pattern_ink is what head.ut:119`` emits and what header.ut's `FS_AXES` reads back, and both `/etc/footstrap` uploads are listed with matching symlinks in the uci-defaults script.

One functional finding inline, in the sysauth.ut block this revision rewrote: the new unconditional auth_message precedence fixes the second dispatcher branch but breaks the first one, where a wrong password on a 2FA router now shows only the plugin's boilerplate. Plus two nits. CI on 98201ff is green across all three FormalityCheck runs, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js Outdated
Comment thread themes/luci-theme-footstrap/root/etc/uci-defaults/30_luci-theme-footstrap Outdated
VizzleTF added a commit to VizzleTF/luci-theme-footstrap that referenced this pull request Aug 4, 2026
`auth_message` is not "the credentials were accepted". The dispatcher's first
branch copies it out of get_challenges() BEFORE the password is checked
(dispatcher.uc:956), and that call answers with a message on every pending
result -- the literal 'Additional verification required' when the plugin has
none of its own (authplugins.uc:312). So on a router running luci-plugin-2fa
both it and `fuser` are set whenever a password is rejected, and the
message-first spelling this theme took from luci-theme-bootstrap then rendered
only the plugin's boilerplate: the user is never told the password was wrong.
The generic template's `auth_message && !fuser` gets that case right and the
other one wrong -- on the OTP step the credentials were correct.

`auth_plugin` separates the two dispatcher branches: it is set only in the
second (dispatcher.uc:1018), i.e. exactly when session_setup() accepted the
credentials. The credentials alert is now gated on !auth_plugin and the
backend's message renders beside it rather than instead of it, so all four
states are distinct. The credentials alert also moves to .alert-message.error,
the danger variant bootstrap uses here, because once both can appear together
an amber box for each reads a rejected password as a routine notice.

Measured on owrt2512 (25.12.4/apk) and owrt2410 (24.10.8/opkg): the template
compiles under `ucode -T -c` on all four dev routers, the four scopes render
through LuCI's own loadfile/render path with the expected alerts and no throw
for the absent auth_plugin, and a real wrong-password POST returns 403 with
.alert-message.error on both package managers. Patching the dispatcher's
sysauth scope reproduces the bug on the shipped spelling: wrong password with
a challenge pending rendered no credentials error at all.

Also in this commit, all found by the same review round on openwrt/luci#8903:

* tools/axes.mjs now holds the window.__fsSD field name every axis reads,
  running the deriving factories' own formula lifted out of fs-prefs.js rather
  than restating it -- a restated copy stays green while the JS says something
  else, which was measured. surfaceAxis joins the key scan too (21 -> 25).
* three stale comments corrected: the "Appearance -> Background image" label,
  propAxis's worked example, and fs-sheets.js's page-ownership rationale, which
  named a LuCI file that exists on none of the four dev routers. Re-measured
  against what actually injects a sheet there.
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 98201ff to 717de96 Compare August 5, 2026 03:33
@openwrt openwrt Bot added the not following guidelines Pull request does not follow formatting guidelines label Aug 5, 2026
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 717de96 to 247891c Compare August 5, 2026 03:39
@openwrt openwrt Bot removed the not following guidelines Pull request does not follow formatting guidelines label Aug 5, 2026

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 98201ff247891c.

All three threads from the last round are addressed and correct against the tree. sysauth.ut now renders the two alerts independently with the credentials one gated on !auth_plugin, which covers all four dispatcher states; the propAxis header comment matches the hyphen fold; and the keep.d / uci-defaults wording now uses the "Appearance -> Wallpaper -> File" spelling that fs-appearance.js actually builds.

The new work is a router-side webfont opt-in (font_sans / font_mono / fonts, the /etc/footstrap/fonts symlink and keep.d entry, a conditional fonts/fonts.css link and an unlayered <style>) plus one CSS narrowing. Two things I checked and found sound, since they are the parts most likely to be wrong: /etc/footstrap/fonts works as a directory in keep.d — sysupgrade's list_static_conffiles() passes the keep.d lines to find … \( -type f -o -type l \), which recurses — and the new <style data-fs-shell> cannot be mistaken for a view sheet or disturb the layer order, because VIEW_SHEETS exempts [data-fs-shell] on style as well as link and both new tags sit after the cascade.css anchor reassertLayerOrder() measures against. The .cbi-page-actions .cbi-dropdown:not(.btn):not(.cbi-button) narrowing also lines up with the real markup: luci.js:2197-2207 gives the Save & Apply combo cbi-dropdown btn cbi-button cbi-button-apply, so the reset no longer flattens it in the Apply unchecked state where .cbi-button-apply has dropped off.

One finding and two nits inline. CI on 247891c is green across all eight checks (three FormalityCheck runs, eslint, CodeQL, build / Test x86_64), so nothing here is CI-grounded.

Commit checks

  • 247891c "luci-theme-footstrap: add new theme" — the message's font paragraph is now a description of only half of what the diff does. It says "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" — still true of the package contents, but this revision adds a router-side opt-in on top of that fallback: three new UCI options in footstrap.settings (font_sans, font_mono, fonts) read by header.ut/head.ut, a new /www/luci-static/footstrap/fonts symlink created by uci-defaults, a new keep.d path, and a conditional @font-face stylesheet link. A UCI contract and a new sysupgrade-preserved path are the kind of thing worth a sentence in the message, since the message is what a bisect or a git log search lands on. (The .cbi-page-actions .cbi-dropdown narrowing is a detail, not worth a line.)

Generated by Claude Code

Comment thread themes/luci-theme-footstrap/root/etc/uci-defaults/30_luci-theme-footstrap Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-sheets.js Outdated
Comment thread themes/luci-theme-footstrap/ucode/template/themes/footstrap/partials/head.ut Outdated
@feckert

feckert commented Aug 5, 2026

Copy link
Copy Markdown
Member

If a new theme is added, I would drop at least the 2020, but maybe also the legacy theme, which is likely not used by many.

(Personally I use bootstrap, which works well)

I feel exactly the same way.

If a modern upstream gets merged I will use this instead of my current used theme https://github.com/jerrykuku/luci-theme-argon

@VizzleTF Unless this is just a one-time "fire-and-forget" operation. Then I would say @jow- 👍

VizzleTF added a commit to VizzleTF/luci-theme-footstrap that referenced this pull request Aug 5, 2026
font_sans and font_mono are printed unescaped into one <style>, and the
charset whitelist has to admit both quote characters -- the shipped default
is `"Manrope",system-ui,...`. A quote does not END anything, it EXTENDS:
`font_sans=My"Font` opens a CSS string that runs to the end of the sheet and
swallows `; --fs-font-mono: ...; }` with it, so a router that set only the
mono font correctly lost it because of a typo in the other option.

Measured on owrt2512 with that value in place. Before: the element renders
`:root {--fs-font-sans: My"Font; --fs-font-mono: JetBrains Mono; }` as one
open string and neither property survives. After: the malformed value is
refused and `--fs-font-mono: JetBrains Mono` is printed alone, while
`"Manrope",system-ui` still renders in full. head.ut compiles under
`ucode -T -c` on all four dev routers and the behaviour was checked on both
package managers.

Also in this commit, from the same review round on openwrt/luci#8903:

* the webfont comments say what the package does -- read the three uci
  options, serve /etc/footstrap/fonts, keep it across sysupgrade -- and where
  the one-line installer lives, since it is in this repository and not in the
  package: it names third-party font hosts, which the theme must not carry.
  The manual path is spelled out beside it.
* fs-sheets.js's ownership example is corrected against the in-tree app: only
  HexEditor's injector runs at module eval, the view's own sheet comes from
  render(), and what makes either invasive is the bare selectors, not the
  ones pinned to #file-manager-container.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit; no new issues found.

Force-push 5e1cdbe1a449945e. Same base (1c70b2b), one commit, message byte-identical, so no commit-check notes. The delta is 17 added / 10 removed across four files and is almost entirely the follow-up to yesterday's rel nit.

The rel collapse checks out on both counts. versionLink in fs-appearance.js:566 and nameThemeInFooter() in fs-chrome.js:306 now spell it the same way as footer.ut's two anchors,`` so the theme has one spelling of the rule instead of two — and it is the spelling the rest of the tree prefers: bare rel="noreferrer" outnumbers the `noopener`-carrying forms 18 to 11 across luci's non-footstrap JS/ucode. No behaviour is lost either; `noreferrer` implies `noopener` per spec, and `target="_blank"` is implicitly `noopener` in current engines regardless.

The same hunk quotes the E() attribute keys, which brings nameThemeInFooter() in line with every other E() call in fs-chrome.js (lines 27, 35, 36, 387, 388 all quote), and wraps the label in an array — E() accepts either, so that is cosmetic. The fs-version.js edit is comment-only and now correctly names both consumers of REPO_URL rather than claiming "used once".

The one substantive line is the footer's line-height, which goes from a flat 26px to max(26px,calc(var(--fs-type) * 2)). --fs-type resolves to a length (calc(13px * var(--fs-density-type)), or the round()ed variant), so the calc() is well-formed, and at density 1 it evaluates to exactly the old 26px. The max() therefore only ever raises the value — at --fs-density-type: 1.15 the footer gets ~30px leading, while the compact .9 density stays pinned at the 26px floor. That reads as a deliberate floor rather than an oversight, and it is the right direction now that the footer carries a third link that can wrap.

Still open from the previous round, not re-posted inline: the commit message and PR body describe the package without mentioning that the theme writes itself into the "Powered by" footer. That is the only user-visible behaviour in the package that a reader of the log would not expect from the subject line.

CI on a449945e: three FormalityCheck runs completed green, the rest not yet reported. Nothing failed, so nothing here is CI-grounded.


Generated by Claude Code

@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from a449945 to 8036368 Compare August 16, 2026 17:18
@hnyman

hnyman commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

On the precedent: none of the four themes in this tree names itself

Yep. That is the practice for themes in the main OpenWrt LuCI repo. They are seen as originating from the repo itself.

So it is new for an in-tree theme, but not against anything stated — and if a maintainer would rather it were not there, say so and it comes out; it is one function and one call.

I do not like that the footer contains theme name & a link to an external site. None of the other themes do that (as you noticed).

I propose that if you want to keep a link of that kind, you move the link to the "footstrap options" page, where it would be visible for those that look more closely to the possibilities offered by the theme. It could be styled more like "feedback on the theme to be given here:"

Note also that there could be PRs and commits from others directly authored to the LuCI repo (instead of yours), so the question where the theme gets developed in future may be more ambiguous than you think.

@VizzleTF

Copy link
Copy Markdown
Contributor Author

I do not like that

Okay, i'll remove that. Not big deal.

there could be PRs and commits from others directly authored to the LuCI repo (instead of yours)

Of course, i understand that.

@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 8036368 to 4868929 Compare August 16, 2026 20:08

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 5e1cdbe148689290. Same base (1c70b2b), one commit, message byte-identical and still matching the diff, so no commit-check notes. The delta is 586 added / 327 removed across ten files, and it is the largest structural change this PR has seen: the "Loading view…" spinner path is gone and the incoming view now renders into a hidden second #view that is swapped in when it is ready.

The staging design holds up on the two things it rests on. getElementById() returning the first match in tree order really is what both of LuCI's #view lookups use — View.__init__'s spinner and DOM.content(document.getElementById('view'), nodes) at the paint — so an in-flight chain writes into the stage and not into the live page, and stageView()'s insertBefore(wrapper, contentHost.firstChild) is what keeps that true. Keeping the live element's identity and moving nodes through dom.content() rather than swapping the element is the right call for the reason given: fs-fit and fs-appearance bind observers to the node that exists at chrome init. And visibility:hidden;height:0 over display:none is load-bearing — view.offsetWidth - 2 at load.js:31 would take a zero from a display:none stage; it also re-queries #view on every updateGraph() call rather than latching the node, so the post-commit reads land on the live element. Serializing renders in exchange for deleting armRenderGuard(), repairStaleRender() and currentViewClass() is a good trade: three mechanisms that only cleaned up afterwards, replaced by one that makes the damage impossible.

readonlyForSegs() going from OR to AND is correct, and for the reason the comment gives. check_acl_depends() at dispatcher.uc:312 is handed the single concatenated ctx.acls list and returns writable as soon as any one group grants write, and resolve_page() computes resolved.node.readonly = !perm at :1083 only when that list is non-empty — which is exactly gated > 0 && gated === locked, since apply_tree_acls() at :436 marks a node readonly precisely when its own list yields no write. Skipping nodes with no depends.acl matches ctx_append() pushing nothing for them.

The session gate reads luci-base accurately on both signals: the 403 + X-LuCI-Login-Required pair is what Request.addInterceptor reacts to at luci.js:2730, the rpc interceptor's (msg, req) argument order is the deliberately swapped one rpc.js documents, and the reasoning for excluding access: false is right — handleCallReply()'s expect loop keeps a same-typed false and resolves, so it never reaches upstream's .catch. Retiring the footer line, VERSION and isReal leaves no dangling readers anywhere in the theme or the templates.

Two findings and three nits inline. The first finding is the one worth a look before merge: a superseded popstate leaves its saved scroll offset parked in a module variable for the next navigation to spend. CI on 48689290 is green across all eight checks, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js Outdated
Comment thread themes/luci-theme-footstrap/po/templates/footstrap.pot Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch 4 times, most recently from 54636ab to 903785c Compare August 17, 2026 20:22
@castillofrancodamian

Copy link
Copy Markdown
Contributor

@VizzleTF Wouldn't it be better if the arrow on the logout button pointed to the right? I find it more intuitive.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 48689290903785c7. Same base (1c70b2b), one commit, message byte-identical and still matching the diff, so no commit-check notes. The delta is 717 added / 69 removed across eleven files, and it is one theme: nothing that reads layout may run while the reader scrolls.

The core of it holds up. Reading movement from the scroll position rather than from the event stream is the right call and the comment gives the right reason — iOS momentum carries the page long after the finger is gone and does not reliably emit scroll through it, so an event-plus-quiet-timer declares the reader still in the middle of a glide. Sampling one offset per frame costs nothing next to the getBoundingClientRect() it is protecting. The onResize width filter is likewise well argued: nothing any fitter asks is about height (roomFor()/overflows() compare a table against its column, fitChrome() asks whether the menu fits beside the brand), and the one apparent counter-example — a vertical scrollbar appearing — takes width from the content box, so the observer sees it as the width change it is. Using e.contentRect instead of re-measuring inside the callback is the detail that makes it free.

The slot cache in fitTables() attacks the actual cause rather than compensating for the symptom, and keying it on the section frame instead of the table's parent is what makes it work at all — a poll tick hands the container a freshly rendered subtree, so the immediate parent is new on every tick and every lookup would miss. Applying the slot's answer before the first measurement is the ordering that matters: the tall full-width intermediate only ever existed because an unmarked table was laid out before anything had been decided. Keeping rows out of shapeOf() and using a row-count drop as the one signal that a remedy may be liftable is a neat way out of the one-way-ratchet problem without paying for a strip-measure-restore per tick.

Splitting the five passes into five registrations, and moving armGate() into the module that writes .fs-fitted, are both the right shape — but see the fs-fit.js comment: the registration call is still the one uncaught run, so the "page with no tables at all" failure the split was made to close is still reachable through it.

Four findings and one nit inline. The two worth a look before merge are the background-tab interval pause, which leaves LuCI's own poll tick running twice, and the scroller memo, whose cache key does not cover what its predicate measures — on a desktop sidebar layout that can switch the whole mid-scroll guard off. CI on 903785c7: eslint and all four FormalityCheck runs green, the x86_64 build and CodeQL still in progress; nothing failed, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch 2 times, most recently from 0c9e205 to 0b99fa4 Compare August 18, 2026 15:42

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 903785c70b99fa49. Same base (1c70b2b), one commit; the message gained two paragraphs — the web manifest and the boot contract — and both describe hunks that are in the diff, so no commit-check notes. The delta is 393 added / 69 removed across eleven files.

All five threads from the last round are addressed, and the two that mattered are addressed at the cause rather than at the symptom.

The interval pause is right now, and factoring "which id is LuCI's" into pollTickId() is a better answer than the one I suggested: the pause and the navigation sweep now fail the same way — active() first, and a running tick with no readable id means nothing is touched — instead of two callers with two policies. The live field is what makes it work: the map stays keyed on the id the caller is holding, so a view can still clearInterval() its own poller after a trip through a hidden tab, and a paused entry is still visible to clearViewIntervals(), which is the "hide the tab mid-navigation" case the old private list could not see. The spec.live == null → undefined arm of the clearInterval hook is the detail that keeps a freed number from being cleared out from under whoever holds it now.

scroller() reading the computed overflow-y instead of probing overflow is the correct reformulation, and it survives the case that worried me when I checked it: .fs-main carries only overflow-x:clip outside the desktop sidebar layout, and a visible paired with clip computes to hidden, not auto — so the top layout and the narrow layout answer null and fall through to the window, while :root[data-layout="sidebar"]:not([data-narrow]) .fs-main{overflow-y:auto} is the only rule that yields the element. The cache key covers both attributes in that selector, and @media print's overflow:visible is the only other writer. applyAnchor() going through the same function removes the second derivation that could disagree with it inside a frame.

@keyframes fs-fade is back beside fs-spin and the two rules that name it resolve again. fs-fit's add() now catches, which makes fs-select's "registered separately, each pass fails alone" true for the registration run as well.

The scrollWidth === 0 guard in fitTables() is sound on the loop question, which is the thing to get wrong here: .fs-fitted is only ever added, never removed, so the branch that lifts the gate and schedules can fire at most once per table, and a table hidden for somebody else's reason returns on the second pass without asking for another frame. Ordering is right too — applyDecision() writes .fs-fitted itself, so a table with a cached slot answer has already been let into the layout by the time scrollWidth is read.

The manifest's one non-obvious claim checks out against the server: uhttpd's compiled-in map has { "json", "application/json" } and no webmanifest entry at all, with manifest mapped to text/cache-manifest (mimetypes.h:49,92-93), so .json really is the only extension that gets a manifest served as a type a browser will take. The icons hold up as any maskable as well — the artwork sits inside the 80% safe circle in both sizes and the plate is full-bleed, so the maskable crop takes background and nothing else. The boot contract's twelve names all resolve on this luci-base, including the two that could plausibly have been wrong: L.Poll.queue is a real array (luci.js:1070) and base_url is not in the L.env object header.ut emits but is filled in by the constructor before anything can read it (luci.js:2252-2263) — so the check cannot turn the router off on a stock install.

Two findings and one nit inline; none of them is a merge blocker. The dead tokenPad field is the only one that is unambiguously wrong.

CI on 0b99fa49: three FormalityCheck runs completed green, the rest not yet reported. Nothing failed, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 0b99fa4 to 5a3d873 Compare August 19, 2026 06:50
@jow-

jow- commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@jow- Have you formed an opinion about the theme universe's future?

Not fully, but my current reasoning is roughly:

  • bootstrap & footstrap are somewhat close design-wise, so eventually it might make sense to supersede the former with the latter, but only after some settling phase and not immediately, given that bootstrap was the year long default
  • classic OpenWrt is antique and probably should be dropped but I suspect it has a larger user base than 2020
  • OpenWrt 2020, while more modern, feels unpolished by todays standards, receives care-only maintenance and its main advantage, the better mobile friendliness, is adequately covered by modern bootstrap and this new footstrap theme, while it's CI (the blue/grey color schema modeled after the OpenWrt logo rework) could be implemented in terms of a color palette choice on top of footstrap

So I guess it's 60% drop 2020, 40% drop OpenWrt classic for me. In the end I only use bootstrap, and bootstrap was the only theme I seriously spent effort in, so I am not emotionally attached to either 2020 or classic.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 0b99fa495a3d8730. Same base (1c70b2b), one commit. The message gained a paragraph on scroll anchoring and changed "its app icons" to "one app icon"; both describe hunks that are in the diff, so no commit-check notes. The delta is 282 added / 149 removed across eleven files.

All three threads from the last round are addressed. tokenPad is gone and git grep tokenPad now returns nothing in the package, with contentPad left carrying the fallback role on its own. --fs-content-max is in the model: the token resolves to 1280px, .fs-content is box-sizing: border-box (the * reset in @layer base), so max-width includes the gutter and Math.min(outerW - cut, contentMax) - contentPad is the right arithmetic — the old form answered ~2280 for a 1224px column at 2560px. And the router's contract-break line now names a URL that resolves from inside openwrt/luci.

The cascade.css delta is worth saying explicitly: I diffed it declaration by declaration, and every one of the 144 changed lines is 1px solid var(--fs-border)var(--fs-hairline) or the field transition pair → var(--fs-field-transition). Nothing else moved, including the overflow-anchor: none on .table.fs-dt that the new anchoring code depends on.

The anchoring rewrite holds up on its two load-bearing choices. Asking CSS.supports('overflow-anchor', 'auto') rather than a browser name is the right shape, and the localStorage.fsEngineAnchor switch is what makes the WebKit branch reachable without a Mac. The hit test is a real improvement over the frame walk for the reason given — a poll tick that grows something inside the frame the fold cuts through leaves that frame's own top at drift 0 — and the [data-fs-chrome] step down past the sticky bar is safe on the elements that carry the mark: .fs-pattern is pointer-events: none, .fs-skip:not(:focus) is a 1×1 clip at left: 8px and #fs-nav-progress is pointer-events: none, so none of them can swallow the hit test at x = the centre of #view; in the desktop sidebar layout nav.fs-sidebar is position: static beside .fs-main, so it is never under that x either. _rest cannot leak a drifted reference into a correction: run() writes it before scheduleAnchor() sets _anchorPending, but a second mutation batch in that window is discarded by scheduleAnchor's own if (_anchorPending) return, and the rAF re-takes it after applyAnchor(). The > innerHeight + 200 clamp is the right guard for a subtree replacement.

The icon consolidation checks out: app-icon-512.png is a valid 512×512 PNG (indexed, 4679 bytes, down from 15709), nothing in the Makefile or root/ enumerates the two deleted files, and manifest.json still parses with one any maskable entry — which clears Chromium's installability bar, since that asks for one icon of 144px or larger rather than for a 192/512 pair. Pointing apple-touch-icon at the same 512 raster is fine; iOS scales it and never reads the manifest's array.

One finding and one nit inline, neither a merge blocker. The finding is the one worth a look: moving fs-overview out of the prologue also moved the guarantee that its three template globals exist before a view can render, and three comments in the tree still state that guarantee as a fact.

CI on 5a3d8730 is green across all eight checks, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 5a3d873 to 3dd9f2d Compare August 19, 2026 22:39

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the force-push 5a3d87303dd9f2de. Same base (1c70b2b), one commit, message byte-identical and still matching the diff, so no commit-check notes. The delta is 210 added / 71 removed across six files, and it is two themes: the last round's ordering finding, and a second front against the poll tick moving the page under the reader.

The ordering finding is addressed at the cause. The three template globals now live in menu-footstrap-common.js, which the footer requires on every page and which evaluates them at module scope — before wirePageModules() can require fs-overview and before the chrome wires the router, so the guarantee that progressbar/renderBox/renderBadge exist before any view can render is back, and it no longer depends on which of two require() chains wins. The typeof guards still make a full page load a no-op, so the template's own copies keep winning where they exist. fs-menutree.js and fs-router.js were repointed for the move; the file header of fs-overview.js was not (nit inline).

measureShell()'s third key term is right and the reasoning for it is the file's own: .fs-content carries no chrome mark, sheets.scopeToCurrentPage() flips foreign sheets on and off on every client navigation, and neither the width nor the density moves when it does — so a memo keyed on those two alone really would stay pinned at the previous page's gutter. body[data-page] is the one attribute the router always restamps, and it restamps it before the staged render, so the term is stamped by the time anything reads the memo on the incoming page.

Two findings and one nit inline. The one worth a look before merge is the by branch: the clamp receipt is guarded by a page stamp the router writes after it resets the scrollers, and across a promise boundary, so the forward-navigation reset sits inside the window where a clamp and a reset look identical. The other is swapContent(), which measures a height and restores the pin in the same statement sequence — no layout ever runs with the pin applied, so it cannot be doing the work its comment credits it with.

CI on 3dd9f2de: three FormalityCheck runs completed green, the rest not yet reported. Nothing failed, so nothing here is CI-grounded.


Generated by Claude Code

Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js Outdated
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
Comment thread themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js Outdated
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 3dd9f2d to 407c9b7 Compare August 20, 2026 06:10
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 <style>,
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 <vizzlef@gmail.com>
@VizzleTF
VizzleTF force-pushed the luci-theme-footstrap branch from 407c9b7 to f3f0ab7 Compare August 20, 2026 11:10
@hnyman

hnyman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@jow-

Based on your comment:

So I guess it's 60% drop 2020, 40% drop OpenWrt classic for me. In the end I only use bootstrap, and bootstrap was the only theme I seriously spent effort in, so I am not emotionally attached to either 2020 or classic.

I suggest that we

  • merge this new theme as it is, just to get the feedback from wider user populace.
  • if there is no major negative feedback (or the needed changes are fixed/implemented quickly), we could at end of September remove the 2020 theme and make this to PROVIDE it.
  • I would also remove the ancient OpenWrt theme at that time, as Bootstrap has been the default now for so many years, that you really need to be old-school to have seen that as natural.

Ps. The AI based tweaking seems to be continued forever, so getting all minor points addressed like a never-ending story. Let's merge it now.

@VizzleTF

VizzleTF commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

The AI based tweaking seems to be continued forever

@hnyman i got reviews by users and fix small SPA bugs on some browsers (safari, firefox) - thats why its looks like that.
But i guess it could be merged like this

@VizzleTF

Copy link
Copy Markdown
Contributor Author

button pointed to the right

@castillofrancodamian did it

@hnyman
hnyman merged commit 6f08de7 into openwrt:master Aug 20, 2026
8 checks passed
@hnyman

hnyman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

I merged this in.
Let's see, what feedback you get (and if there are bugs not found so far).

@hnyman

hnyman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@VizzleTF
One wish for further development, regarding the package infra:

Your current uci-defaults file is quite verbose and makes lots of cleanup actions based on assumption that there might have been old Footstrap versions.

In the next few weeks (by mid-September or so), you should clean up the uci-defaults file in the main LuCI repo, and start from assumption that the users of the officially downloadable package have started from the current now published first version. There may naturally be a few old initial early adopters / test users, but even most of them will have upgraded to the new "official" version by then.
(And in general, OpenWrt pretty much starts from the assumption that users rather sysupgrade the whole firmware image than upgrade individual packages. So cleanup is more limited to config than to removing old left-overs, as they usually disappear in sysupgrade in any case.)

So, please cleanup and shorten the uci-defaults file to be more in line with the other themes, and remove/shorten most of the verbose commentary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

add package Introduces a new package Makefile build script

Projects

None yet

Development

Successfully merging this pull request may close these issues.