Skip to content

Repository files navigation

Quiet — a LuCI theme

A low-chrome theme for OpenWrt's LuCI web interface, with a light/dark preference that is resolved before first paint, so the page never flashes.

Quiet login page, light Quiet login page, dark

Install

One command. Paste it into your router over SSH:

wget -O /tmp/quiet.ipk https://github.com/ovexro/luci-theme-quiet/releases/latest/download/luci-theme-quiet_all.ipk && opkg install /tmp/quiet.ipk

Reload the page and you're on Quiet. That's the whole thing — the package registers the theme and selects it, because nothing else would.

If wget complains about certificates

Some minimal builds ship without CA certificates. Either install them — opkg update && opkg install ca-bundle — or fetch once with wget --no-check-certificate. The package's own integrity does not depend on the transport: every release lists a sha256 you can check with sha256sum /tmp/quiet.ipk.

Uninstall, just as cleanly:

opkg remove luci-theme-quiet

That puts you back on Bootstrap before it deletes anything, so removing the theme can never leave you staring at an unstyled page.

Switch back without uninstalling — System → System → Language and Style → Theme, or:

uci set luci.main.mediaurlbase=/luci-static/bootstrap; uci commit luci; rm -f /tmp/luci-indexcache.*

Requires luci-base and luci-theme-bootstrap, both of which you already have. Architecture: all, so it installs on any target. Built against OpenWrt 24.10.x / LuCI 26.x.


What it looks like

Light Dark
Network configuration page, light Network configuration page, dark
Quiet on a phone, light Quiet on a phone, dark Quiet login page on a phone

The light/dark control sits in the top-right corner, and on the login page too. It has three states, not two — light, dark, and follow the system — so it can stop having an opinion once you've expressed one. Alt-click returns it to automatic. The choice persists in localStorage and is applied by an inline script in <head>, which is why there is no flash of the wrong theme on load.


How it is built

cascade.css and mobile.css are the stock luci-theme-bootstrap sheets verbatim, followed by an override layer. Every widget the layer never mentions keeps upstream's own correct styling — which is the point of the architecture, and also its one sharp edge (see the four failure modes).

mobile.css additionally rewrites stock's deprecated max-device-width to max-width. That is not cosmetic: both of stock's media queries are gated on it, and max-device-width tracks the physical screen rather than the viewport, so at 400% browser zoom or in a narrow window on a large display, none of the responsive rules apply at all. WCAG 2.1 SC 1.4.10, Reflow.

./build.sh          # regenerate src/{cascade,mobile}.css and the ?v= token
./build-ipk.sh      # -> dist/luci-theme-quiet_<version>_all.ipk + .manifest
./deploy.sh         # push loose files over ssh (the fast dev loop)
./leak-check.sh     # refuse to publish anything naming your infrastructure
./release.sh 1.0-r2 "what changed"

build.sh is the single implementation of the build, and everything else calls it. A plain cat does not reproduce cascade.css — the two blank lines between the stock sheet and the layer are load-bearing for every md5 comparison in the repo. Three scripts each carried their own copy of those concatenations once; that is how they drift.

The .ipk build is reproducible: the same sources produce the same sha256. RELEASE_EPOCH in build-ipk.sh is the timestamp for every archive entry — bump it alongside VERSION. Timestamps are deliberately not taken from file mtimes, because build.sh rewrites four files on every run and a fresh clone stamps everything at checkout time.

build-ipk.sh verifies the finished archive against a layout table written out independently of the install calls that produced it. The duplication is the point and must not be refactored away: driving both from one list was tried, and it silently passes a wrong destination and a wrong file mode. Mutation-tested — a bad mode, a bad destination, a smuggled extra file, a stale or missing ?v= token, tampered content, and a dropped LICENSE all fail the build.

Layout

src/            the theme sources; cascade.css and mobile.css here are BUILT
stock/          upstream's cascade.css and mobile.css, never modified
package/        control.in + the opkg maintainer scripts + uci-defaults
docs/           screenshots used by this page
dist/           built .ipk and its per-file manifest

deploy.sh needs a deploy.conf — copy deploy.conf.example. That file holds the only router-specific values anywhere in this repo and is gitignored; build-ipk.sh needs none of them and runs entirely offline.


Notes for anyone changing it

The override layer has four known failure modes

Each of these shipped a silent, real defect at least once:

  1. Specificity loss. A more specific vendor rule drops your declaration with no error anywhere. Mirror the vendor's exact selector rather than inventing a shorter one.
  2. Leftover geometry. When your rule changes a box, every vendor value pinned to the old geometry is now wrong — including properties you never mentioned.
  3. Disabled mechanism. When the vendor steers behaviour through a utility class (.hidden) or a single property (height:0, margin-top:auto), an equal-specificity restatement in your layer silently turns it off. Restate utility classes last and bare.
  4. Deletion. Removing a line from an override layer does not return the element to your other rule — it hands the element to whatever wins next in the whole cascade, and the vendor's variant rules are usually more specific than your base rule. Before deleting, ask what paints it once your line is gone, and answer by measuring.

Two template hazards that are invisible until they bite

  • Never emit a <button> before the Log in button. The reused bootstrap.sysauth view binds both the click handler and the Enter key to document.querySelector('button'), and the Log in button sits outside the <form> with no form= attribute — so that binding is the only submit path there is. header.ut renders ahead of sysauth.ut, so a control emitted from the header becomes the first button and login becomes impossible from a browser. The light/dark control on the login page is a <div role="button"> for exactly this reason.
  • <main id="maincontent"> is a two-file tag pair — it opens in header.ut and closes in footer.ut. Change one and you must change the other.

Why luci-theme-bootstrap is a hard dependency

The templates reuse its menu-bootstrap.js renderer and its bootstrap.sysauth login view unchanged, in place, rather than copying them. Remove that package and this theme has no menu and no way to log in.


Licence

Apache-2.0. See LICENSE, and NOTICE for exactly which bytes are upstream's, which were modified and how. Both are installed to /usr/share/luci-theme-quiet/ on the router.

Derived from luci-theme-bootstrap, part of the OpenWrt LuCI project.

About

A low-chrome LuCI theme for OpenWrt, with a light/dark preference resolved before first paint. Ships as an opkg package.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages