🎄 Sync 2026-07-26 03:59:09

This commit is contained in:
github-actions[bot] committed 2026-07-26 03:59:09 +08:00
1 parent 85de1d9922
commit 6efc8c18ce
68 files changed
+1556 -9154

No files matched your search

@@ -45,7 +45,7 @@ label, fixing a typo, adjusting a translation string).
- **S4** — Memory has a ceiling. → references/server.md
- **L1** — Kill render-blocking requests. → references/loading.md
- **L2** — Download once, cache forever. → references/loading.md
- **L3** — Ship compressed. → references/loading.md
- **L3** — Make identity-transfer assets compact. → references/loading.md
- **R1** — Animate compositor properties only. → references/runtime.md
- **R2** — JS must not force synchronous layout. → references/runtime.md
- **R3** — Accessibility is functionality. → references/runtime.md
@@ -11,8 +11,12 @@ address, so they stay local).
| Metric | Budget | Track | Source |
|---|---|---|---|
| main.css (gzip-transferred) | ≤ 30 KB | size | measured 2026-07 (28 KB) |
| Per-page cold transfer (all theme assets, gzip) | ≤ 60 KB | size | sum of current gzip sizes + headroom |
| main.css (identity/raw) | ≤ 190 KB | size | production build, 2026-07 (183,820 B) |
| login.css (identity/raw) | ≤ 12 KB | size | production build, 2026-07 (10,935 B, token-pruned) |
| menu-aurora.js (identity/raw) | ≤ 20 KB | size | production build, 2026-07 (19,100 B) |
| Default logo (identity/raw) | ≤ 16 KB | size | production build, 2026-07 (15,057 B) |
| Core admin cold theme assets (identity/raw) | ≤ 250 KB | size | main CSS + menu JS + default font + logo, 2026-07 (241,557 B) |
| Login cold theme assets, excluding configured background (identity/raw) | ≤ 55 KB | size | login CSS + default font + logo, 2026-07 (49,572 B) |
| Blocking requests before first paint | ≤ 4 | count | current waterfall |
| Repeat-visit asset requests | ≈ 0 | count | target state; package-built CSS/JS URLs are versioned, but long-lived cache headers still need live verification |
| TTFB, login page (device) | proposed: ≤ 130 ms | latency | local device baseline, 2026-07 |
@@ -25,16 +29,22 @@ Budget revisions require a new baseline entry under `../baselines/`.
## Optimization ledger
### Landed
(compositor animation rework; mega-menu idle pre-measurement; on-demand
patches; `font-display: swap`)
- Compositor animation rework; mega-menu idle pre-measurement; on-demand
patches; `font-display: swap` and inline `@font-face` CSS.
- Tailwind `source(none)`, login-only Preflight removal, native scrollbar
styling, and local fade animation; unused CSS plugins removed.
- Shared SVG custom properties prevent repeated mask payloads.
- Terser compression/mangling with LuCI loader directives preserved.
- Default logo raster resized inside its compatibility SVG wrapper.
- Login template reuses its board/UCI reads when including `header.ut`.
- Package-root `.DS_Store` metadata removed and covered by a regression test.
- login.css pruned to its reachable custom properties at build time (the
shared token sheet is admin-sized; the login page consumes a fraction).
### Pending
| Item | Principle | Estimated gain |
|---|---|---|
| Precompressed `.gz` assets | S1+L3 | ~260 KB → ~35 KB cold |
| Terser `compress`+`mangle` in `vite.config.ts` | L3 | ~20 KB → ~10 KB |
| Inline `@font-face` + preload woff2 | L1 | −1 blocking RTT |
| SVGO `logo.svg` | L3 | 45 KB → est. < 20 KB |
| Long-lived cache headers for versioned CSS/JS | L2 | after LuCI build-time `?v=$(PKG_VERSION)`, kills per-click 304s if headers permit disk/memory cache reuse |
| `defer` head scripts | L1 | needs on-device timing verification |
@@ -50,6 +60,9 @@ patches; `font-display: swap`)
`/luci-static/aurora/login.css?v=1.0.7`. Do not re-propose manual
cache-versioning for these links unless inspecting the installed package
or live HTML proves the rewrite did not happen.
- **No gzip on the target uhttpd** — budgets and the bench harness use raw
identity bytes. Do not add precompressed sidecars unless the deployed HTTP
server is changed and live response headers prove negotiation works.
### Accepted exceptions
@@ -54,21 +54,23 @@ served from disk/memory cache).
**Quantify.** Repeat-visit request count.
## L3 — Ship compressed
## L3 — Make identity-transfer assets compact
**Why.** An uncompressed asset costs both wire time on the slow link (see
The Wire) and CPU on the slow server (see server.md S1) — compression is the
one change that helps both planes at once.
**Why.** This deployment's stock `uhttpd` serves identity responses even when
the browser advertises gzip. Raw output size is therefore both flash cost and
wire cost; minification and dead-code removal must do the work that transfer
compression normally would.
**Do / Don't.** Precompress at build time so `uhttpd` can serve `foo.gz`
verbatim (cross-reference server.md S1). Run shipped SVGs through SVGO
(example: aurora's `logo.svg`, which doubles as the favicon, ships at 45 KB —
an unoptimized SVG is a common place for dead editor metadata to hide).
Choose sane image formats (WebP/AVIF over unoptimized PNG/JPEG) for anything
that isn't vector.
**Do / Don't.** Disable broad Tailwind content scanning when all utilities are
declared with `@apply`; omit Preflight from isolated entries that only need a
small reset; remove unused plugins; minify and mangle JS while preserving
LuCI's loader directives; define repeated SVG data URLs once as custom
properties. Resize or re-encode raster assets at their real display needs and
keep generated metadata out of package roots. Do not ship precompressed
sidecars when the target server cannot negotiate them.
**Verify.** `bench.mjs`'s gzip-negotiated rows; `du` on the build output
directory to catch anything that grew.
**Verify.** `bench.mjs`'s identity rows, automated raw-size budgets, and `du`
on the build output directory to catch anything that grew.
**Quantify.** Per-page transferred bytes (cold load); individual asset
sizes.
@@ -79,5 +81,5 @@ sizes.
|---|---|---|
| Blocking requests before first paint | DevTools Network (Slow 4G) | `aurora-budgets.md` |
| Repeat-visit request count | DevTools Network | `aurora-budgets.md` |
| Transferred bytes (cold, gzip) | `bench.mjs` | `aurora-budgets.md` |
| Transferred bytes (cold, identity/raw) | `bench.mjs` | `aurora-budgets.md` |
| Individual asset size | `du` / `bench.mjs` | `aurora-budgets.md` |
@@ -23,15 +23,19 @@ or returns an HTTP error status mid-run, it aborts with a one-line message
naming the target that failed — it does not silently skip it.
**What it measures.** `BENCH_RUNS` (default 10) samples per target via
`curl`, reduced to a median, against 5 fixed targets:
`curl`, reduced to a median, against 6 fixed targets. All static assets use
identity transfer because the target `uhttpd` cannot serve gzip:
1. `GET /cgi-bin/luci` (the login page) — the honest unauthenticated target:
it's the one page a bench script can hit without a session, and TTFB
comparisons must compare like with like.
2. `main.css`, identity (no `Accept-Encoding`)
3. `main.css`, gzip-negotiated
4. `login.css`, gzip-negotiated
5. the theme's menu script, gzip-negotiated (example: aurora's `menu-aurora.js`)
A 403 is accepted for this target only because some LuCI builds render the
login form with that status; other error responses still abort the run.
2. `main.css`, identity
3. `login.css`, identity
4. the theme's menu script, identity (example: aurora's `menu-aurora.js`)
5. the default first-paint font, identity
6. the default logo, identity
Per target it reports TTFB, total time, transferred bytes, and HTTP status,
summarized as a Markdown table on stdout.
@@ -11,20 +11,18 @@ same 1–2 cores and 64–512 MB of RAM as the rest of OpenWrt. Nothing here is
is already slow. Anything that can be computed once, at build time, should
never run again inside a request handler.
**Do / Don't.** Precompress assets at build time and let `uhttpd` serve the
precompressed file verbatim when the client sends `Accept-Encoding: gzip` —
this makes the request-time cost *decrease*, since `uhttpd` has no dynamic
compression of its own. Generate derived CSS/config/tokens at build time,
never per-request (example: aurora's generated `_tokens.css`, built from
`.dev/tokens/` and never touched at runtime). When a genuinely per-request
computation is unavoidable, record the accepted exception with its rationale
in the theme budget sheet (`aurora-budgets.md`) so a future session doesn't re-litigate it
(example: aurora's per-request `lsdir()` patch discovery in `header.ut` — one
cheap readdir, measured negligible and accepted as-is).
**Do / Don't.** Generate and minify derived CSS, JS, config, tokens and image
assets at build time, never per-request (example: aurora's generated
`_tokens.css`). This deployment uses stock `uhttpd` without gzip support, so
raw file size is the wire size: do not add `.gz` files that the server will
never select. When a genuinely per-request computation is unavoidable,
record the accepted exception with its rationale in the theme budget sheet
(`aurora-budgets.md`) so a future session doesn't re-litigate it (example:
aurora's per-request `lsdir()` patch discovery in `header.ut` — one cheap
readdir, measured negligible and accepted as-is).
**Verify.** `top` on the device shows no request-time compression process;
`curl -H 'Accept-Encoding: gzip' -w '%{size_download}'` against the asset
shows the byte drop versus an uncompressed request.
**Verify.** A production build is deterministic and already minified; the
identity-transfer rows in `bench.mjs` match the files' raw byte sizes.
**Quantify.** Transferred bytes; package/flash size delta.
@@ -55,10 +53,10 @@ the branch against the base.
slow link, and it costs it on every page view — this is not "just bandwidth,"
it's compute on the same constrained cores serving the request.
**Do / Don't.** Set (and respect) a per-page transferred-bytes budget.
Prefer gzip-negotiated transfer over raw bytes wherever the client supports
it (example: aurora's `main.css` is 211,725 B raw but ~28 KB gzip — always
measure and budget the negotiated size, not the raw one).
**Do / Don't.** Set (and respect) a per-page transferred-bytes budget. On this
stock `uhttpd` deployment, measure identity responses and optimize the raw
files themselves: restrict Tailwind source detection, remove unused framework
layers/plugins, minify JS, deduplicate inlined SVGs, and resize raster assets.
**Verify.** The `bench.mjs` bytes column, compared against the theme's own
budget table.
@@ -85,5 +83,5 @@ after the change.
|---|---|---|
| TTFB | `bench.mjs` | `aurora-budgets.md` |
| ubus/uci/fs call count | diff review | `aurora-budgets.md` |
| Transferred bytes (gzip) | `bench.mjs` | `aurora-budgets.md` |
| Transferred bytes (identity/raw) | `bench.mjs` | `aurora-budgets.md` |
| uhttpd VmRSS | `bench.mjs` device report | `aurora-budgets.md` |
@@ -20,8 +20,12 @@ export function parseCurlMetrics(line) {
};
}
export function assertOkStatus(sample) {
if (!Number.isInteger(sample.status) || sample.status < 200 || sample.status >= 400) {
export function assertOkStatus(sample, allowedStatuses = []) {
if (
!Number.isInteger(sample.status) ||
(!(sample.status >= 200 && sample.status < 400) &&
!allowedStatuses.includes(sample.status))
) {
throw new Error(`HTTP ${sample.status}`);
}
return sample;
@@ -44,6 +44,11 @@ test("assertOkStatus: rejects 404 samples", () => {
);
});
test("assertOkStatus: permits an explicitly expected login-page 403", () => {
const sample = parseCurlMetrics("0.010000\t0.020000\t5788\t403\n");
assert.equal(assertOkStatus(sample, [403]), sample);
});
test("summarize: medians of times, first sample's bytes/status, run count", () => {
const s = summarize([
{ ttfbMs: 50, totalMs: 200, bytes: 1000, status: 200 },
@@ -49,11 +49,19 @@ const CURL_FORMAT = "%{time_starttransfer}\\t%{time_total}\\t%{size_download}\\t
// unauthenticated bench can measure, and TTFB comparisons must compare like
// with like (spec: error handling / edge cases).
const TARGETS = [
{ label: "GET /cgi-bin/luci (login page)", path: "/cgi-bin/luci" },
{
label: "GET /cgi-bin/luci (login page)",
path: "/cgi-bin/luci",
allowedStatuses: [403],
},
{ label: "main.css (identity)", path: "/luci-static/aurora/main.css" },
{ label: "main.css (gzip negotiated)", path: "/luci-static/aurora/main.css", gzip: true },
{ label: "login.css (gzip negotiated)", path: "/luci-static/aurora/login.css", gzip: true },
{ label: "menu-aurora.js (gzip negotiated)", path: "/luci-static/resources/menu-aurora.js", gzip: true },
{ label: "login.css (identity)", path: "/luci-static/aurora/login.css" },
{ label: "menu-aurora.js (identity)", path: "/luci-static/resources/menu-aurora.js" },
{
label: "Lato regular font (identity)",
path: "/luci-static/aurora/fonts/lato-v24-latin-regular.woff2",
},
{ label: "logo.svg (identity)", path: "/luci-static/aurora/images/logo.svg" },
];
async function measureOnce(target) {
@@ -63,10 +71,12 @@ async function measureOnce(target) {
"-w", CURL_FORMAT,
"--max-time", "30",
];
if (target.gzip) args.push("-H", "Accept-Encoding: gzip");
args.push(`${HOST}${target.path}`);
const { stdout } = await execFileAsync("curl", args);
return assertOkStatus(parseCurlMetrics(stdout));
return assertOkStatus(
parseCurlMetrics(stdout),
target.allowedStatuses ?? [],
);
}
async function measureTarget(target) {
+2 -2
View File
@@ -103,8 +103,8 @@ This will be compiled to standard CSS that works in all browsers.
The theme has two independent Tailwind CSS v4 entry points, both sourced from `.dev/src/media/`:
- **`main.css`** — the LuCI admin UI. It contains no rules of its own; it's a pure import manifest that pulls in (in order) `_tokens.css` (OKLCH theme tokens, mapped via `@theme inline`), `_base.css`, `_elements.css`, `_layout.css`, every file in `components/` (one partial per UI component — buttons, cards, modals, tables, etc.), and `_utilities.css`.
- **`login.css`** — the standalone login page (`sysauth.ut`). Self-contained: imports Tailwind and `_tokens.css` directly.
- **`main.css`** — the LuCI admin UI. It is an import manifest that disables Tailwind's automatic source scan (`source(none)`) and pulls in (in order) `_tokens.css` (OKLCH theme tokens, mapped via `@theme inline`), shared `_icons.css`, `_base.css`, `_elements.css`, `_layout.css`, every file in `components/` (one partial per UI component — buttons, cards, modals, tables, etc.), and `_utilities.css`.
- **`login.css`** — the standalone login page (`sysauth.ut`). Self-contained: imports Tailwind theme/utilities with `source(none)`, omits full Preflight in favor of a tiny local reset, and imports `_tokens.css` directly. At build time the `login-css-prune` plugin (`vite.config.ts`) strips every custom property the page's var() chains never reach, so the admin-sized token sheet ships login-sized.
Third-party compatibility patches are **not** bundled into `main.css` — they are split into per-page files under `media/patches/` and loaded on demand (see [On-Demand Third-Party Patches](#on-demand-third-party-patches) below).
-2
View File
@@ -22,10 +22,8 @@
"lightningcss": "^1.32.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.11",
"terser": "^5.43.1",
"tw-animate-css": "^1.4.0",
"vite": "^7.1.1"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
-52
View File
@@ -32,18 +32,12 @@ importers:
prettier-plugin-tailwindcss:
specifier: ^0.7.0
version: 0.7.2(prettier@3.8.1)
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@19.1.1)(tailwindcss@4.3.2)
tailwindcss:
specifier: ^4.1.11
version: 4.3.2
terser:
specifier: ^5.43.1
version: 5.46.0
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
vite:
specifier: ^7.1.1
version: 7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)
@@ -466,9 +460,6 @@ packages:
'@types/node@24.10.9':
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
@@ -490,10 +481,6 @@ packages:
caniuse-lite@1.0.30001799:
resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
colorjs.io@0.6.1:
resolution: {integrity: sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==}
@@ -698,15 +685,6 @@ packages:
engines: {node: '>=14'}
hasBin: true
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
rollup@4.57.1:
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -723,12 +701,6 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 4.x
tailwindcss@4.3.2:
resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
@@ -745,9 +717,6 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
@@ -1054,8 +1023,6 @@ snapshots:
dependencies:
undici-types: 7.16.0
'@types/prismjs@1.26.5': {}
acorn@8.15.0: {}
baseline-browser-mapping@2.10.37: {}
@@ -1072,8 +1039,6 @@ snapshots:
caniuse-lite@1.0.30001799: {}
clsx@2.1.1: {}
colorjs.io@0.6.1: {}
commander@2.20.3: {}
@@ -1202,14 +1167,6 @@ snapshots:
prettier@3.8.1: {}
prism-react-renderer@2.4.1(react@19.1.1):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 19.1.1
react@19.1.1: {}
rollup@4.57.1:
dependencies:
'@types/estree': 1.0.8
@@ -1250,13 +1207,6 @@ snapshots:
source-map@0.6.1: {}
tailwind-scrollbar@4.0.2(react@19.1.1)(tailwindcss@4.3.2):
dependencies:
prism-react-renderer: 2.4.1(react@19.1.1)
tailwindcss: 4.3.2
transitivePeerDependencies:
- react
tailwindcss@4.3.2: {}
tapable@2.3.3: {}
@@ -1273,8 +1223,6 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tw-animate-css@1.4.0: {}
undici-types@7.16.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.2):
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><image width="512" height="512" href="data:image/png;base64,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 truncated
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><image width="512" height="512" href="data:image/png;base64,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 truncated

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760890864667" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18898" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M918.8 83.7c-43.5-30.3-104-19.5-134.6 24.1l-425.8 606-127-135.7c-36.4-38.9-98.1-41.1-137-4.8-39 36.2-41.1 97.7-4.7 136.6l206.5 220.7c4.7 5 9.8 9.4 15.1 13.1 0.7 0.5 1.4 1 2.2 1.5 43.5 30.3 104 19.5 134.6-24.1l494-703.2c30.7-43.5 20.1-103.9-23.3-134.2z" fill="#0f162b" p-id="18899"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M918.8 83.7c-43.5-30.3-104-19.5-134.6 24.1l-425.8 606-127-135.7c-36.4-38.9-98.1-41.1-137-4.8-39 36.2-41.1 97.7-4.7 136.6l206.5 220.7c4.7 5 9.8 9.4 15.1 13.1.7.5 1.4 1 2.2 1.5 43.5 30.3 104 19.5 134.6-24.1l494-703.2c30.7-43.5 20.1-103.9-23.3-134.2z"/></svg>

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 330 B

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758357645269" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7610" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 85.333333c235.648 0 426.666667 191.018667 426.666667 426.666667s-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512 276.352 85.333333 512 85.333333z m0 128a298.666667 298.666667 0 1 0 0 597.333334 298.666667 298.666667 0 0 0 0-597.333334z" fill="#000000" fill-opacity=".05" p-id="7611"></path><path d="M813.696 813.696c166.613333-166.613333 166.613333-436.778667 0-603.392-166.613333-166.613333-436.778667-166.613333-603.392 0A64 64 0 0 0 300.8 300.8a298.666667 298.666667 0 1 1 422.4 422.4 64 64 0 0 0 90.496 90.496z" fill="#000000" p-id="7612"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path opacity=".05" d="M512 85.333c235.648 0 426.667 191.019 426.667 426.667S747.648 938.667 512 938.667 85.333 747.648 85.333 512 276.352 85.333 512 85.333zm0 128a298.667 298.667 0 1 0 0 597.334 298.667 298.667 0 0 0 0-597.334z"/><path d="M813.696 813.696c166.613-166.613 166.613-436.779 0-603.392s-436.779-166.613-603.392 0a64 64 0 0 0 90.496 90.496 298.667 298.667 0 1 1 422.4 422.4 64 64 0 0 0 90.496 90.496z"/></svg>

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 486 B

@@ -1,9 +1,9 @@
*:not(html) {
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-transparent scrollbar-thumb-text-muted;
@apply [scrollbar-color:var(--text-muted)_transparent] [scrollbar-width:thin];
}
html {
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-surface scrollbar-thumb-text-muted;
@apply [scrollbar-color:var(--text-muted)_var(--surface)] [scrollbar-width:thin];
body {
@apply text-text relative flex min-h-dvh flex-col text-sm leading-relaxed font-normal;
@@ -0,0 +1,32 @@
/* Shared encoded icon payloads. Referencing these variables from masks keeps
Vite from inlining the same SVG once per selector and vendor declaration. */
:root {
@apply [--icon-activity:url('@assets/icons/activity.svg')];
@apply [--icon-adjustments:url('@assets/icons/adjustments.svg')];
@apply [--icon-apps:url('@assets/icons/apps.svg')];
@apply [--icon-arrow-down:url('@assets/icons/arrow-down.svg')];
@apply [--icon-arrow-down-dark:url('@assets/icons/arrow-down-dark.svg')];
@apply [--icon-arrow-right:url('@assets/icons/arrow-right.svg')];
@apply [--icon-box:url('@assets/icons/box.svg')];
@apply [--icon-category:url('@assets/icons/category.svg')];
@apply [--icon-chart-bar:url('@assets/icons/chart-bar.svg')];
@apply [--icon-check:url('@assets/icons/check.svg')];
@apply [--icon-cpu:url('@assets/icons/cpu.svg')];
@apply [--icon-error:url('@assets/icons/error.svg')];
@apply [--icon-gauge:url('@assets/icons/gauge.svg')];
@apply [--icon-info:url('@assets/icons/info.svg')];
@apply [--icon-package:url('@assets/icons/package.svg')];
@apply [--icon-phone:url('@assets/icons/phone.svg')];
@apply [--icon-radio:url('@assets/icons/radio.svg')];
@apply [--icon-refresh:url('@assets/icons/refresh.svg')];
@apply [--icon-refresh-off:url('@assets/icons/refresh-off.svg')];
@apply [--icon-router:url('@assets/icons/router.svg')];
@apply [--icon-search:url('@assets/icons/search.svg')];
@apply [--icon-server:url('@assets/icons/server.svg')];
@apply [--icon-settings:url('@assets/icons/settings.svg')];
@apply [--icon-shield:url('@assets/icons/shield.svg')];
@apply [--icon-spinning:url('@assets/icons/spinning.svg')];
@apply [--icon-tag:url('@assets/icons/tag.svg')];
@apply [--icon-world:url('@assets/icons/world.svg')];
@apply [--icon-x:url('@assets/icons/x.svg')];
}
+18 -14
View File
@@ -114,9 +114,12 @@ header {
/* First-level icon: mask + currentColor so it follows the theme.
--menu-icon comes from the shared name → icon map in
components/_nav.css; unmapped → default. */
components/_nav.css; unmapped → the var() fallback. The default
must live in the fallback, not as a --menu-icon declaration
here: this nested chain compiles to (0,5,1) and would outrank
every (0,2,0) [data-section] mapping. */
&::before {
@apply size-6 shrink-0 bg-current content-[''] [mask:var(--menu-icon,url('@assets/icons/category.svg'))_center/contain_no-repeat];
@apply size-6 shrink-0 bg-current content-[''] [mask:var(--menu-icon,var(--icon-category))_center/contain_no-repeat];
}
}
}
@@ -165,19 +168,19 @@ header {
}
& .board-line-host::before {
@apply [mask:url('@assets/icons/router.svg')_center/contain_no-repeat];
@apply [mask:var(--icon-router)_center/contain_no-repeat];
}
& .board-line-model::before {
@apply [mask:url('@assets/icons/cpu.svg')_center/contain_no-repeat];
@apply [mask:var(--icon-cpu)_center/contain_no-repeat];
}
& .board-line-firmware::before {
@apply [mask:url('@assets/icons/package.svg')_center/contain_no-repeat];
@apply [mask:var(--icon-package)_center/contain_no-repeat];
}
& .board-line-kernel::before {
@apply [mask:url('@assets/icons/tag.svg')_center/contain_no-repeat];
@apply [mask:var(--icon-tag)_center/contain_no-repeat];
}
}
}
@@ -318,21 +321,21 @@ header {
@apply before:text-text size-5 cursor-pointer text-[0px] transition-transform duration-150 before:absolute before:size-5 before:bg-current hover:scale-105 active:scale-95;
&[data-indicator="media_error"] {
@apply before:[mask:url('@assets/icons/error.svg')_center/cover_no-repeat];
@apply before:[mask:var(--icon-error)_center/cover_no-repeat];
}
&[data-indicator="poll-status"] {
&[data-style="active"] {
@apply before:[mask:url('@assets/icons/refresh.svg')_center/cover_no-repeat];
@apply before:[mask:var(--icon-refresh)_center/cover_no-repeat];
}
&[data-style="inactive"] {
@apply before:[mask:url('@assets/icons/refresh-off.svg')_center/cover_no-repeat];
@apply before:[mask:var(--icon-refresh-off)_center/cover_no-repeat];
}
}
&[data-indicator="uci-changes"] {
@apply relative before:[mask:url('@assets/icons/settings.svg')_center/cover_no-repeat];
@apply relative before:[mask:var(--icon-settings)_center/cover_no-repeat];
&[data-count]:not([data-count="0"]) {
@apply after:bg-danger after:text-on-brand after:absolute after:-top-0.5 after:-right-0.5 after:z-10 after:flex after:min-h-3 after:min-w-3 after:items-center after:justify-center after:rounded-full after:px-0.5 after:text-[8px] after:leading-none after:font-bold after:shadow-sm after:content-[attr(data-count)];
@@ -349,9 +352,9 @@ body[data-nav-type="sidebar"] {
reads as the content area's toolbar, not a page-wide bar.
A CSS grid (not flex-wrap) keeps the header row at its natural height —
so collapsing the sidebar never stretches it — lets the sidebar column
animate to zero, and lets #maincontent fluidly reclaim the freed space.
collapse to zero, and lets #maincontent reclaim the freed space.
overflow-x-clip keeps the sliding sidebar out of horizontal overflow. */
@apply md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip md:transition-[grid-template-columns] md:duration-[250ms] md:ease-out;
@apply md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip;
&.sidebar-collapsed {
@apply md:grid-cols-[0_minmax(0,1fr)];
@@ -396,8 +399,9 @@ body[data-nav-type="sidebar"] {
@apply border-hairline sticky top-0 hidden h-dvh w-full overflow-hidden rounded-none border-r transition-[visibility] duration-[250ms] md:block;
}
/* Visibility flips at the end of the slide so collapsed controls leave the
tab order without clipping the animation. */
/* The grid column snaps closed to avoid a full-page layout animation.
Visibility removes collapsed controls from the tab order; on expansion,
the inner panel still enters via compositor-only translate + opacity. */
&.sidebar-collapsed .sidebar-panel {
@apply invisible;
}
@@ -51,8 +51,14 @@
}
}
@keyframes aurora-fade-in {
from {
opacity: 0;
}
}
.fade-in {
@apply animate-in fade-in-0 fill-mode-backwards duration-[250ms];
@apply animate-[aurora-fade-in_250ms_ease_both];
}
.fade-out {
@@ -61,7 +67,7 @@
.spinning {
@apply relative inline-block pl-8!;
@apply before:absolute before:top-1/2 before:left-[6px] before:size-5 before:-translate-y-1/2 before:animate-spin before:bg-current before:[mask:url('@assets/icons/spinning.svg')_center/cover_no-repeat];
@apply before:absolute before:top-1/2 before:left-[6px] before:size-5 before:-translate-y-1/2 before:animate-spin before:bg-current before:[mask:var(--icon-spinning)_center/cover_no-repeat];
#view > & {
@apply first:mx-auto first:grid first:min-h-48 first:w-fit first:place-items-center first:max-md:min-h-[calc(100dvh-4rem)];
}
@@ -27,11 +27,11 @@
@apply relative size-5 cursor-pointer text-[0px] before:absolute before:size-5 before:bg-current before:transition-transform before:duration-150 before:ease-out hover:before:scale-110 active:before:scale-95;
&[data-style="active"] {
@apply before:rotate-0 before:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat];
@apply before:rotate-0 before:[mask:var(--icon-arrow-right)_center/cover_no-repeat];
}
&[data-style="inactive"] {
@apply before:rotate-90 before:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat];
@apply before:rotate-90 before:[mask:var(--icon-arrow-right)_center/cover_no-repeat];
}
}
}
@@ -28,7 +28,7 @@
@apply text-text-muted relative mt-1 pl-4 leading-none wrap-break-word;
&:not(:empty) {
@apply before:bg-info before:absolute before:-left-0.5 before:inline-block before:size-4 before:[mask:url('@assets/icons/info.svg')_center/cover_no-repeat];
@apply before:bg-info before:absolute before:-left-0.5 before:inline-block before:size-4 before:[mask:var(--icon-info)_center/cover_no-repeat];
}
}
}
@@ -27,11 +27,11 @@ input[type="checkbox"] {
}
input[type="radio"] {
@apply before:rounded-full after:[mask:url('@assets/icons/radio.svg')_center/cover_no-repeat];
@apply before:rounded-full after:[mask:var(--icon-radio)_center/cover_no-repeat];
}
input[type="checkbox"] {
@apply before:rounded after:[mask:url('@assets/icons/check.svg')_center/cover_no-repeat];
@apply before:rounded after:[mask:var(--icon-check)_center/cover_no-repeat];
}
.cbi-checkbox {
@@ -11,7 +11,7 @@
/* hover:no-underline: the global `a:hover { underline }` in _elements.css
outranks a bare class (a:hover is 0,1,1), so nav rows must opt out at
the same specificity tier. */
@apply text-text hover:bg-hover-faint block rounded-xl px-3 py-1.5 no-underline transition-all duration-150 hover:no-underline;
@apply text-text hover:bg-hover-faint block rounded-xl px-3 py-1.5 no-underline transition-colors duration-150 hover:no-underline;
}
.navigation-direct {
@@ -33,7 +33,7 @@
}
.navigation-group-toggle {
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-[250ms] after:content-[''] after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] hover:after:opacity-100;
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-[250ms] after:content-[''] after:[mask:var(--icon-arrow-right)_center/cover_no-repeat] hover:after:opacity-100;
}
.nav-category-label {
@@ -90,38 +90,38 @@
The only maintenance point: add new first-level sections here; anything
unmapped falls back to the default icon at the consumer. */
.desktop-nav-title[data-section="status"] {
@apply [--menu-icon:url('@assets/icons/activity.svg')];
@apply [--menu-icon:var(--icon-activity)];
}
.desktop-nav-title[data-section="statistics"] {
@apply [--menu-icon:url('@assets/icons/chart-bar.svg')];
@apply [--menu-icon:var(--icon-chart-bar)];
}
.desktop-nav-title[data-section="nlbw"] {
@apply [--menu-icon:url('@assets/icons/gauge.svg')];
@apply [--menu-icon:var(--icon-gauge)];
}
.desktop-nav-title[data-section="system"] {
@apply [--menu-icon:url('@assets/icons/settings.svg')];
@apply [--menu-icon:var(--icon-settings)];
}
.desktop-nav-title[data-section="services"] {
@apply [--menu-icon:url('@assets/icons/apps.svg')];
@apply [--menu-icon:var(--icon-apps)];
}
.desktop-nav-title[data-section="nas"] {
@apply [--menu-icon:url('@assets/icons/server.svg')];
@apply [--menu-icon:var(--icon-server)];
}
.desktop-nav-title[data-section="control"] {
@apply [--menu-icon:url('@assets/icons/adjustments.svg')];
@apply [--menu-icon:var(--icon-adjustments)];
}
.desktop-nav-title[data-section="network"] {
@apply [--menu-icon:url('@assets/icons/world.svg')];
@apply [--menu-icon:var(--icon-world)];
}
.desktop-nav-title[data-section="vpn"] {
@apply [--menu-icon:url('@assets/icons/shield.svg')];
@apply [--menu-icon:var(--icon-shield)];
}
.desktop-nav-title[data-section="docker"] {
@apply [--menu-icon:url('@assets/icons/box.svg')];
@apply [--menu-icon:var(--icon-box)];
}
/* "asterisk" covers VoIP/PBX apps (e.g. luci-app-asterisk forks) — no
official OpenWrt package registers a verified menu.d node under this
name, so this is a best-effort key. */
.desktop-nav-title[data-section="asterisk"] {
@apply [--menu-icon:url('@assets/icons/phone.svg')];
@apply [--menu-icon:var(--icon-phone)];
}
@@ -1,5 +1,7 @@
.mobile-menu-overlay {
@apply max-md:bg-mega-menu-bg max-md:invisible max-md:fixed max-md:inset-0 max-md:z-60 max-md:opacity-0 max-md:backdrop-blur-lg max-md:backdrop-saturate-150 md:hidden;
/* The mobile surface is opaque, so filtering pixels behind it has no visual
effect and only adds a full-screen paint/compositing cost. */
@apply max-md:bg-mega-menu-bg max-md:invisible max-md:fixed max-md:inset-0 max-md:z-60 max-md:opacity-0 md:hidden;
/* Transitions live on the OPEN state only: opening fades in, closing cuts
instantly. Link taps close into a full-page navigation, and any close
@@ -30,7 +30,7 @@
search icon carries the same stroke weight as the poll/changes ones
sitting next to it. */
&::before {
@apply size-5 bg-current content-[''] [mask:url('@assets/icons/search.svg')_center/contain_no-repeat];
@apply size-5 bg-current content-[''] [mask:var(--icon-search)_center/contain_no-repeat];
}
&:hover {
@@ -59,7 +59,7 @@
@apply max-md:border-hairline max-md:bg-surface max-md:border-b max-md:px-4 max-md:py-2.5;
&::before {
@apply text-text-subtle size-5 shrink-0 bg-current content-[''] [mask:url('@assets/icons/search.svg')_center/contain_no-repeat];
@apply text-text-subtle size-5 shrink-0 bg-current content-[''] [mask:var(--icon-search)_center/contain_no-repeat];
}
& .cmdk-input {
@@ -72,7 +72,7 @@
@apply text-text-subtle grid size-5 shrink-0 cursor-pointer place-items-center rounded-md border-0 bg-transparent p-0 shadow-none transition-colors duration-150;
&::before {
@apply size-4 bg-current content-[''] [mask:url('@assets/icons/x.svg')_center/contain_no-repeat];
@apply size-4 bg-current content-[''] [mask:var(--icon-x)_center/contain_no-repeat];
}
&:hover {
@@ -1,6 +1,6 @@
select {
@apply text-text border-hairline bg-control-bg focus:border-brand focus:ring-focus-ring min-w-52.5 max-w-full appearance-none md:max-w-100 rounded-2xl border px-3 py-1.5 pr-10 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-150 focus:ring-2 focus:outline-none;
@apply bg-[url('@assets/icons/arrow-down.svg')] bg-size-[16px] bg-position-[right_0.75rem_center] bg-no-repeat dark:bg-[url('@assets/icons/arrow-down-dark.svg')];
@apply bg-size-[16px] bg-position-[right_0.75rem_center] bg-no-repeat [background-image:var(--icon-arrow-down)] dark:[background-image:var(--icon-arrow-down-dark)];
&[disabled] {
@apply cursor-not-allowed opacity-(--app-opacity-disabled);
}
+37 -7
View File
@@ -4,18 +4,48 @@
* Licensed to the public under the Apache License 2.0.
*/
@import "tailwindcss";
/* Login only needs Tailwind's theme variables and generated utilities. The
full Preflight reset is intentionally omitted from this standalone entry. */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
@import "./_tokens.css";
@import "./_reduced-motion.css";
@import "./_base.css";
/* See main.css: keep dev-only tooling out of Tailwind's source detection. */
@source not "../../vite.config.ts";
@source not "../../scripts/*.client.js";
@custom-variant dark (&:where([data-darkmode=true], [data-darkmode=true] *));
/* Minimal reset for this template, replacing the much larger Preflight. */
@layer base {
*,
*::before,
*::after {
@apply box-border;
}
body,
hr,
p,
button,
input {
@apply m-0;
}
button,
input {
@apply [font:inherit];
}
a {
@apply no-underline;
}
[hidden] {
@apply hidden!;
}
}
@keyframes divider-in {
from {
opacity: 0;
@@ -40,7 +70,7 @@
/* Full image fades in on top once loaded (compositor-only opacity change) */
.login-bg::after {
@apply pointer-events-none absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 [will-change:opacity] content-[''] [transition:opacity_0.5s_ease] [background-image:var(--login-bg)];
@apply pointer-events-none absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 content-[''] [transition:opacity_0.5s_ease] [background-image:var(--login-bg)];
}
.login-bg.full-loaded::after {
@@ -102,7 +132,7 @@
/* Focus ring: opacity-only transition runs on compositor thread, not main thread —
border-color changes require a repaint (~400 ms INP); opacity changes do not. */
.input-wrap::after {
@apply border-brand pointer-events-none absolute -inset-px rounded-[calc(var(--radius-2xl)+1px)] border-2 opacity-0 [will-change:opacity] content-[''] [transition:opacity_150ms_ease];
@apply border-brand pointer-events-none absolute -inset-px rounded-[calc(var(--radius-2xl)+1px)] border-2 opacity-0 content-[''] [transition:opacity_150ms_ease];
}
.input-wrap:focus-within::after {
+5 -14
View File
@@ -3,24 +3,15 @@
* Licensed under the Apache License, Version 2.0.
*/
@import "tailwindcss";
/* Dev-only tooling holds prose and plain CSS whose bare words (static,
transition, relative, …) Tailwind's automatic source detection would
otherwise read as utility candidates and emit into this shipped stylesheet.
Nothing here renders markup, so none of it belongs in the scan. */
@source not "../../vite.config.ts";
@source not "../../scripts/*.client.js";
/* Every shipped utility is referenced through @apply in these CSS sources.
Disabling automatic repository scanning prevents class-like words in docs,
scripts and fixtures from leaking into the OpenWrt payload. */
@import "tailwindcss" source(none);
@custom-variant dark (&:where([data-darkmode=true], [data-darkmode=true] *));
@plugin "tailwind-scrollbar" {
nocompatible: true;
}
@import "tw-animate-css";
@import "./_tokens.css";
@import "./_icons.css";
@import "./_base.css";
@import "./_elements.css";
@@ -0,0 +1,101 @@
import assert from "node:assert/strict";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import test from "node:test";
const projectRoot = resolve(import.meta.dirname, "../..");
const output = resolve(projectRoot, "htdocs/luci-static");
const asset = (path) => resolve(output, path);
const bytes = (path) => statSync(asset(path)).size;
test("production assets stay within raw-transfer budgets", () => {
const main = bytes("aurora/main.css");
const login = bytes("aurora/login.css");
const menu = bytes("resources/menu-aurora.js");
const font = bytes("aurora/fonts/lato-v24-latin-regular.woff2");
const logo = bytes("aurora/images/logo.svg");
assert.ok(main <= 190_000, "main.css exceeds 190 KB");
assert.ok(login <= 12_000, "login.css exceeds 12 KB");
assert.ok(menu <= 20_000, "menu-aurora.js exceeds 20 KB");
assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB");
assert.ok(main + menu + font + logo <= 250_000, "admin assets exceed 250 KB");
assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB");
});
test("compressed LuCI JS preserves its loader directives", () => {
const js = readFileSync(asset("resources/menu-aurora.js"), "utf8");
assert.match(js, /["']require baseclass["'];["']require ui["'];/);
const module = new Function("baseclass", "ui", js)(
{ extend: (value) => value },
{},
);
assert.equal(typeof module.__init__, "function");
});
test("compiled CSS does not duplicate SVG payloads for every mask property", () => {
const css = readFileSync(asset("aurora/main.css"), "utf8");
const dataUrls = css.match(/data:image\/svg\+xml,[^"]+/g) ?? [];
const encodedBytes = dataUrls.reduce((sum, value) => sum + value.length, 0);
assert.equal(
dataUrls.length,
new Set(dataUrls).size,
"duplicate SVG data URL",
);
assert.ok(encodedBytes <= 17_000, `SVG data URLs occupy ${encodedBytes} B`);
});
test("pruned login.css keeps every consumed variable resolvable", () => {
const css = readFileSync(asset("aurora/login.css"), "utf8");
const declared = new Set(
[...css.matchAll(/[{;](--[\w-]+):/g)].map((m) => m[1]),
);
const registered = new Set(
[...css.matchAll(/@property\s+(--[\w-]+)/g)].map((m) => m[1]),
);
// header.ut injects these from UCI at render time — consumed here, never
// declared here.
const injected = new Set(["--login-bg", "--login-bg-lqip"]);
const unresolvable = [];
for (const [, name, delim] of css.matchAll(/var\(\s*(--[\w-]+)\s*([,)])/g)) {
const hasFallback = delim === ",";
if (
!declared.has(name) &&
!registered.has(name) &&
!injected.has(name) &&
!hasFallback
) {
unresolvable.push(name);
}
}
assert.deepEqual(unresolvable, []);
// The prune must actually strip admin-only tokens from the shared sheet.
for (const adminOnly of ["--mega-menu-bg", "--icon-", "--sidebar"]) {
assert.ok(!css.includes(adminOnly), `${adminOnly} should be pruned`);
}
// And the login page's own consumed tokens must survive, light and dark.
for (const kept of ["--surface:", "--brand:", "--control-bg:"]) {
const count = css.split(kept).length - 1;
assert.ok(count >= 2, `${kept} should be declared for both modes`);
}
});
test("package roots contain no macOS metadata", () => {
const offenders = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.name === ".DS_Store") offenders.push(path);
else if (entry.isDirectory()) visit(path);
}
};
visit(resolve(projectRoot, "htdocs"));
visit(resolve(projectRoot, "ucode"));
assert.deepEqual(offenders, []);
});
@@ -108,7 +108,7 @@ test("shared navigation styles own accordion animation without a guide rail", ()
"after:transition-[transform,opacity]",
"after:duration-[250ms]",
]);
assert.match(toggle, /arrow-right\.svg/);
assert.match(toggle, /var\(--icon-arrow-right\)/);
assertIncludesUtilities(region, [
"grid",
"grid-rows-[0fr]",
@@ -218,7 +218,11 @@ test("mega-menu category masks use Tailwind arbitrary utilities", () => {
assert.match(
icon,
/@apply[^;]*\[mask:var\(--menu-icon,url\(["']@assets\/icons\/category\.svg["']\)\)_center\/contain_no-repeat\]/,
/@apply[^;]*\[mask:var\(--menu-icon,var\(--icon-category\)\)_center\/contain_no-repeat\]/,
);
// The default lives in the var() fallback only. Declaring --menu-icon on
// the title compiles into a (0,5,1) selector chain that outranks every
// .desktop-nav-title[data-section=…] (0,2,0) mapping in _nav.css.
assert.doesNotMatch(title, /--menu-icon:/);
assert.doesNotMatch(layoutStyles, /^\s*mask\s*:/m);
});
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
const projectRoot = resolve(import.meta.dirname, "../..");
const source = (path) => readFileSync(resolve(projectRoot, path), "utf8");
test("navigation transitions avoid blanket and full-page layout animation", () => {
const nav = source(".dev/src/media/components/_nav.css");
const layout = source(".dev/src/media/_layout.css");
assert.doesNotMatch(nav, /\btransition-all\b/);
assert.doesNotMatch(layout, /transition-\[grid-template-columns\]/);
});
test("login effects do not reserve compositor layers permanently", () => {
const login = source(".dev/src/media/login.css");
assert.doesNotMatch(login, /\[will-change:opacity\]/);
});
test("login template reuses server data when including the header", () => {
const sysauth = source("ucode/template/themes/aurora/sysauth.ut");
const header = source("ucode/template/themes/aurora/header.ut");
assert.match(
sysauth,
/include\('header',\s*\{[^}]*prefetched_boardinfo:\s*boardinfo[^}]*prefetched_tokens:\s*themeTokens[^}]*\}\)/s,
);
assert.match(header, /prefetched_boardinfo\s*\?\?/);
assert.match(header, /prefetched_tokens\s*\?\?/);
});
@@ -114,7 +114,10 @@ test("mega-menu reveal is compositor-only and the frost never overlaps it", () =
// The curtain carries the blur permanently (Apple's globalnav-curtain) and
// fades it with opacity/visibility. Close must start that fade immediately;
// delaying the curtain left blur visible after the sheet had collapsed.
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
assert.ok(
!overlay.includes("max-md:backdrop-blur-lg"),
"opaque mobile overlay must not blur hidden pixels",
);
const curtain = overlay
.split("\n")
.find((l) => l.includes("bg-mega-menu-scrim"));
+101 -7
View File
@@ -7,9 +7,9 @@ import tailwindcss from "@tailwindcss/vite";
import browserslist from "browserslist";
import { exec } from "child_process";
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
import type { IncomingMessage, ServerResponse } from "http";
import { browserslistToTargets } from "lightningcss";
import { browserslistToTargets, transform as lightningcssTransform } from "lightningcss";
import { basename, dirname, join, relative, resolve, sep } from "path";
import { minify as terserMinify } from "terser";
import { promisify } from "util";
@@ -27,6 +27,10 @@ const CURRENT_DIR = process.cwd();
const PROJECT_ROOT = resolve(CURRENT_DIR, "..");
const BUILD_OUTPUT = resolve(PROJECT_ROOT, "htdocs/luci-static");
const LIGHTNINGCSS_TARGETS = browserslistToTargets(
browserslist("last 4 versions, Firefox ESR, not dead"),
);
function createLuciJsCompressPlugin(): Plugin {
let outDir: string;
@@ -48,8 +52,10 @@ function createLuciJsCompressPlugin(): Plugin {
const sourceCode = await readFile(join(srcDir, relPath), "utf-8");
const compressed = await terserMinify(sourceCode, {
parse: { bare_returns: true },
compress: false,
mangle: false,
/* LuCI dependency declarations are string directives. Keep
them while enabling normal compression and local mangling. */
compress: { directives: false, passes: 2 },
mangle: true,
format: { comments: false, beautify: false },
});
const outputPath = join(outDir, "resources", normalized);
@@ -66,6 +72,94 @@ function createLuciJsCompressPlugin(): Plugin {
};
}
async function removeMacOsMetadata(dir: string): Promise<void> {
if (!existsSync(dir)) return;
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) await removeMacOsMetadata(path);
else if (entry.name === ".DS_Store") await rm(path, { force: true });
}
}
/* login.css imports the full shared token sheet but the login page consumes
only a fraction of it. Prune every custom-property declaration (and --tw-*
@property registration) that no var() reference can reach, following
declaration-value chains (--a: var(--b) keeps --b alive). --login-bg and
--login-bg-lqip stay consumed-without-declaration by design: header.ut
injects them from UCI at render time, so pruning never touches consumers. */
function createLoginCssPrunePlugin(): Plugin {
return {
name: "login-css-prune",
apply: "build",
enforce: "post",
async closeBundle() {
const path = resolve(BUILD_OUTPUT, "aurora/login.css");
if (!existsSync(path)) return;
const css = await readFile(path, "utf-8");
// Roots are vars consumed by normal declarations; custom-property
// declarations only contribute edges to the reachability walk.
const VALUE = `(?:"[^"]*"|'[^']*'|[^;{}"'])*`;
const roots = new Set<string>();
const edges = new Map<string, Set<string>>();
for (const [, name, value] of css.matchAll(
new RegExp(`(?<=[{;])(--[\\w-]+|[a-zA-Z-]+):(${VALUE})`, "g"),
)) {
const refs = [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1]);
if (!name.startsWith("--")) refs.forEach((r) => roots.add(r));
else {
const deps = edges.get(name) ?? new Set<string>();
refs.forEach((r) => deps.add(r));
edges.set(name, deps);
}
}
const keep = new Set(roots);
const stack = [...roots];
while (stack.length) {
for (const dep of edges.get(stack.pop()!) ?? []) {
if (!keep.has(dep)) {
keep.add(dep);
stack.push(dep);
}
}
}
const pruned = css
.replace(
new RegExp(`(?<=[{;])(--[\\w-]+):${VALUE};?`, "g"),
(decl, name) => (keep.has(name) ? decl : ""),
)
.replace(/@property\s+(--[\w-]+)\{[^{}]*\}/g, (rule, name) =>
keep.has(name) ? rule : "",
);
// Re-minify with the shared targets: validates the edited syntax and
// drops any rule the pruning emptied out.
const { code } = lightningcssTransform({
filename: "login.css",
code: Buffer.from(pruned),
minify: true,
targets: LIGHTNINGCSS_TARGETS,
});
await writeFile(path, code);
},
};
}
function createPackageHygienePlugin(): Plugin {
return {
name: "package-hygiene",
apply: "build",
enforce: "post",
async closeBundle() {
await Promise.all([
removeMacOsMetadata(resolve(PROJECT_ROOT, "htdocs")),
removeMacOsMetadata(resolve(PROJECT_ROOT, "ucode")),
]);
},
};
}
// On-demand third-party patches: serve src/media/patches/<page>.css at
// /luci-static/aurora/patches/<page>.css in dev. Without this, header.ut's patch
// <link> falls through to the OpenWrt proxy (404 / stale router asset) and patch
@@ -789,12 +883,12 @@ export default defineConfig(({ mode }) => {
createMockPlugin(),
createUtSyncPlugin(OPENWRT_SSH_HOST),
createLuciJsCompressPlugin(),
createLoginCssPrunePlugin(),
createPackageHygienePlugin(),
],
css: {
lightningcss: {
targets: browserslistToTargets(
browserslist("last 4 versions, Firefox ESR, not dead"),
),
targets: LIGHTNINGCSS_TARGETS,
},
},
build: {
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.1.2
PKG_RELEASE:=55
PKG_VERSION:=1.1.3
PKG_RELEASE:=56
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><image width="512" height="512" href="data:image/png;base64,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 truncated
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><image width="512" height="512" href="data:image/png;base64,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 truncated

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base) * .25);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--radius-2xl:calc(var(--radius-base) * 2);--radius-3xl:calc(var(--radius-base) * 3);--radius-4xl:calc(var(--radius-base) * 4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--container-max-width:var(--container-max-width)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacinLine truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans)}html body{background-color:var(--bg);min-height:100dvh}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.login-bg{pointer-events:none;background-image:var(--login-bg-lqip,var(--login-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.login-bg:after{pointer-events:none;background-image:var(--login-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.login-bg.full-loaded:after{opacity:1}.login-card{gap:calc(var(--spacing) * 7);border-radius:calc(var(--radius-base) * 4);background-color:var(--surface);padding:calc(var(--spacing) * 14);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}@media not all and (width>=48rem){.login-card{border-radius:calc(var(--radius-base) * 3);padding:calc(var(--spacing) * 9)}}.login-brand{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.login-divider{border-style:var(--tw-border-style);--tw-gradient-position:to right;borderLine truncated
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-scrollbar-thumb:#0000;--tw-scrollbar-track:#0000;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .25);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--radius-2xl:calc(var(--radius-base) * 2);--radius-3xl:calc(var(--radius-base) * 3);--radius-4xl:calc(var(--radius-base) * 4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--container-max-width:var(--container-max-width)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mLine truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-butLine truncated
@@ -1 +1 @@
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const mobileList=overlay.querySelector("#mobile-nav-list");const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const updateToggleState=expanded=>{const desktopSidebar=isDesktopSidebar();navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded?"true":"false");navigationToggle.setAttribute("aria-controls",desktopSidebar?"sidebar-panel":"mobile-menu-overlay");const label=expanded&&!desktopSidebar?_("Close"):_("Navigation");navigationToggle.setAttribute("title",label);navigationToggle.setAttribute("aria-label",label)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.classList.remove("mobile-navigation-open");document.body.style.overflow="";this.resetNavigationGroups(mobileList)};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.classList.toggle("mobile-navigation-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)this.expandActiveNavigationGroup(mobileList);else this.resetNavigationGroups(mobileList);updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const destination=e.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");if(destination&&overlay.contains(destination)){setNavigationExpanded(false)}})},renderMobileMenu(items){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");if(list)list.innerHTML="";if(footerAction)footerAction.innerHTML="";if(!list)return;items.forEach(item=>{if(item.isLogout){if(footerAction){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:item.href},[item.title]))}return}list.appendChild(this.renderNavigationItem(item,"mobile"))});this.bindNavigationAccordion(list)},render(tree){this.renderModeMenu(tree);if(L.env.dispatchpath.length>=3){let node=tree;let url="";for(let i=0;i<3&&node;i++){const segment=L.env.dispatchpath[i];node=node.children?.[segment];url+=(url?"/":"")+segment}if(node)this.renderTabMenu(node,url)}},renderTabMenu(tree,url,level=0){const container=document.querySelector("#tabmenu");const ul=E("ul",{class:"tabs"});const children=ui.menu.getChildren(tree);let activeNode=null;children.forEach(child=>{const isActive=L.env.dispatchpath[3+level]===child.name;ul.appendChild(E("li",{class:`tabmenu-item-${child.name}${isActive?" active":""}`},[E("a",{href:L.url(url,child.name)},[_(child.title)])]));if(isActive)activeNode=child});if(!ul.children.length)return E([]);container.appendChild(ul);container.style.display="";if(activeNode){this.renderTabMenu(activeNode,`${url}/${activeNode.name}`,level+1)}return ul},renderMainMenu(tree,url,level=0,navigationItems=null){const ul=level?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu");const children=ui.menu.getChildren(tree);if(level>1)return E([]);if(level===0){const navType=document.body?.dataset?.navType||"mega-menu";if(navType==="sidebar"){this.renderSidebLine truncated
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(e=>this.render(e)),this.initNavigationControls(),this.initUciIndicator()},initUciIndicator(){const e=ui.changes?.setIndicator;e&&(ui.changes.setIndicator=function(t){e.call(this,t),document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",t||0)})},initNavigationControls(){const e=document.querySelector("#mobile-menu-overlay"),t=document.querySelector("#navigation-toggle");if(!t||!e)return;const i=e.querySelector("#mobile-nav-list"),n=window.matchMedia("(min-width: 768px)"),a="aurora.sidebarCollapsed",s=()=>n.matches&&"sidebar"===document.body.dataset.navType,o=e=>{const i=s();t.classList.toggle("is-expanded",e&&!n.matches),t.setAttribute("aria-expanded",e?"true":"false"),t.setAttribute("aria-controls",i?"sidebar-panel":"mobile-menu-overlay");const a=e&&!i?_("Close"):_("Navigation");t.setAttribute("title",a),t.setAttribute("aria-label",a)},l=()=>{e.classList.remove("mobile-menu-open"),document.body.classList.remove("mobile-navigation-open"),document.body.style.overflow="",this.resetNavigationGroups(i)},r=t=>{if(s()){l();const e=!t;return document.body.classList.toggle("sidebar-collapsed",e),localStorage.setItem(a,e),void o(t)}if(n.matches)return l(),void o(!1);e.classList.toggle("mobile-menu-open",t),document.body.classList.toggle("mobile-navigation-open",t),document.body.style.overflow=t?"hidden":"",t?this.expandActiveNavigationGroup(i):this.resetNavigationGroups(i),o(t)},c=()=>{if(l(),s()){const e="true"===localStorage.getItem(a);return document.body.classList.toggle("sidebar-collapsed",e),void o(!e)}o(!1)};t.addEventListener("click",t=>{t.stopPropagation(),r(!(s()?!document.body.classList.contains("sidebar-collapsed"):!n.matches&&e.classList.contains("mobile-menu-open")))}),e.addEventListener("click",t=>{t.target===e&&r(!1)}),document.addEventListener("keydown",t=>{"Escape"===t.key&&e.classList.contains("mobile-menu-open")&&r(!1)}),n.addEventListener("change",c),c(),document.addEventListener("click",t=>{const i=t.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");i&&e.contains(i)&&r(!1)})},renderMobileMenu(e){const t=document.querySelector("#mobile-nav-list"),i=document.querySelector("#mobile-nav-footer-action");t&&(t.innerHTML=""),i&&(i.innerHTML=""),t&&(e.forEach(e=>{e.isLogout?i&&i.appendChild(E("a",{class:"mobile-nav-logout",href:e.href},[e.title])):t.appendChild(this.renderNavigationItem(e,"mobile"))}),this.bindNavigationAccordion(t))},render(e){if(this.renderModeMenu(e),L.env.dispatchpath.length>=3){let t=e,i="";for(let e=0;e<3&&t;e++){const n=L.env.dispatchpath[e];t=t.children?.[n],i+=(i?"/":"")+n}t&&this.renderTabMenu(t,i)}},renderTabMenu(e,t,i=0){const n=document.querySelector("#tabmenu"),a=E("ul",{class:"tabs"}),s=ui.menu.getChildren(e);let o=null;return s.forEach(e=>{const n=L.env.dispatchpath[3+i]===e.name;a.appendChild(E("li",{class:`tabmenu-item-${e.name}${n?" active":""}`},[E("a",{href:L.url(t,e.name)},[_(e.title)])])),n&&(o=e)}),a.children.length?(n.appendChild(a),n.style.display="",o&&this.renderTabMenu(o,`${t}/${o.name}`,i+1),a):E([])},renderMainMenu(e,t,i=0,n=null){const a=i?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu"),s=ui.menu.getChildren(e);if(i>1)return E([]);if(0===i){const e=document.body?.dataset?.navType||"mega-menu";if("sidebar"===e)return this.renderSidebar(n||[]),a||E([]);if(!a||!s.length)return E([]);"mega-menu"===e?this.initMegaMenu(s,t,a):this.initDropdown(s,t,a)}else{if(!s.length)return E([]);s.forEach(i=>{const n=this.isActivePath(e.name,i.name),s={class:n?"is-active-page":"",href:L.url(t,i.name)};n&&(s["aria-current"]="page"),a.appendChild(E("li",{},[E("a",s,[_(i.title)])]))})}return a.style.display="",a},isActivePath:(e,t)=>L.env.dispatchpath[1]===e&&(null==t||L.env.dispatchpath[2]===t),buildNavigationModel(e,t){return e.filter(e=>e?.name).map(e=>{const i=ui.menu.getChildren(e).filter(e=>e?.name).map(i=>({name:i.name,title:_(i.title),href:L.url(t,e.name,i.name),isActivePage:this.isActivePath(e.name,i.name)})),n=i.length>0,a=this.isActivePath(e.name);return{name:e.name,title:_(e.title),href:L.url(t,e.name),hasChildren:n,isLogout:"logout"===e.name,isActiveGroup:n&&a,isActivePage:!n&&a,activePage:i.find(e=>e.isActivePage)||null,pages:i}})},navigationSubmenuId:(e,t)=>`${e}-submenu-${encodeURIComponent(String(t))}`,renderNavigationItem(e,t){const i="mobile"===t,n=i?"mobile-nav-item":"",a=i?"navigation-direct mobile-nav-link":"navigation-direct nav-link";if(!e.hasChildren){const t={class:`${a}${e.isActivePage?" is-active-page":""}`,href:e.href};return e.isActivePage&&(t["aria-current"]="page"),E("li",{class:n},[E("a",t,[e.title])])}const s=this.navigationSubmenuId(t,e.name),o=["navigation-group",i?"mobile-nav-item":"sidebar-group",e.isActiveGroup?"is-active-group":"",e.isActiveGroup?"is-expanded":""].filter(Boolean),l={class:i?"navigation-group-toggle mobile-nav-link mobile-nav-toggle":"navigatLine truncated
@@ -14,10 +14,10 @@
import { cursor } from 'uci';
import { lsdir, readfile } from 'fs';
const boardinfo = ubus.call('system', 'board') ?? {};
const boardinfo = prefetched_boardinfo ?? (ubus.call('system', 'board') ?? {});
const hostname = striptags(boardinfo.hostname ?? '?');
const uci = cursor();
const tokens = uci.get_all('aurora', 'theme') || {};
const tokens = prefetched_tokens ?? (uci.get_all('aurora', 'theme') || {});
const page_segs = length(ctx?.request_path) ? ctx.request_path : (ctx?.path || []);
const page = join('-', page_segs);
@@ -14,7 +14,11 @@ const themeTokens = cursor().get_all('aurora', 'theme') || {};
const loginLogoSvg = themeTokens.logo_svg || 'logo.svg';
const loginIconVer = themeTokens.icon_cache_version || '0';
-%}
{% include('header', { blank_page: true }) %}
{% include('header', {
blank_page: true,
prefetched_boardinfo: boardinfo,
prefetched_tokens: themeTokens
}) %}
{% if (themeTokens.struct_login_bg): %}
<div class="login-bg" aria-hidden="true"></div>