🍕 Sync 2026-09-14 23:30:37

This commit is contained in:
github-actions[bot] committed 2026-09-14 23:30:37 +08:00
1 parent 0603b094cf
commit 81fe4cd866
214 files changed
+10806 -2630

No files matched your search

@@ -17,7 +17,10 @@ workflow. No edit to a shipped or source path happens before gate 1, and the
1. **Research (read-only).** Load `luci-knowledge`; read
`references/aurora-contract.md` and the section relevant to the files
involved; `git log -8 --oneline -- <files>`; the tests and budgets that
cover them.
cover them. If the request names a page, a package, a DOM element or
shows a page screenshot, load `luci-dom-compat` and complete its steps
1–2 (locate the page, read `.dev/compat/pages/` for the files and
selectors about to change) before planning.
2. **Plan.** Call `EnterPlanMode`. The plan states: files to change; the
approach in ≤ 10 lines; budget impact (bytes per `tests/build-performance.test.js`,
per-request ubus/uci/fs calls, dispatches per navigation); floor it
@@ -46,7 +49,10 @@ workflow. No edit to a shipped or source path happens before gate 1, and the
out and why. Write the same report as a decision record to
`.dev/decisions/<YYYY-MM-DD>-<slug>.md` (git-ignored; header lines
`Request`, `Decision: implemented | reverted | deferred`, `Commit:` once
one exists) so the ledger outlives the session. `rm -f
one exists) so the ledger outlives the session. A page-scoped change also
appends its entry to `.dev/compat/pages/<data-page prefix>.md` (and a
fixture under `.dev/compat/fixtures/` when the symptom is geometric —
`luci-dom-compat` step 5) in the same commit. `rm -f
.claude/.change-approved`. Commit only when the user asks: one logical
change, short conventional subject, body carries the reasoning, no
session trailers. Never push.
@@ -0,0 +1,78 @@
---
name: luci-dom-compat
description: Use whenever a request is about how one LuCI page renders under this theme — a cell that wraps or overflows, misaligned controls, a mobile card layout, a screenshot of a page, a data-page value, a package name (luci-app-*), or an issue about a plugin. Stock LuCI pages count as much as third-party apps. Reads the DOM compat ledger before any CSS is touched and writes the entry (and a geometry fixture) after.
argument-hint: <page, package, symptom or issue>
---
# luci-dom-compat $ARGUMENTS
The ledger lives in `.dev/compat/` (format in its README). It answers "why
is this rule here" for every page-scoped rule in the theme, and
`tests/compat.test.js` runs the fixtures that guard the geometric ones.
This skill does not replace `/luci-change`: the edit itself still goes
through that gate; this is what happens before and after.
## 1. Locate — write the quadruple down before touching CSS
| what | where to get it |
|---|---|
| `data-page` | `body[data-page]` on the page; or the URL segments after `/cgi-bin/luci/` joined with `-` |
| package | on the device: `apk list -I \| grep -i <name>` or `opkg list-installed \| grep -i <name>`; stock pages are `luci-mod-*`/`luci-app-*` from openwrt/luci |
| menu path | `/usr/share/luci/menu.d/*.json` (`grep -o '"admin/[^"]*"'` + the `title` fields) or the breadcrumb on the page |
| DOM element | the selector, plus any inline `style`/attribute the layout depends on (`table-layout:fixed`, `white-space:nowrap`, `width:`) — that is usually the cause |
## 2. Read the ledger first
```
ls .dev/compat/pages/ | grep <prefix> # this page
grep -rln "<file you will edit>" .dev/compat/pages/
grep -rln "<selector you will touch>" .dev/compat/pages/
```
For every hit, state in the plan whether the change keeps, changes or
removes that entry's behaviour. A rule that looks pointless usually has an
entry; if it has none, that is the first thing to add.
## 3. Evidence
Device reachable (`VITE_OPENWRT_HOST` in `.dev/.env`, empty root password
over http on the dev routers): `node .claude/skills/luci-dom-compat/scripts/compat-probe.mjs`.
It logs in, opens each page in headless Chrome, serves the *before* CSS
through `Fetch` interception, measures, hot-swaps the *after* CSS into the
same document, measures again, and prints per-element computed-style
diffs on `.table` subtrees, wrap/overflow counts for `SELECTOR`, and
screenshots. Nothing on the device is written. Env: `HOST`, `PAGES`
(comma-separated `admin/...` paths), `WIDTHS`, `BEFORE`/`AFTER` (css files;
default before = the file the device serves, after = `htdocs/.../main.css`),
`SELECTOR`, `OUT`, `PRESET_LS` (a `localStorage` key=value the page needs,
e.g. the selected disk).
Device unreachable: a `.dev/mocks/` snapshot through `pnpm dev`
(`/mocks/`), or an existing fixture. Compare against luci-theme-bootstrap
(`../luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css`) when
the question is "what does upstream do here" — serve it as `AFTER` to the
same page.
Wrap detection that survives inline swatches and checkboxes: walk the
cell's text nodes, collect client-rect tops, and treat "text starts below
the inline indicator" as a wrap (`nowrap` in `tests/compat.test.js`).
## 4. Fix
Through `/luci-change`. Prefer the component partial over a `patches/`
file when the cause is a theme rule blocking what the page asked for
(#65: a font size on `td` blocked the table's own); a patch is for markup
the theme cannot reasonably absorb.
## 5. Sink — same commit as the fix
1. Append the entry to `.dev/compat/pages/<data-page prefix>.md` in the
README format. `verified:` carries the numbers from step 3.
2. Symptom geometric (wrap, overflow, width, height)? Trim the DOM to a
fixture in `.dev/compat/fixtures/<data-page>--<slug>.html`: the block,
the page's own injected `<style>`, a `max-width` wrapper matching the
measured content width if the wrap depends on it, and the `#compat`
checks. The theme font is injected by the runner. Confirm the fixture
**fails against the pre-fix CSS** (rebuild from the previous commit, or
swap the utility back) before committing it.
3. `mirror: pending` until the same change lands in luci-theme-shadcn.
@@ -0,0 +1,182 @@
#!/usr/bin/env node
/**
* compat-probe.mjs — before/after a theme CSS change on real device pages,
* in one page load per page: log in, open the page in headless Chrome,
* serve BEFORE as the theme's main.css through Fetch interception, measure,
* hot-swap AFTER into the same document, measure again. Writes nothing on
* the device. Raw CDP, no npm deps, node >= 22.
*
* env:
* HOST http://192.168.8.1 (default: VITE_OPENWRT_HOST from .dev/.env)
* PAGES comma-separated paths after /cgi-bin/luci/ (default: a stock set)
* WIDTHS viewport widths, default 1280,390
* BEFORE css file served as main.css first (default: fetch the device's own)
* AFTER css file swapped in (default: htdocs/luci-static/aurora/main.css)
* SELECTOR elements whose wrap/overflow are counted (default: .table td, .table .td)
* PRESET_LS key=value stored in localStorage before load (e.g. the disk a plugin remembers)
* OUT output directory for screenshots + report (default: ./compat-probe-out)
* USER/PASS login (default root / empty)
* CHROME_BIN
*/
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
const root = resolve(import.meta.dirname, "../../../..");
const envFile = join(root, ".dev/.env");
const dotenv = existsSync(envFile)
? Object.fromEntries(readFileSync(envFile, "utf8").split("\n").filter((l) => /^[A-Z_]+=/.test(l)).map((l) => l.split("=", 2)))
: {};
const HOST = (process.env.HOST ?? (dotenv.VITE_OPENWRT_HOST ? `http://${dotenv.VITE_OPENWRT_HOST}` : "http://192.168.1.1")).replace(/\/+$/, "");
const PAGES = (process.env.PAGES ?? "admin/status/overview,admin/network/network,admin/network/firewall,admin/network/dhcp,admin/system/mounts").split(",");
const WIDTHS = (process.env.WIDTHS ?? "1280,390").split(",").map(Number);
const AFTER = readFileSync(process.env.AFTER ?? join(root, "htdocs/luci-static/aurora/main.css"));
const BEFORE = process.env.BEFORE ? readFileSync(process.env.BEFORE) : null;
const SELECTOR = process.env.SELECTOR ?? ".table td, .table .td";
const OUT = resolve(process.env.OUT ?? "compat-probe-out");
const CHROME = process.env.CHROME_BIN ?? (process.platform === "darwin" ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" : "google-chrome");
mkdirSync(OUT, { recursive: true });
const login = await fetch(`${HOST}/cgi-bin/luci/`, {
method: "POST",
body: new URLSearchParams({ luci_username: process.env.USER_NAME ?? "root", luci_password: process.env.PASS ?? "" }),
redirect: "manual",
});
const cookie = (login.headers.get("set-cookie") ?? "").match(/(sysauth[^=]*)=([^;]+)/);
if (!cookie) throw new Error(`login failed (${login.status})`);
let before = BEFORE;
if (!before) {
const r = await fetch(`${HOST}/luci-static/aurora/main.css`, { headers: { cookie: `${cookie[1]}=${cookie[2]}` } });
if (!r.ok) throw new Error(`device main.css: ${r.status}`);
before = Buffer.from(await r.arrayBuffer());
}
const profile = mkdtempSync(join(tmpdir(), "cdp-compat-probe-"));
const chrome = spawn(CHROME, ["--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "about:blank"], { stdio: "ignore" });
let port = null;
for (let i = 0; i < 100 && !port; i++) {
await sleep(100);
const f = join(profile, "DevToolsActivePort");
if (existsSync(f)) port = +readFileSync(f, "utf8").split("\n")[0];
}
if (!port) throw new Error("chrome: no DevToolsActivePort");
const ws = new WebSocket((await (await fetch(`http://127.0.0.1:${port}/json/version`)).json()).webSocketDebuggerUrl);
await new Promise((res, rej) => { ws.addEventListener("open", res, { once: true }); ws.addEventListener("error", () => rej(new Error("CDP connect failed")), { once: true }); });
let mid = 0; const pending = new Map(); const handlers = new Set();
ws.addEventListener("message", (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.error ? rej(new Error(m.error.message)) : res(m.result); }
else for (const h of handlers) h(m);
});
const send = (method, params = {}, sessionId) => new Promise((res, rej) => { const id = ++mid; pending.set(id, { res, rej }); ws.send(JSON.stringify({ id, method, params, sessionId })); });
// Computed font-size/line-height per element under every .table, plus the
// wrap/overflow state of SELECTOR — the two things a table change moves.
const MEASURE = `(() => {
const pick = (el, pseudo) => { const s = getComputedStyle(el, pseudo); return s.fontSize + '/' + s.lineHeight + ' ' + s.whiteSpace; };
const path = (el) => { const p = []; for (let e = el; e && e !== document.body; e = e.parentElement) p.unshift(e.tagName.toLowerCase() + (e.id ? '#' + e.id : '') + (e.classList[0] ? '.' + e.classList[0] : '')); return p.slice(-4).join('>'); };
const styles = [];
[...document.querySelectorAll('table.table, .table')].forEach((t, ti) => {
styles.push(['T' + ti, path(t), pick(t)]);
t.querySelectorAll('*').forEach((el, i) => {
if (el.closest('.table') !== t) return;
const txt = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim());
styles.push(['T' + ti + '#' + i + (txt ? ' txt' : ''), path(el), pick(el)]);
});
});
const wrapped = [], overflow = [];
for (const el of document.querySelectorAll(${JSON.stringify(SELECTOR)})) {
const tops = []; const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let n; (n = w.nextNode());) { if (!n.textContent.trim()) continue; const r = document.createRange(); r.selectNodeContents(n); for (const x of r.getClientRects()) if (x.width > 0) tops.push(x.top); }
const ind = el.querySelector('input, img, svg, [class*=indicator]');
if (tops.some(t => Math.abs(t - tops[0]) > 3) || (ind && tops.length && tops[0] >= ind.getBoundingClientRect().bottom - 1)) wrapped.push(el.textContent.trim().slice(0, 30));
if (el.scrollWidth > el.clientWidth + 1) overflow.push(el.textContent.trim().slice(0, 30));
}
return { page: document.body.dataset.page, styles, wrapped, overflow, pageOverflow: document.documentElement.scrollWidth > innerWidth };
})()`;
const report = [];
try {
for (const width of WIDTHS) {
const { targetId } = await send("Target.createTarget", { url: "about:blank" });
const { sessionId } = await send("Target.attachToTarget", { targetId, flatten: true });
await send("Page.enable", {}, sessionId);
await send("Network.enable", {}, sessionId);
await send("Network.setCacheDisabled", { cacheDisabled: true }, sessionId);
await send("Network.setCookie", { name: cookie[1], value: cookie[2], domain: new URL(HOST).hostname, path: "/" }, sessionId);
await send("Emulation.setDeviceMetricsOverride", { width, height: 900, deviceScaleFactor: 1, mobile: width < 768 }, sessionId);
if (process.env.PRESET_LS) {
const [k, v] = process.env.PRESET_LS.split("=", 2);
await send("Page.addScriptToEvaluateOnNewDocument", { source: `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})` }, sessionId);
}
await send("Fetch.enable", { patterns: [{ urlPattern: "*/luci-static/aurora/main.css*", requestStage: "Request" }] }, sessionId);
handlers.add((m) => {
if (m.sessionId !== sessionId || m.method !== "Fetch.requestPaused") return;
const body = m.params.request.url.includes("variant=after") ? AFTER : before;
send("Fetch.fulfillRequest", { requestId: m.params.requestId, responseCode: 200, responseHeaders: [{ name: "Content-Type", value: "text/css" }, { name: "Cache-Control", value: "no-store" }], body: body.toString("base64") }, sessionId);
});
const evaljs = async (expression) => {
const r = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, sessionId);
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text);
return r.result.value;
};
const shot = async (name) => {
const s = await send("Page.captureScreenshot", { format: "png" }, sessionId);
writeFileSync(join(OUT, name), Buffer.from(s.data, "base64"));
};
for (const page of PAGES) {
await send("Page.navigate", { url: `${HOST}/cgi-bin/luci/${page}` }, sessionId);
for (let i = 0; i < 80; i++) {
await sleep(250);
const ready = await evaljs(`document.readyState === 'complete' && !document.querySelector('#view > .spinning') && !!document.querySelector('#view > :not(script)')`).catch(() => false);
if (ready) break;
}
await sleep(2000);
if (await evaljs(`!!document.querySelector('input[name="luci_username"]')`)) throw new Error(`landed on the login form for ${page}`);
// Device on another theme: swap its shell stylesheets for aurora's so the
// content region can still be measured. Header/nav markup stays foreign,
// so only in-view numbers mean anything then.
const foreign = await evaljs(`!document.querySelector('link[rel=stylesheet][href*="/luci-static/aurora/main.css"]')`);
if (foreign) {
console.log(` (device is not on aurora for ${page}: aurora main.css injected over the active theme; compare the content region only)`);
await evaljs(`new Promise(r => {
document.querySelectorAll('link[rel=stylesheet][href*="/luci-static/"]').forEach(l => { if (!/\\/resources\\//.test(l.href)) l.remove(); });
const l = Object.assign(document.createElement('link'), { rel: 'stylesheet', href: '/luci-static/aurora/main.css?probe=before', onload: r });
document.head.append(l);
})`);
await sleep(400);
}
const b = await evaljs(MEASURE);
const slug = page.replaceAll("/", "-");
await shot(`${slug}-${width}-before.png`);
await evaljs(`new Promise(r => { const l = [...document.querySelectorAll('link[rel=stylesheet]')].find(l => l.href.includes('/aurora/main.css')); const n = l.cloneNode(); n.href = '/luci-static/aurora/main.css?variant=after'; n.onload = () => { l.remove(); r(); }; l.after(n); })`);
await sleep(400);
const a = await evaljs(MEASURE);
await shot(`${slug}-${width}-after.png`);
const diffs = [];
const n = Math.min(b.styles.length, a.styles.length);
for (let i = 0; i < n; i++) {
if (b.styles[i][0] !== a.styles[i][0]) { diffs.push(`structure changed at ${i}`); break; }
if (b.styles[i][2] !== a.styles[i][2]) diffs.push(`${b.styles[i][0]} ${b.styles[i][1]}: ${b.styles[i][2]} -> ${a.styles[i][2]}`);
}
const textDiffs = diffs.filter((d) => / txt /.test(d) || d.startsWith("structure"));
report.push({ width, page, dataPage: b.page, elements: b.styles.length, diffs: diffs.length, textDiffs: textDiffs.length, wrapped: [b.wrapped.length, a.wrapped.length], overflow: [b.overflow.length, a.overflow.length], pageOverflow: [b.pageOverflow, a.pageOverflow] });
console.log(`[${width}] ${page} (${b.page}) els=${b.styles.length} styleDiffs=${diffs.length} textDiffs=${textDiffs.length} wrapped ${b.wrapped.length}->${a.wrapped.length} overflow ${b.overflow.length}->${a.overflow.length} pageOverflow ${b.pageOverflow}->${a.pageOverflow}`);
for (const d of textDiffs.slice(0, 6)) console.log(" ", d);
const uniq = [...new Set(diffs.filter((d) => !textDiffs.includes(d)).map((d) => d.replace(/^T\d+#\d+ /, "")))];
for (const d of uniq.slice(0, 4)) console.log(" (no text)", d);
if (a.wrapped.length) console.log(" still wrapped:", a.wrapped.slice(0, 8).join(" | "));
}
await send("Target.closeTarget", { targetId });
}
writeFileSync(join(OUT, "report.json"), JSON.stringify(report, null, 2));
console.log(`report: ${join(OUT, "report.json")}`);
} finally {
ws.close();
chrome.kill();
await sleep(300);
rmSync(profile, { recursive: true, force: true });
}
@@ -53,10 +53,12 @@ login.css ≤ 12 KB, menu-aurora.js ≤ 22 KB, router-aurora.js ≤ 15 KB, admin
## Patches contract (third-party compatibility)
File name = the page's `data-page` prefix; prefix matching on segment boundaries; both
CSS and JS discovered per render by `lsdir()` in `header.ut:30`; any package may drop a
file into `/www/luci-static/aurora/patches/`. Known quirks already patched: dashboard,
qmodem (+sms/conversation), modemdata, openclash config/settings, statistics graphs
(dark invert), diskman, filemanager, network bridge-vlan; log viewer JS on
`admin-status-logs` (ids `#syslog`, third-party `#log_textarea`).
file into `/www/luci-static/aurora/patches/`. Every page-scoped rule — patches and
the page selectors inside the component partials alike — has an entry in
`.dev/compat/pages/<data-page prefix>.md` (page, package, menu path, DOM, cause, fix,
verification); grep it for a file or selector before changing either
(`luci-dom-compat` skill). Geometric ones are guarded by `.dev/compat/fixtures/` via
`tests/compat.test.js`.
## Verification available
`cd .dev && pnpm test` (140 tests, ~1 s) · `pnpm build` · on-device:
+91
View File
@@ -0,0 +1,91 @@
# DOM compat ledger
Every page-scoped rule in the theme exists because some page's DOM did not
sit right under Aurora. This directory records which page, which package,
where in the menu, which element, what went wrong, why the rule is written
the way it is, and how it was verified — so the next edit to `_table.css`
can see what it is about to undo.
"compat" means page DOM ↔ theme. Stock LuCI pages (Software, Mount Points,
Wireless) count as much as third-party apps.
```
pages/<data-page prefix>.md one file per page prefix; entries appended in time order
fixtures/<data-page>--<slug>.html trimmed DOM + checks, run by tests/compat.test.js
```
The page prefix follows the same rule as `media/patches/`: the file
`admin-system-diskman.md` also covers `admin-system-diskman-partition-…`.
Rules that are not tied to one page but were added for one (`.td.cbi-value-field`
for Dockerman) go under the page that motivated them, and the `dom:` line
names the selector so a grep for the selector finds the entry.
## Entry format
```
## <slug> — <one line: what the rule does>
- page: <data-page> · <Menu → Path as shown in the UI>
- package: <luci-app-x (author)> | stock <luci-mod-x>
- dom: <selector / element, plus the inline style or attribute the fix depends on>
- symptom: <what the user sees>
- cause: <why, one or two lines> | unknown (<what the record lacks>)
- fix: <file> `<selector>` <utilities> · <commit> · <version>
- verified: <device/fixture, method, numbers before → after> | not measured
- fixture: fixtures/<name>.html | none (<why>)
- issue: #n | — · mirror: shadcn <commit> | pending | n/a · status: active | removed <commit>
```
Facts only. A cause that cannot be recovered from history is written as
`unknown`, never guessed. `status: removed` entries stay: a removed rule is
the one most likely to be re-added blindly.
Before editing a component partial or a patch, grep here for the file and
every selector you touch:
```
grep -rn "_table.css" .dev/compat/pages/
grep -rln "status_leases" .dev/compat/pages/
```
## Fixtures
A fixture is a trimmed copy of the real page: `<body data-page="…">`,
`#maincontent > #view`, only the offending block (three table rows are
enough), any `<style>` the page injects itself, and the theme CSS by relative
path. Keep the viewport meta: without it a mobile viewport lays out at 980px
and the `max-md:` card rules never apply. When the wrap depends on the
content width, put the width measured on the device as `max-width` on the
block's own container (not on `.cbi-section`, whose padding eats into it).
The runner injects the theme font itself.
```html
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../htdocs/luci-static/aurora/main.css">
<!-- add ../../../htdocs/luci-static/aurora/patches/<prefix>.css when the page has one -->
<script type="application/json" id="compat">
{ "ledger": "#65", "viewports": [1280, 390],
"checks": [ { "nowrap": "table[id^=partition-table] td" },
{ "fits": "table[id^=partition-table]" } ] }
</script>
```
Checks:
| check | passes when |
|---|---|
| `nowrap: <sel>` | every matched element's text sits on one line, and no text starts below an inline checkbox/indicator inside it |
| `fits: <sel>` | `scrollWidth ≤ clientWidth + 1` and the element's right edge is inside its parent's |
| `js: "<expr>"` | the expression is truthy in the page — escape hatch; the ledger entry says why |
A check may carry its own `"viewports": [...]` (a mobile card layout wraps on
purpose). Capture the DOM from a device running this theme (`.dev/mocks/`
snapshot, DevTools `copy(el.outerHTML)`, or the probe script in
`.claude/skills/luci-dom-compat/scripts/`), strip it down, and confirm the
check fails against the pre-fix CSS before committing it.
## Running
`cd .dev && pnpm build && pnpm test` — `tests/compat.test.js` opens each
fixture in headless Chrome over CDP (`CHROME_BIN`, then the macOS app path,
then `google-chrome`/`chromium` on PATH). Without Chrome the test skips with
a hint; with `CI` set it fails.
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>compat: Dockerman overview info table (#96)</title>
<link rel="stylesheet" href="../../../htdocs/luci-static/aurora/main.css">
<script type="application/json" id="compat">
{
"ledger": "#96",
"viewports": [1440, 1024, 390],
"checks": [
{ "nowrap": "td.cbi-value-field[data-name=entry]" },
{ "fits": "table.table" }
]
}
</script>
</head>
<body data-page="admin-services-dockerman-overview">
<div id="maincontent">
<div id="view">
<div class="cbi-map">
<div class="cbi-section cbi-tblsection">
<table class="table cbi-section-table"><thead class="thead cbi-section-thead"></thead><tbody class="tbody cbi-section-tbody"><tr id="cbi-json-ib12" class="tr cbi-section-table-row" data-sid="ib12" data-section-id="ib12"><td class="td cbi-value-field" data-title="Entry" data-description="" data-name="entry" data-widget="CBI.DummyValue"><div id="cbi-json-ib12-entry" data-index="0" data-depends="[]" data-field="cbid.json.ib12.entry"><output for="cbid.json.ib12.entry">Plugins</output></div></td><td class="td cbi-value-field" data-title="Value" data-description="" data-name="value" data-widget="CBI.DummyValue"><div id="cbi-json-ib12-value" data-index="1" data-depends="[]" data-field="cbid.json.ib12.value"><output for="cbid.json.ib12.value">{"Volume":["local"],"Network":["bridge","host","ipvlan","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","local","splunk","syslog"]}</output></div></td></tr><tr id="cbi-json-ib13" class="tr cbi-section-table-row" data-sid="ib13" data-section-id="ib13"><td class="td cbi-value-field" data-title="Entry" data-description="" data-name="entry" data-widget="CBI.DummyValue"><div id="cbi-json-ib13-entry" data-index="0" data-depends="[]" data-field="cbid.json.ib13.entry"><output for="cbid.json.ib13.entry">MemoryLimit</output></div></td><td class="td cbi-value-field" data-title="Value" data-description="" data-name="value" data-widget="CBI.DummyValue"><div id="cbi-json-ib13-value" data-index="1" data-depends="[]" data-field="cbid.json.ib13.value"><output for="cbid.json.ib13.value">true</output></div></td></tr><tr id="cbi-json-ib24" class="tr cbi-section-table-row" data-sid="ib24" data-section-id="ib24"><td class="td cbi-value-field" data-title="Entry" data-description="" data-name="entry" data-widget="CBI.DummyValue"><div id="cbi-json-ib24-entry" data-index="0" data-depends="[]" data-field="cbid.json.ib24.entry"><output for="cbid.json.ib24.entry">CgroupDriver</output></div></td><td class="td cbi-value-field" data-title="Value" data-description="" data-name="value" data-widget="CBI.DummyValue"><div id="cbi-json-ib24-value" data-index="1" data-depends="[]" data-field="cbid.json.ib24.value"><output for="cbid.json.ib24.value">cgroupfs</output></div></td></tr><tr id="cbi-json-ib39" class="tr cbi-section-table-row" data-sid="ib39" data-section-id="ib39"><td class="td cbi-value-field" data-title="Entry" data-description="" data-name="entry" data-widget="CBI.DummyValue"><div id="cbi-json-ib39-entry" data-index="0" data-depends="[]" data-field="cbid.json.ib39.entry"><output for="cbid.json.ib39.entry">Runtimes</output></div></td><td class="td cbi-value-field" data-title="Value" data-description="" data-name="value" data-widget="CBI.DummyValue"><div id="cbi-json-ib39-value" data-index="1" data-depends="[]" data-field="cbid.json.ib39.value"><output for="cbid.json.ib39.value">{"io.containerd.runc.v2":{"path":"runc","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.2.1\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"time\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true,\"rdma\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SLine truncated
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>compat: mini-diskmanager partition table (#65)</title>
<link rel="stylesheet" href="../../../htdocs/luci-static/aurora/main.css">
<!-- The rule the plugin injects into <head> for the colour swatch in front of
the filesystem name; the wrap in #65 is "swatch on one line, name below". -->
<style>
.partition-color-indicator {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid;
border-radius: 3px;
margin-right: 8px;
vertical-align: middle;
opacity: 0.85;
}
</style>
<script type="application/json" id="compat">
{
"ledger": "#65",
"viewports": [1280, 390],
"checks": [
{ "nowrap": "#partition-table-mmcblk0 tr.tr:not(.table-titles) td" },
{ "fits": "#partition-table-mmcblk0" },
{ "js": "getComputedStyle(document.querySelector('#partition-table-mmcblk0 td')).fontSize === '12px'" }
]
}
</script>
</head>
<body data-page="admin-services-mini-diskmanager-minidiskmanager">
<div id="maincontent">
<div id="view">
<div class="cbi-section">
<!-- 1153px is the table width measured on the device at a 1280px viewport;
max-width keeps the mobile viewport unconstrained. -->
<div class="partition-table-container" style="max-width: 1153px">
<table class="table" id="partition-table-mmcblk0" style="border:1px solid var(--border-color-medium)!important; table-layout:fixed; border-collapse:collapse; width:100%; font-size:12px;"><tr class="tr table-titles"><th class="th left" style="width: 180px; min-width: 180px; text-align: left; cursor: default;" data-sortable-row="true"><span style="display: inline-block;"><input type="checkbox" id="wipeall-checkbox" title="Select all partitions for wiping" style="margin-right: 8px; vertical-align: middle; cursor: pointer;" disabled=""></span><span style="pointer-events: none; user-select: none;">Partition</span></th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Type / Filesystem</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Mount Point</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Label</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Size</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Used</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Unused</th><th class="th left" style="width:12.50%; min-width: 90px; text-align: left; cursor: default; pointer-events: none;" data-sortable-row="true">Flags</th></tr><tr class="tr cbi-rowstyle-2"><td class="td left" data-title="Partition"><label data-tooltip="Click to select" style="cursor: pointer; display: inline-flex; align-items: center; gap:6px;"><input type="checkbox" class="partition-select-checkbox" name="partition_select" value="mmcblk0p2" data-partition="mmcblk0p2" aria-label="/dev/mmcblk0p2"><span>/dev/mmcblk0p2</span></label></td><td class="td left" data-title="Type / Filesystem"><span class="partition-color-indicator 0bootconfig"></span> 0BOOTCONFIG</td><td class="td left" data-title="Mount Point">-</td><td class="td left" data-title="Label">-</td><td class="td left" data-title="Size">256.00 KiB</td><td class="td left" data-title="Used">-</td><td class="td left" data-title="Unused">-</td><td class="td left" data-title="Flags">-</td></tr><tr class="tr cbi-rowstyle-1"><td class="td left" data-title="Partition"><label data-tooltip="Click to select" style="cursor: pointer; display: inline-flex; align-items: center; gap:6px;"><input type="checkbox" class="partition-select-checkbox" name="partition_select" value="mmcblk0p18" data-partition="mmcblk0p18" aria-label="/dev/mmcblk0p18"><span>/dev/mmcblk0p18</span></label></td><td class="td left" data-title="Type / Filesystem"><span class="partition-color-indicator squashfs"></span> SQUASHFS</td><td class="td left" data-title="Mount Point">/rom</td><td class="td left" data-title="Label">-</td><td class="td left" data-title="Size">2.00 GiB</td><td class="td left" data-title="Used">110.0 MiB</td><td class="td left" data-title="Unused">0</td><td class="td left" data-title="Flags">-</td></tr><tr class="tr cbi-rowstyle-2"><td class="td left" data-title="Partition"><label data-tooltip="Click to select" style="cursor: pointer; display: inline-flex; align-items: center; gap:6px;"><input type="checkbox" class="partition-select-checkbox" name="partition_select" value="mmcblk0p6" data-partition="mmcblk0p6" aria-label="/dev/mmcblk0p6"><span>/dev/mmcblk0p6</span></label></td><td class="td left" data-title="Type / Filesystem"><span class="partition-color-indicator ext4"></span> Ext4</td><td class="td left" data-title="Mount Point">/mnt/mmcblk0p6</td><td class="td left" data-title="Label">-</td><td class="td left" data-title="Size">113.26 GiB</td><td class="td left" data-title="Used">2.0 MiB</td><td class="td left" data-title="Unused">105.2 GiB</td><td class="td left" data-title="Flags">storage</td></tr></table>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,25 @@
# admin-dashboard
Patch file: `.dev/src/media/patches/admin-dashboard.css` (scope `[data-page="admin-dashboard"] .Dashboard > .section-content`, line 8-10). Route: main.css `@layer luci-mod-dashboard` (7f25128) → `_patches.css` (aedf3dc, 7d9f6a1) → own on-demand file with `@reference`/`@apply` (1c924cf, PR #82) → native CSS (e14cfcd). Page package: stock luci-mod-dashboard (menu.d "admin/dashboard" "Dashboard"); it ships its own `view/dashboard/css/custom.css`.
## dashboard-cards-and-dark — card decoration on `.dashboard-bg`, dark-mode svg invert, title rule, wifi device cell
- page: admin-dashboard · Dashboard
- package: stock luci-mod-dashboard
- dom: `.dashboard-bg` (card), `.dashboard-bg.tr` (row variant, no radius), `img[src*=".svg"]`, `.title h3`, `.router-status-wifi .wifi-info .devices-info .tr .td:nth-child(3)`
- symptom: not recorded beyond the subjects ("dark mode display issue in luci-mod-dashboard"; 56db83c: "Adapt for missing data-page="admin-dashboard" on default login redirect to dashboard", "Remove border-radius on .dashboard-bg for mobile view")
- cause: unknown for the card/wifi/h3 rules (7f25128 has subject only). For the scope: 56db83c body says the `[data-page="admin-dashboard"]` attribute was missing when the login redirect landed on the dashboard, so the scope was dropped; 1c924cf restored it when the patch became on-demand (PR #82: "Backfilled missing data-page scoping in the original patches (… dashboard)"), and header.ut now computes the page name from `ctx.path` when `request_path` is empty (DEVELOPMENT.md §On-Demand Third-Party Patches, item 1)
- fix: patches/admin-dashboard.css — `.dashboard-bg { background: var(--surface-overlay) }` + `&.tr { border-radius: 0 }` + `&:not(.tr) { border: 1px solid var(--hairline); box-shadow: var(--app-shadow-lg); &:hover {…} }` (line 11-27); `img[src*=".svg"] { [data-darkmode="true"] & { filter: invert(100%) } }` (line 38-42); `.title h3 { color: var(--text); border: 0; padding-bottom: 1rem; @media (width < 48rem) { margin-inline: 0; padding-bottom: 0.5rem } }` (line 93-102); `.router-status-wifi .wifi-info .devices-info .tr .td { &:nth-child(3) { position: relative; top: 0; width: 100%; padding-inline: 0 } @media (width < 48rem) { flex: 1 1 50% } }` (line 104-117) · 7f25128 2025-09-28 · 0.5.1_beta (first tag v0.5.5_beta); 56db83c 2025-10-12 · 0.5.9_beta (first tag v0.6.0_beta) dropped the scope and added `.tr { rounded-none }`; the original `.router-status-lan .devices-info tr th { max-md:flex-[1_1_50%] }` from 7f25128 was removed by c3fae55 2025-09-29 "fix: mobile display issue in luci-mod-dashboard th" (subject only)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## dashboard-dark-text-and-inline-tables — force theme text colour in dark mode, compact assoclist cells, undecorate inline info tables
- page: admin-dashboard · Dashboard (issue #66 names the System, DHCP Devices and Wireless panels)
- package: stock luci-mod-dashboard
- dom: `.dashboard-bg span/td/th` (dark mode), `.dashboard-bg .td/.th` (assoclist cells carry `.td/.th` classes), `.dashboard-bg .table:not(.assoclist)` with its `tr` and `td` (the DNS-style inline info tables)
- symptom: issue #66: "Using luci-mod-dashboard with aurora in dark mode results in multiple UI readability and layout issues … Some elements are rendered with very low contrast (almost black on dark background), and spacing/padding in certain tables and panels (System, DHCP Devices, Wireless) appears inconsistent." (OpenWrt 25.12.4, two screenshots)
- cause: the patch's own comment: "Dark mode: override custom.css hardcoded light-mode colors on all text nodes" — luci-mod-dashboard's `custom.css` sets text colours the theme's dark palette does not reach; the spacing/inline-table rules have no recorded cause (944e7c5 has subject only)
- fix: patches/admin-dashboard.css — `& span, & td, & th { [data-darkmode="true"] & { color: var(--text) !important } }` (line 30-36); `& .td { padding-block: 0.5rem; @media (width < 48rem) { padding-block: 0.25rem } }` and `& .th { padding-block: 0.5rem }` (line 45-54); `& .table:not(.assoclist) { margin-bottom: 0; border-radius: 0; border: 0; background: transparent; box-shadow: none; & tr { &:hover { background: transparent } @media (width < 48rem) { display: table-row; border: 0; padding: 0 } } & td { border: 0; padding: 0.125rem 0; @media (width < 48rem) { display: table-cell } &:first-child { padding-right: 0.5rem; white-space: nowrap; opacity: 0.7 } } }` (line 57-91) · 944e7c5 2026-05-31 (in `_patches.css`) · 0.11.10 (first tag v0.12.0)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: #66 (still open) · mirror: pending · status: active
@@ -0,0 +1,12 @@
# admin-modem-modemdata-modempreview
## modemdata-badge-width — let signal badges inside info cards size to content
- page: admin-modem-modemdata-modempreview · Modem → Modem Data → (preview) (menu path inferred from the page name; not in the local LuCI checkouts)
- package: luci-app-modemdata (third-party; named in the patch header and PR #80)
- dom: `.ifacebox-body .ifacebadge` on the "General Information" cards (PR #80 text)
- symptom: PR #80 body: "Aurora's default ifacebox styles spread modemdata rows apart, wrap signal badges vertically, and leave cards at uneven heights."
- cause: unknown beyond the PR wording — the merged diff (910c85d, 12 lines) contains only the badge width override; the row-spacing and card-height parts of the PR description have no rule in the commit
- fix: patches/admin-modem-modemdata-modempreview.css line 8-12 `[data-page="admin-modem-modemdata-modempreview"] { & .ifacebox-body .ifacebadge { width: auto !important; } }` · 910c85d 2026-06-28 (author CJay D Acopra, PR #80) · 1.0.2 (first tag v1.1.0) — added to `_patches.css` as `@apply w-auto!`, split into its own file by 1c924cf, native CSS by e14cfcd
- verified: not measured (no record)
- fixture: none (backfill)
- issue: PR #80 · mirror: pending · status: active
@@ -0,0 +1,12 @@
# admin-modem-qmodem
## qmodem-theming — colours, borders, hover states, tab and table styling across the QModem pages
- page: admin-modem-qmodem (prefix; covers `-overview`, `-sms`, `-sms-conversation-<contact>`, `-sms_sim`, `-config_advanced`, `-settings`, `-network_config`) · Modem → QModem → … (menu paths inferred from the page names; the app is not in the local LuCI checkouts)
- package: luci-app-qmodem (third-party; named in the patch header and PR #92)
- dom: `[data-page^="admin-modem-qmodem"]`: `.copyright-section`, `fieldset.cbi-section > legend`, `fieldset.cbi-section`, `.cbi-section-table-titles.named::before` (line 9-42); `[data-page^="admin-modem-qmodem-sms-conversation"]`: `#sms-messages-area`, `.sms-message-bubble`, `.sms-message-content`, `.sms-message-meta` (line 44-64); `[data-page="admin-modem-qmodem-config_advanced"]`: `.cbi-tabmenu li` and `.cbi-tab` (line 66-96; the file's comment: "targets <li> directly (not li > a) because qmodem's tab markup has no <a> inside tabs"); `[data-page="admin-modem-qmodem-sms"]`: `.td > div`, `.td > span[style*="margin-left"]`, `.sms-conversations` (+ `> .cbi-section`, `fieldset.cbi-section`, `.table.cbi-section-table`), `.cbi-section-table-row.tr` (+ hover), `.sms-unread-conversation`, `.sms-unread-badge` (line 98-175); `[data-page="admin-modem-qmodem-sms_sim"]`: `.cbi-section-table-row.tr` (+ hover) (line 177-197); `[data-page="admin-modem-qmodem-settings"]`: `.cbi-section-table-descr.named::before` (line 201-206)
- symptom: not recorded per rule. PR #92 body: "Styles qmodem overview, SMS, conversation, advanced config, network config, and settings pages with consistent theming (borders, colors, hover states, table formatting, and tab styling)."
- cause: unknown (commit 7066351 has subject only; the PR body lists scope, not what each page did wrong)
- fix: patches/admin-modem-qmodem.css (whole file) · 7066351 2026-07-25 (author shiraneko, PR #92 by cjayacopra) · 1.1.2 (first tag v1.2.0) — written with `@reference`/`@apply`, rewritten natively by e14cfcd 2026-07-27 (1.1.8; radii became `calc(var(--radius-base) * n)`)
- verified: not measured (no record)
- fixture: none (backfill; DOM snapshots exist in `.dev/mocks/admin-modem-qmodem*.html` — overview, sms, sms_sim, config_advanced, settings, network_config)
- issue: PR #92 · mirror: pending · status: active
@@ -0,0 +1,47 @@
# admin-network-network
## bridge-vlan-descr-hidden — hide the table description row on mobile
- page: admin-network-network · Network → Interfaces → Devices → Bridge VLAN Filtering (efa07d8 body: "Network -> Interfaces -> Devices -> Bridge VLAN Filtering")
- package: stock luci-mod-network
- dom: `.table tr.cbi-section-table-descr` — global selector, added for this page
- symptom: not recorded beyond "to improve layout" (efa07d8 body)
- cause: unknown (efa07d8 body states the action, not what the descr row did wrong)
- fix: components/_table.css `&.table-titles, &.cbi-section-table-titles, &.cbi-section-table-descr { max-md:hidden }` (line 58-62) — `.cbi-section-table-descr` was added to the existing list · efa07d8 2025-09-18 · 0.3.7_alpha (first tag v0.4.0-alpha)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## ifacebox-cells-full-row — interface status/box cells stack full-width on mobile
- page: admin-network-network · Network → Interfaces (menu.d title "Interfaces")
- package: stock luci-mod-network
- dom: `td[data-name="_ifacebox"]`, `td[data-name="_ifacestat"]` — DummyValue cells of the `interface` GridSection (interfaces.js:296-297 query `[data-name="_ifacestat"] > div` and `[data-name="_ifacebox"] .ifacebox-body`)
- symptom: not recorded (bec6830 subject: "feat: enhance ifacebox display in td")
- cause: unknown (commit bec6830 has subject only)
- fix: components/_table.css `[data-page="admin-network-network"] & { &[data-name="_ifacebox"], &[data-name="_ifacestat"] { … } }` nested under `& td, & .td` (line 214-219) `max-md:basis-[100%]` · bec6830 2025-09-23 · 0.4.3_alpha (first tag v0.5.0_beta). The same commit added the `td &` variants of `.ifacebox` that 397e46b later moved under `#cbi-network-interface` (next entry)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## interface-cards-floor — equal-width interface cards, horizontal card layout on mobile
- page: admin-network-network · Network → Interfaces
- package: stock luci-mod-network
- dom: `#cbi-network-interface` (id of `form.Map('network')` + GridSection `'interface'`, interfaces.js:497-501) — five descendant rules on `.ifacebox`, `.ifacebox-head`, `.ifacebox-body`, `.ifacebox-body > div`, `.ifacebox-body small`
- symptom: 397e46b body: "The interfaces overview sizes every card by its content, so a bridge carrying seven device icons rendered far wider than a single-port one."
- cause: 397e46b body: "The shared `.ifacebox` min-width had been masking half of that until 1.2.7 lowered it to stop the port status grid overlapping. The floor now sits on the overview's own section instead — `#cbi-network-interface`, the scope luci-theme-bootstrap already uses for this page — at 152px, and the five `td &` rules that only ever applied there move under it too rather than reaching every `.ifacebox` inside any table cell."
- fix: components/_card.css `#cbi-network-interface &` at line 72-74 (`.ifacebox`: `max-md:flex-row md:min-w-38`), 83-85 (`.ifacebox-head`: `max-md:flex max-md:w-auto max-md:shrink-0 max-md:items-center max-md:justify-center max-md:rounded-l-3xl max-md:rounded-tr-none max-md:border-r max-md:border-b-0`), 107-109 (`.ifacebox-body`: `max-md:flex-row max-md:items-center max-md:rounded-r-3xl max-md:rounded-bl-none max-md:py-2 max-md:pr-2 max-md:pl-4`), 144-146 (`.ifacebox-body > div`: `max-md:w-auto max-md:flex-1 max-md:space-y-1`), 163-165 (`.ifacebox-body small`: `max-md:mt-0`) · 397e46b 2026-08-30 · 1.2.10 (first tag v1.3.0). The mobile rules were first written as `td &` in bec6830 (2025-09-23)
- verified: no before/after measurement of the card widths recorded; the body states the size changes it made: "icons 24px -> 20px, the gap between adjacent ones 8px -> 4px, card padding 16px -> 12px and the label line spacing 16px -> 4px", floor 152px, `.ifacebox-body > img` / badge icons "intrinsic 32px" → 20px, zone swatch 33px tall → vertical padding 2px
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## bridge-vlan-dropdown-clip — let the Bridge VLAN table's dropdowns escape their wrapper on mobile
- page: admin-network-network · Network → Interfaces → Devices → Configure → Bridge VLAN filtering (path from efa07d8's wording of the same table; c5a2dff itself only says "bridge-vlan table")
- package: stock luci-mod-network
- dom: `[data-name="bridge-vlan"] > div` — the SectionValue option `bridge-vlan` wrapping the Bridge VLAN TableSection (tools/network.js:1354); patch rule under `@media (width < 48rem)`
- symptom: dropdown clipped on mobile (c5a2dff subject: "fix(mobile): resolve clipped dropdown in bridge-vlan table")
- cause: unknown (commit c5a2dff has subject only; the diff shows only the override, not which rule clipped the dropdown)
- fix: patches/admin-network-network.css line 8-12 `@media (width < 48rem) { [data-name="bridge-vlan"] > div { overflow: visible !important; } }` · c5a2dff 2025-10-19 · 0.6.1_beta (first tag v0.7.0_beta) — written as `@layer patches > admin-network-network { … @apply max-md:!overflow-visible }` in main.css, moved to `_patches.css` by aedf3dc, split into its own on-demand file by 1c924cf (#82, `@reference` + `@apply`), rewritten as native CSS by e14cfcd
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
See also: e86b70f lists "Network => (Interfaces, Wireless, Firewall)" for the `.cbi-section-actions` mobile rule — recorded in `admin-status-overview.md`.
@@ -0,0 +1,14 @@
# admin-network-wireless
## assoclist-full-row-cells — Associated Stations cells take a full row on mobile
- page: admin-network-wireless · Network → Wireless, "Associated Stations" table (wireless.js:2667 creates `#wifi_assoclist_table`, h3 at :2680). The same id is also created by luci-mod-status 60_wifi.js:243 on Status → Overview; 1dbd95a does not say which page it was written against
- package: stock luci-mod-network (and luci-mod-status for the overview copy)
- dom: `#wifi_assoclist_table td`, `#wifi_assoclist_table .td` (class `table assoclist`); rule also moves the `data-title` ::before label (`before:max-md:mb-1.5`)
- symptom: not recorded (1dbd95a subject: "Ensure each table cell in Associated Stations takes up a full row on mobile")
- cause: unknown (commit 1dbd95a has subject only)
- fix: components/_table.css `#wifi_assoclist_table &` nested under `& td, & .td` (line 156-158) `max-md:flex-[0_0_100%] max-md:px-3 max-md:py-2 before:max-md:mb-1.5` · 1dbd95a 2025-10-17 · 0.5.14_beta (first tag v0.6.0_beta)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
See also: 50c2b84 lists "Network => Wireless host: Fix overflow when td text is too long" as one motivation for `.td.cbi-value-field { break-all }` — that lineage is recorded in `admin-services-dockerman.md` (the rule's current form comes from a2d110a). e86b70f lists "Network => (Interfaces, Wireless, Firewall)" for the `.cbi-section-actions` mobile rule — recorded in `admin-status-overview.md`.
@@ -0,0 +1,13 @@
# admin-services-dockerman
## info-table-entry-column — value cells break only words that do not fit
- page: admin-services-dockerman-overview · Services → Dockerman JS → Overview
- package: luci-app-dockerman (openwrt/luci, the JS rewrite)
- dom: `.cbi-section.cbi-tblsection > table.table.cbi-section-table > tr.cbi-section-table-row > td.td.cbi-value-field[data-name=entry|value]` — `docker info` rendered as two-column TableSections; the value column holds multi-KB JSON with no spaces (Runtimes, Plugins)
- symptom: the entry column collapsed to ~42px and showed one character per line (`MemoryLimit` over 7–8 lines) beside the JSON values
- cause: `.td.cbi-value-field` carried `word-break: break-all` at every width (added in 50c2b84 as an overflow guard). `break-all` drops every cell's min-content to about one character; auto table layout then hands spare width out by max-content, so the JSON took nearly all of it. bootstrap sets no word-break on desktop cells — its labels stay whole but the JSON stretches the table to ~3.5k px and the page scrolls sideways.
- fix: `components/_table.css` `& td, & .td { &.cbi-value-field { wrap-anywhere md:max-w-prose } }` — `wrap-anywhere` keeps break-all's min-content (same overflow guard) but only breaks a word that does not fit; `md:max-w-prose` caps each cell's intrinsic contribution so one huge value cannot starve the other columns (the used width may still exceed it: 944px measured) · a2d110a · 1.3.2. Lineage: 50c2b84 2025-10-12 (0.5.8_beta) added `&.cbi-value-field { break-all }` for "System => (Software, Mount Points): Fix overflow when td text is too long" and "Network => Wireless host: Fix overflow when td text is too long", replacing the global `td max-w-94` it removed for passwall (see `admin-services-passwall.md`); 031283c 2026-07-09 added the mobile-card utilities on the same rule (`max-md:mb-1 max-md:basis-full max-md:self-start max-md:text-left` and the `[data-widget=…] max-md:basis-1/2` sub-rule)
- verified: device, Chromium, Environment table before → after: entry column 42 → 260px at 1440, 42 → 196 at 1024, 41 → 152 at 800; short words split across lines 41 → 0 / 0 / 2; mobile cards unchanged; section height 8301 → 3797px at 1440; firewall zone and rule GridSections kept identical column widths at 1440/1024/800/390; a file:// fixture gave the same result in Chromium, WebKit and Firefox. Side effect noted, not measured: on admin-system-mounts the GridSection value cells take the 65ch cap instead of the page's `max-w-104` (same specificity, later rule).
- fixture: fixtures/admin-services-dockerman-overview--info-table.html (entry column `nowrap` at 1440/1024/390 — at 800 two short words still split, so that width is not asserted)
- issue: #96 · mirror: shadcn f4d7219 · status: active
@@ -0,0 +1,13 @@
# admin-services-mini-diskmanager
## partition-table-font — the table's own font-size reaches its cells
- page: admin-services-mini-diskmanager-minidiskmanager · Services → Disk Manager
- package: luci-app-mini-diskmanager (4IceG/luci-app-mini-diskmanager)
- dom: `table#partition-table-<dev> > tr.tr > td.td` — the `<table>` carries inline `table-layout:fixed; width:100%; font-size:12px`; first `th` 180px, the other seven `width:12.5%; min-width:90px`; the type cell is `<span class="partition-color-indicator"> NAME`
- symptom: type/filesystem and mount-point cells wrap — swatch on one line, name below; long names break mid-word (`0BOOTCONFIG` / `1`, `/mnt/mmcblk0` / `p6`)
- cause: `td, .td` set `text-base` (16px) directly, so the table's inline 12px never reached the cells; fixed columns sized for 12px got 16px text and `wrap-break-word` split what did not fit. bootstrap puts its font size on `.table` and lets cells inherit.
- fix: `components/_table.css` `table.table, .table` `text-base max-md:text-sm` (moved off `td, .td`, which now inherit) · dd7c64b · 1.3.3
- verified: device 192.168.8.1, CDP before/after in one page load — cells 16 → 12px; wrapped cells 5 → 1 at 1280 (the remaining `0BOOTCONFIG1` wraps under bootstrap at that width too), 5 → 0 at 1920, 0 → 0 at 390; eleven stock pages (overview, routes, processes, interfaces, firewall zones/rules, DHCP, wireless, mounts, startup, SSH keys) showed zero font-size/line-height changes on any text-bearing table element at 1280 and 390
- fixture: fixtures/admin-services-mini-diskmanager-minidiskmanager--partition-table.html
- issue: #65 · mirror: shadcn 4f1c7f4 · status: active
@@ -0,0 +1,12 @@
# admin-services-openclash-config
## openclash-subinfo-icons — fix the subscription-info icon size
- page: admin-services-openclash-config · Services → OpenClash → Config Manage (controller openclash.lua:84 entry title "Config Manage")
- package: luci-app-openclash (vernesong; the ImmortalWrt feed copy was checked)
- dom: `.sub_div img` — `.sub_div` comes from `luasrc/view/openclash/sub_info_show.htm`, the template used by config.lua:191
- symptom: not recorded (87522d8 subject: "feat: add openclash patch and remove btn spacing")
- cause: unknown (commit 87522d8 has subject only)
- fix: patches/admin-services-openclash-config.css line 8-13 `[data-page="admin-services-openclash-config"] { .sub_div img { height: 1.25rem; width: 1.25rem; } }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha) — written as `@layer patches > luci-app-openclash { [data-page=…] .sub_div img { @apply h-5 w-5 } }` in main.css; `_patches.css` via aedf3dc; own file via 1c924cf; native via e14cfcd
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
@@ -0,0 +1,12 @@
# admin-services-openclash-log
## openclash-log-dark-bg — dark page background on the Server Logs page
- page: admin-services-openclash-log · Services → OpenClash → Server Logs (controller openclash.lua:85 entry title "Server Logs")
- package: luci-app-openclash (vernesong)
- dom: `[data-page="admin-services-openclash-log"]` (the body element itself)
- symptom: not recorded (87522d8 subject only)
- cause: unknown (commit 87522d8 has subject only)
- fix: (historic, main.css `@layer patches > luci-app-openclash`) `[data-page="admin-services-openclash-log"] { @apply dark:bg-aurora-night-teal; }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: n/a · status: removed 30f922d (2025-11-09 "chore: remove patch styles and adjust DOM indentation" — body: "Removed temporary patch styles … Test case: openclash → Server Logs (dark mode background)"; 0.7.9_beta, first tag v0.8.0_beta)
@@ -0,0 +1,23 @@
# admin-services-openclash-settings
## openclash-settings-select-full — selects fill the value field on mobile
- page: admin-services-openclash-settings · Services → OpenClash → Plugin Settings (controller openclash.lua:71 entry title "Plugin Settings")
- package: luci-app-openclash (vernesong)
- dom: `.cbi-value-field .cbi-input-select` under `@media (width < 48rem)`
- symptom: not recorded (a667f02 subject: "feat: add openclash cbi-input-select style patch")
- cause: unknown (commit a667f02 has subject only; its diff is a whole-file rewrite of main.css, the rule as first written was `[data-page="admin-services-openclash-settings"] .cbi-value-field .cbi-input-select { @apply max-md:!w-full }`)
- fix: patches/admin-services-openclash-settings.css line 9-13 `@media (width < 48rem) { .cbi-value-field .cbi-input-select { width: 100% !important; } }` · a667f02 2025-09-26 · 0.4.6_alpha (first tag v0.5.0_beta) — `_patches.css` via aedf3dc; own file via 1c924cf; native via e14cfcd
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## openclash-diag-grid — stack the Debug Logs diagnostic controls in one column on mobile
- page: admin-services-openclash-settings · Services → OpenClash → Plugin Settings → Debug Logs tab (settings.lua:81 tab "debug", :1308 DummyValue rendering `debug.htm`)
- package: luci-app-openclash (vernesong)
- dom: `.diag-style` and `.diag-style > div` — from `luasrc/view/openclash/debug.htm` (which itself styles `.diag-style` for `[data-darkmode="true"]` at its line 12-17)
- symptom: not recorded (87522d8 subject only)
- cause: unknown (commit 87522d8 has subject only)
- fix: patches/admin-services-openclash-settings.css line 15-33 `.diag-style { & > div { display: flex; flex-direction: column; align-items: center; gap: 0.5rem } @media (width < 48rem) { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 1rem; & > div { width: 100% !important; gap: 0.25rem } } }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha) — first written unscoped under `@layer plugins > luci-app-openclash` (applied on every page); 7d9f6a1 merged `_plugins.css` into `_patches.css`; 1c924cf scoped it to this page (PR #82: "openclash `.diag-style` → settings"); native via e14cfcd
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
@@ -0,0 +1,14 @@
# admin-services-passwall
## td-global-max-width — a global `max-w-94` on every table cell
- page: admin-services-passwall-node_list · Services → PassWall → Node List (controller passwall.lua:39 entry title "Node List") — the page that got the rule removed; the page it was added for is not recorded
- package: luci-app-passwall (xiaorouji; the ImmortalWrt feed copy was checked)
- dom: `.table td, .table .td` (global) — on Node List the `td.cbi-section-actions` cell holds the row's action buttons
- symptom: on removal — 50c2b84 subject: "Services => passwall => Node List cbi-section-actions: Fix overflow beyond td max width" (the action buttons ran past the capped cell). On addition — not recorded
- cause: addition: unknown (efa07d8's body describes two other changes — "Removed conflicting styling that prevented setting `overflow-auto`, which was necessary to maintain the sticky positioning of `th` elements" and hiding `.cbi-section-table-descr` on the Bridge VLAN page — and says nothing about `max-w-94`, which the diff adds to td alongside `max-md:max-w-full` and col caps). Removal: the fixed cap could not hold the Node List action cell (50c2b84 subject); d276fd3 later restates the lesson: "the fix avoids applying a global maximum width to table data cells (`td`) to prevent regressions in CBI section actions (e.g., in Services -> passwall -> Node List)"
- fix: (historic, main.css `& td, & .td`) `max-w-94` · efa07d8 2025-09-18 · 0.3.7_alpha (first tag v0.4.0-alpha)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: n/a · status: removed 50c2b84 (2025-10-12 "fix: table content overflow", 0.5.8_beta, first tag v0.6.0_beta). The same commit added `.td.cbi-value-field { break-all }` as the replacement overflow guard for "System => (Software, Mount Points)" and "Network => Wireless host" — see `admin-services-dockerman.md`; per-page caps came back scoped: `[data-page="admin-system-mounts"]` (d276fd3, `admin-system-mounts.md`) and `[id*="status_leases"]` (80be54c, `admin-status-overview.md`)
See also: bbb355b 2025-10-26 "fix: resolve tooltip flicker and add footer spacing" — body: "added top margin to the footer to avoid overlap with main content in plugins that lack proper view containers (e.g., PassWall)"; a global `footer` rule (currently _layout.css line 644), not traced further here.
@@ -0,0 +1,23 @@
# admin-services-samba4
## samba4-cell-padding — tighter th/td padding inside the Network Shares form
- page: admin-services-samba4 · Services → Network Shares (title from luci-app-samba4 menu.d; 1dbd95a writes it as "NAS => Network Shares => Shared Directories")
- package: stock luci-app-samba4
- dom: `#cbi-samba4` (id of `form.Map('samba4')`, samba4.js:23) containing the `sambashare` TableSection "Shared Directories" (samba4.js:93); rule hits every `th/.th` and `td/.td` under it
- symptom: not recorded (1dbd95a subject: "Reduce cell padding in NAS => Network Shares => Shared Directories")
- cause: unknown (commit 1dbd95a has subject only; no symptom or measurement)
- fix: components/_table.css `#cbi-samba4 &` (nested under `& th, & .th`, line 114-116, and under `& td, & .td`, line 148-150) `px-0.5 py-1.5` · 1dbd95a 2025-10-17 · 0.5.14_beta (first tag v0.6.0_beta)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## samba4-section-overflow — horizontal scroll on the samba4 card
- page: admin-services-samba4 · Services → Network Shares (menu path inferred from the page name)
- package: stock luci-app-samba4
- dom: `#cbi-samba4 .cbi-section`
- symptom: not recorded (2e1d18a subject: "table overflow issue on samba4, package, and autoreboot pages")
- cause: unknown (commit 2e1d18a has subject only). The diff shows the section went from `overflow-x-auto` to `overflow-visible` globally and got `#cbi-samba4 & { @apply overflow-x-auto; }` back for this page only, alongside dropping `table-fixed` from `.table`
- fix: (historic, main.css `.cbi-section`) `#cbi-samba4 & { @apply overflow-x-auto; }` · 2e1d18a 2025-08-31 · 0.1.3_alpha (first tag v0.2.0-alpha)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: n/a · status: removed 6b9b328 (2025-09-03 "feat: make dropdown fill based on content, simplify table" — `.cbi-section` went back to global `overflow-x-auto`, so the per-page override was deleted with it)
@@ -0,0 +1,12 @@
# admin-statistics-graphs
## rrd-graphs-dark — invert and hue-rotate the rendered graph images in dark mode
- page: admin-statistics-graphs · Statistics → Graphs (menu.d title "Graphs"; edf378d: "Affected pages: Statistics => Graphs")
- package: stock luci-app-statistics
- dom: `[data-plugin] img` — the per-plugin tab containers built in graphs.js:75 (`'data-plugin': plugin`) holding the rrdtool PNGs; dark scope is `[data-darkmode="true"]` on `<html>`
- symptom: not recorded beyond "Adapt img background color for dark mode" (edf378d body)
- cause: edf378d body: "Adapt img background color for dark mode" — the graphs are server-rendered images whose light background does not follow the theme; nothing further recorded
- fix: patches/admin-statistics-graphs.css line 8-10 `[data-darkmode="true"] [data-page="admin-statistics-graphs"] [data-plugin] img { filter: hue-rotate(150deg) invert(100%); }` · edf378d 2025-10-17 · 0.5.16_beta (first tag v0.6.0_beta) — written as `@layer luci-app-statistics { [data-page=…] [data-plugin] img { @apply dark:hue-rotate-150 dark:invert } }` in main.css; `_plugins.css` after aedf3dc, merged into `_patches.css` by 7d9f6a1, own file via 1c924cf, native via e14cfcd (the e14cfcd body cites this file: "the two-rule statistics patch alone went 1,609 B → 110 B")
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
@@ -0,0 +1,12 @@
# admin-status-logs
## log-viewer — parse the read-only `#syslog` textarea into a column-aligned, colour-coded view
- page: admin-status-logs (prefix; covers `admin-status-logs-syslog`, `admin-status-logs-dmesg` and the bare `/logs` alias) · Status → System Log / Kernel Log (menu.d: "admin/status/logs" "System Log" alias → "…/syslog" "System Log", "…/dmesg" "Kernel Log")
- package: stock luci-mod-status
- dom: `textarea#syslog` inside `#content_syslog`; the patch inserts `div.syslog-view` (rows `.log-row.lv-*` with `.log-time/.log-sev/.log-tag/.log-msg`) after the textarea and adds `.log-enhanced` to the container. Styling lives in components/_syslog.css in main.css, not in a patch
- symptom: none — this is an enhancement, not a breakage fix (a5b44da: "The first payload reworks the System/Kernel Log pages")
- cause: a5b44da body: "the read-only #syslog textarea stays in place as LuCI's polled data source while a dirty-checked viewer parses each line (classic logread and the bracketed RPC format, whitelist-gated) into aligned time/level/source columns with per-severity colors, whole lines scrolling sideways, and a flash-free takeover via an insertion observer. Unknown formats trip a parse gate and the stock textarea simply shows again. One admin-status-logs name covers both tabs on every supported release — 23.05, 24.10 and master all mount the pages under admin/status/logs/*." Why the CSS is not a patch: "The viewer's CSS is core-page styling, not third-party compat, so it lives in components/_syslog.css inside main.css where the full utility pipeline applies"
- fix: src/resource/patches/admin-status-logs.js (whole file; router lifecycle `window.luciPatches["admin-status-logs"] = { mount, unmount }` at line 292-295, `mount()` at line 296) · a5b44da 2026-07-27 · 1.1.7 (first tag v1.2.0). Follow-ups: ee16953 2026-07-29 (1.1.9) "contain select-all, follow the tail, leave dmesg unsplit" — Ctrl+A grabbed the whole page, tail-follow when pinned to the bottom, dmesg rendered verbatim because "kernel lines have no uniform tag grammar"; e681ecb 2026-07-29 (1.1.10) "render every log token verbatim, copies byte-identical" — timestamps/colons were being rewritten, now "joining the rendered tokens with single spaces reproduces the source line byte for byte, and a test pins that round-trip"; 664c795 2026-08-16 (1.2.1) and e748f7c 2026-08-26 (1.2.6) wired the mount/unmount lifecycle for the same-document router (subjects only read)
- verified: a5b44da: "Node tests pin the parser against each format family and the payload's home" (`.dev/tests/log-patch.test.js`); e681ecb: round-trip test. No rendering measurement recorded
- fixture: none (backfill; DOM snapshot exists in `.dev/mocks/admin-status-logs-syslog.html`)
- issue: — · mirror: pending · status: active
@@ -0,0 +1,34 @@
# admin-status-overview
## leases-td-max-width — cap DHCP lease cells so DUID/IAID columns cannot widen the table
- page: admin-status-overview · Status → Overview, "Active DHCP Leases" / "Active DHCPv6 Leases" tables (luci-mod-status 40_dhcp.js:110 `#status_leases`, :169 `#status_leases6`; `[id*=]` matches both). The commit subject says "DHCP > Leases"; the ids exist only in this status include
- package: stock luci-mod-status
- dom: `.table[id*="status_leases"] .td` (class `table leases` / `table leases6`, div-based `.td` cells)
- symptom: table overflow from the DUID/IAID columns (80be54c subject)
- cause: unknown (commit 80be54c has subject only; no DOM or widths recorded)
- fix: components/_table.css `&[id*="status_leases"] { & .td { … } }` (line 137-141) `max-w-62 max-md:max-w-full` · 80be54c 2025-12-12 · 0.9.1_beta (first tag v0.10.0)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## section-actions-mobile — action-button cell spans the mobile card, buttons share the row
- page: admin-status-overview · Status → Overview → Active DHCP Leases; also System → Mount Points and Network → Interfaces / Wireless / Firewall (all five listed in e86b70f)
- package: stock luci-mod-status / luci-mod-system / luci-mod-network (per the pages listed)
- dom: `.table td.cbi-section-actions` (`> div` wrapper, `> div > .cbi-button`) and `.table td > .cbi-button`
- symptom: not recorded (e86b70f subject: "cbi-button display on mobile for cell view")
- cause: unknown (commit e86b70f has subject only)
- fix: components/_table.css `&.cbi-section-actions` nested under `& td, & .td` (line 190-200) `max-md:flex-[0_0_100%]`, `& > div` `max-md:w-full`, `& > div > .cbi-button` `max-md:flex-1`; plus `& > .cbi-button` (line 202-204) `max-md:w-full` · e86b70f 2025-10-17 · 0.5.15_beta (first tag v0.6.0_beta)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
## section-actions-mobile-v0 — original scaffold rule for the actions cell on mobile
- page: unknown (076281f is the initial scaffold commit; no page recorded)
- package: unknown
- dom: `.table td.cbi-section-actions` (`& div`, `& .btn, & .cbi-button`) and a sibling `.td.cbi-section-actions { p-2 max-md:p-1 … }` block in the button layer
- symptom: not recorded
- cause: unknown (076281f "chore: setup luci-theme-aurora with Vite and TailwindCSS" has subject only)
- fix: (historic, main.css) `&.cbi-section-actions { @apply max-md:mt-3 max-md:border-t max-md:pt-3; & div { max-md:flex max-md:flex-wrap max-md:justify-start max-md:gap-2 } & .btn, & .cbi-button { max-md:min-w-0 max-md:flex-1 … } }` · 076281f 2025-08-25 · 0.1.0 (first tag v0.2.0-alpha)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: n/a · status: removed 386dec5 (2025-08-30 "feat: optimize mobile display for table" rewrote the mobile table block and dropped it; the `.td.cbi-section-actions` button-layer block was dropped by 4c99ac8 2025-09-05 "chore: adjust button nesting style structure", which replaced it with the generic `.cbi-section-actions > div { flex flex-row … }`). e86b70f re-added a different rule seven weeks later — see the entry above
@@ -0,0 +1,12 @@
# admin-system-diskman
## diskman-partition-bar — keep the injected partition bar inside the card on small screens, clip segment labels
- page: admin-system-diskman (prefix; also `-partition-<dev>`, `-btrfs-<uuid>`) · System → Disk Man (controller diskman.lua:24 entry title "Disk Man", alias to `disks`; issue #58 calls it the storage area on the home page)
- package: luci-app-diskman (lisaac; the ImmortalWrt feed copy was checked)
- dom: `td[colspan]:has(> div[title])` and `td[colspan] > div[title]`, `.tr > div[style*="nowrap"] > div[title]` — disk_info.htm:34/88 emit `<tr width="100%" style="white-space:nowrap;"><td style="… white-space:nowrap;" colspan="15">` and :36 `<div class="tr cbi-section-table-row …"><div style="white-space:nowrap; position:absolute; width:100%">`, with one inline-block `div[title]` per partition sized in percent
- symptom: issue #58: "首页的存储区域会超出宽度,导致页面可以左右滑动" (the storage area exceeds the width; page scrolls sideways), Safari 26 / iOS 26, OpenWrt 24.10.5, theme 0.11; 49fff90 body: "drags the page ~300px wide"
- cause: 49fff90 body: "luci-app-diskman injects the partition bar as a full-span row whose cell carries an inline white-space:nowrap. Once .tr becomes a flex row on mobile, that cell's automatic minimum size is its min-content — the whole unwrapped bar — so it cannot shrink and drags the page ~300px wide (reported in #58 with the page DOM). … clip each segment so its label stops spilling onto the neighbouring one (visible on desktop too)". The file adds: "The segments are inline-blocks sized in percent: any whitespace between them adds a word space on top of the 100% they already sum to" (hence `font-size: 0` on the cell)
- fix: patches/admin-system-diskman.css line 9-35 `[data-page^="admin-system-diskman"] { td[colspan]:has(> div[title]) { flex: 0 0 100%; min-width: 0; overflow: hidden; border-radius: var(--radius-base); font-size: 0 } td[colspan] > div[title], .tr > div[style*="nowrap"] > div[title] { overflow: hidden; font-size: 0.75rem; line-height: 1.5rem; text-overflow: ellipsis } }` · 49fff90 2026-08-02 · 1.1.13 (first tag v1.2.0)
- verified: not measured (the body states the before width, "~300px wide", from the reporter's DOM; no after-number recorded)
- fixture: none (backfill; DOM snapshot exists in `.dev/mocks/admin-system-diskman.html`)
- issue: #58 · mirror: pending · status: active
@@ -0,0 +1,12 @@
# admin-system-filemanager
## filemanager-container-scroll — scrollable file-manager container, flat status bar
- page: admin-system-filemanager · System → File Manager (menu.d title "File Manager")
- package: luci-app-filemanager (in openwrt/luci applications)
- dom: `#file-manager-container` (the app's own `<style>` block targets it, filemanager.js:269-272) and its `#status-bar`
- symptom: not recorded (347d645 subject: "feat: optimize mobile h3 display and add layer plugins")
- cause: unknown (commit 347d645 has subject only)
- fix: patches/admin-system-filemanager.css line 8-17 `[data-page="admin-system-filemanager"] { #file-manager-container { overflow: auto; #status-bar { background: var(--surface); border: 0; } } }` · 347d645 2025-09-12 · 0.2.1_alpha (first tag v0.3.0-alpha) — first written unscoped as `@layer plugins > luci-app-filemanager { #file-manager-container { @apply overflow-auto; #status-bar { @apply border-0 dark:bg-slate-900 } } }` (applied on every page); `_plugins.css` after aedf3dc, merged into `_patches.css` by 7d9f6a1; 1c924cf scoped it to the page (PR #82: "Backfilled missing data-page scoping … filemanager"); native via e14cfcd (the dark-only `bg-slate-900` became an always-on `var(--surface)`)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
@@ -0,0 +1,14 @@
# admin-system-mounts
## mounts-td-max-width — cap cells on Mount Points so a long mount path cannot widen the table
- page: admin-system-mounts · System → Mount Points → "Mounted file systems" (menu.d title "Mount Points"; d276fd3: "Tests: System -> Mount Points -> Mounted file systems")
- package: stock luci-mod-system (page depends on block-mount)
- dom: every `td/.td` under `.table` on the page. The "Mounted file systems" table is a plain `E('table', { class: 'table' })` (mounts.js:168) with no id, so the page scope is the only handle; the rule also reaches the `mount`/`swap` GridSections
- symptom: a long docker directory path breaks the Mount Points layout (issue #24: "docker目录比较长,mounts下显示问题", Chrome 142, OpenWrt 24.10, screenshot only)
- cause: d276fd3 body: "the fix avoids applying a global maximum width to table data cells (`td`) to prevent regressions in CBI section actions (e.g., in Services -> passwall -> Node List)" — i.e. the cap is page-scoped because the earlier global `max-w-94` on td was removed in 50c2b84 for passwall (see `admin-services-passwall.md`). Why the path overflowed rather than wrapped is not recorded
- fix: components/_table.css `[data-page="admin-system-mounts"] &` nested under `& td, & .td` (line 152-154) `max-w-104 max-md:max-w-full` · d276fd3 2025-11-28 · 0.8.6_beta (first tag v0.9.0_beta)
- verified: d276fd3 says "Tests: System -> Mount Points -> Mounted file systems"; no numbers. a2d110a (2026-09-13) notes: "On admin/system/mounts the GridSection value cells now take the 65ch cap instead of the page's 26rem one (same specificity, later rule). That only shifts width distribution and was not measured: the test device has no block-mount, so the page is absent."
- fixture: none (backfill)
- issue: #24 · mirror: pending · status: active
See also: 50c2b84 lists "System => (Software, Mount Points): Fix overflow when td text is too long" for `.td.cbi-value-field { break-all }` — recorded in `admin-services-dockerman.md`. e86b70f lists "System => Mount Points" for the `.cbi-section-actions` mobile rule — recorded in `admin-status-overview.md`.
@@ -0,0 +1,17 @@
# admin-system-package-manager
## progress-title-10px — shrink the progress bar's title text below the sm breakpoint
- page: admin-system-package-manager · System → Software (menu.d title "Software")
- package: stock luci-app-package-manager
- dom: `.cbi-progressbar[title]` (package-manager.js:1253 `E('div', { class: 'cbi-progressbar', title })`); the theme prints the title through `before:content-[attr(title)]` with `before:whitespace-nowrap` (_progress.css line 2)
- symptom: progress text clipping on the package page (cdcc680 subject)
- cause: unknown (commit cdcc680 has subject only). What the diff shows: the title is a nowrap ::before centred in the bar; the fix only lowers its font size under the sm breakpoint. 386dec5 (2025-08-30) had earlier given every progressbar `max-md:before:text-[10px]`; when that left is not traced here
- fix: components/_progress.css `[data-page="admin-system-package-manager"] &` nested under `.cbi-progressbar` (line 3-5) `max-sm:before:text-[10px]` · cdcc680 2025-12-05 · 0.8.10_beta (first tag v0.9.0_beta)
- verified: not measured (no record)
- fixture: none (backfill)
- issue: — · mirror: pending · status: active
See also (global rules motivated by this page, not page-scoped):
- 2e1d18a 2025-08-31 "fix: table overflow issue on samba4, package, and autoreboot pages" — dropped `table-fixed` from `.table` globally (see `admin-services-samba4.md`).
- 50c2b84 2025-10-12 "System => (Software, Mount Points): Fix overflow when td text is too long" — `.td.cbi-value-field { break-all }`, now `wrap-anywhere md:max-w-prose` (see `admin-services-dockerman.md`).
- edf378d 2025-10-17 "Adjust .controls button layout — Affected pages: Statistics => Graphs, System => Software (filter form and paginator)" — `.controls { m-2 flex flex-wrap items-center gap-3 … }` in main.css at the time; current location not traced here.
+1 -1
View File
@@ -26,5 +26,5 @@
"terser": "^5.43.1",
"vite": "^7.1.1"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c"
}
+158
View File
@@ -1,3 +1,161 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.4.1
version: 12.4.1
packages:
'@pnpm/exe.android-arm64@12.4.1':
resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.4.1':
resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.4.1':
resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.4.1':
resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.freebsd-x64@12.4.1':
resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.4.1':
resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.4.1':
resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-ppc64@12.4.1':
resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.4.1':
resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.4.1':
resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.4.1':
resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.4.1':
resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.4.1':
resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.4.1':
resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==}
cpu: [x64]
os: [win32]
pnpm@12.4.1:
resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.android-arm64@12.4.1':
optional: true
'@pnpm/exe.android-x64@12.4.1':
optional: true
'@pnpm/exe.darwin-arm64@12.4.1':
optional: true
'@pnpm/exe.darwin-x64@12.4.1':
optional: true
'@pnpm/exe.freebsd-x64@12.4.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.4.1':
optional: true
'@pnpm/exe.linux-arm64@12.4.1':
optional: true
'@pnpm/exe.linux-ppc64@12.4.1':
optional: true
'@pnpm/exe.linux-riscv64@12.4.1':
optional: true
'@pnpm/exe.linux-s390x@12.4.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.4.1':
optional: true
'@pnpm/exe.linux-x64@12.4.1':
optional: true
'@pnpm/exe.win32-arm64@12.4.1':
optional: true
'@pnpm/exe.win32-x64@12.4.1':
optional: true
pnpm@12.4.1:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.4.1
'@pnpm/exe.android-x64': 12.4.1
'@pnpm/exe.darwin-arm64': 12.4.1
'@pnpm/exe.darwin-x64': 12.4.1
'@pnpm/exe.freebsd-x64': 12.4.1
'@pnpm/exe.linux-arm64': 12.4.1
'@pnpm/exe.linux-arm64-musl': 12.4.1
'@pnpm/exe.linux-ppc64': 12.4.1
'@pnpm/exe.linux-riscv64': 12.4.1
'@pnpm/exe.linux-s390x': 12.4.1
'@pnpm/exe.linux-x64': 12.4.1
'@pnpm/exe.linux-x64-musl': 12.4.1
'@pnpm/exe.win32-arm64': 12.4.1
'@pnpm/exe.win32-x64': 12.4.1
---
lockfileVersion: '9.0'
settings:
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
@@ -1,6 +1,6 @@
table.table,
.table {
@apply border-hairline mb-3 w-full border border-separate border-spacing-0 overflow-visible rounded-2xl max-md:mx-0 max-md:block max-md:w-full max-md:overflow-visible max-md:rounded-none max-md:border-0 max-md:shadow-none;
@apply border-hairline mb-3 w-full border border-separate border-spacing-0 overflow-visible rounded-2xl text-base max-md:mx-0 max-md:block max-md:w-full max-md:overflow-visible max-md:rounded-none max-md:border-0 max-md:text-sm max-md:shadow-none;
&[width="100%"] {
@apply w-full;
@@ -142,7 +142,7 @@ table.table,
& td,
& .td {
@apply border-hairline table-cell border-b px-3 py-3 align-middle text-base wrap-break-word whitespace-normal max-md:flex-[1_1_50%] max-md:border-0 max-md:px-1 max-md:py-1 max-md:text-sm;
@apply border-hairline table-cell border-b px-3 py-3 align-middle wrap-break-word whitespace-normal max-md:flex-[1_1_50%] max-md:border-0 max-md:px-1 max-md:py-1;
@apply before:hidden before:content-[attr(data-title)] before:max-md:mb-1 before:max-md:block before:max-md:text-xs before:max-md:font-semibold before:max-md:tracking-wider;
#cbi-samba4 & {
+187
View File
@@ -0,0 +1,187 @@
// Geometry checks for the DOM compat fixtures (see ../compat/README.md).
// Each fixture is a trimmed page that links the built main.css and declares
// its checks in <script type="application/json" id="compat">. They run in
// headless Chrome over raw CDP — no npm dependency, same approach as the
// performance skill's bench scripts.
import assert from "node:assert/strict";
import { spawn, execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import { setTimeout as sleep } from "node:timers/promises";
const fixtureDir = resolve(import.meta.dirname, "../compat/fixtures");
const output = resolve(import.meta.dirname, "../../htdocs/luci-static/aurora");
// header.ut inlines fonts/aurora-font.css; its absolute /luci-static/ URL
// cannot resolve under file://, so the runner injects it with a file URL.
// Without the theme font the fallback metrics differ enough to flip a wrap.
const fontCss = existsSync(join(output, "fonts/aurora-font.css"))
? readFileSync(join(output, "fonts/aurora-font.css"), "utf8")
.replaceAll("/luci-static/aurora/", pathToFileURL(output).href + "/")
: "";
const fixtures = existsSync(fixtureDir)
? readdirSync(fixtureDir).filter((f) => f.endsWith(".html")).sort()
: [];
function findChrome() {
const candidates = [
process.env.CHROME_BIN,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
].filter(Boolean);
for (const c of candidates) if (existsSync(c)) return c;
for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]) {
try {
return execFileSync("which", [name], { encoding: "utf8" }).trim() || null;
} catch {}
}
return null;
}
const chromeBin = findChrome();
const skipReason =
fixtures.length === 0
? "no fixtures in compat/fixtures"
: chromeBin
? null
: "no Chrome found (set CHROME_BIN); required when CI is set";
if (skipReason && process.env.CI && fixtures.length > 0) {
test("compat fixtures need a browser on CI", () => assert.fail(skipReason));
}
// The checks run inside the page; `sel` is the selector from the fixture.
const CHECKS = {
// Every matched element's text sits on one line, and no text starts below
// an inline checkbox/indicator inside it (the "label under the icon" wrap).
nowrap: (sel) => `(() => {
const bad = [];
for (const el of document.querySelectorAll(${JSON.stringify(sel)})) {
const tops = [];
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let n; (n = w.nextNode());) {
if (!n.textContent.trim()) continue;
const r = document.createRange(); r.selectNodeContents(n);
for (const x of r.getClientRects()) if (x.width > 0) tops.push(x.top);
}
const ind = el.querySelector('input, .partition-color-indicator, img, svg');
const wrapped = tops.some((t) => Math.abs(t - tops[0]) > 3) ||
(ind && tops.length && tops[0] >= ind.getBoundingClientRect().bottom - 1);
if (wrapped) bad.push(el.textContent.trim().slice(0, 40));
}
return bad;
})()`,
// No horizontal overflow inside, and the box stays inside its parent.
fits: (sel) => `(() => {
const bad = [];
for (const el of document.querySelectorAll(${JSON.stringify(sel)})) {
const r = el.getBoundingClientRect(), p = el.parentElement.getBoundingClientRect();
if (el.scrollWidth > el.clientWidth + 1 || r.right > p.right + 1)
bad.push((el.id || el.className || el.tagName) + ' ' + Math.round(el.scrollWidth) + '/' + Math.round(el.clientWidth) + ' right ' + Math.round(r.right) + '>' + Math.round(p.right));
}
return bad;
})()`,
js: (expr) => `(() => { const v = (${expr}); return v ? [] : ['expression was ' + JSON.stringify(v)]; })()`,
};
function readSpec(file) {
const html = readFileSync(join(fixtureDir, file), "utf8");
const m = html.match(/<script[^>]*id="compat"[^>]*>([\s\S]*?)<\/script>/);
assert.ok(m, `${file}: missing <script id="compat"> block`);
const spec = JSON.parse(m[1]);
assert.ok(Array.isArray(spec.checks) && spec.checks.length, `${file}: no checks`);
return spec;
}
test("compat fixtures keep their geometry", { skip: skipReason ?? false }, async (t) => {
const profile = mkdtempSync(join(tmpdir(), "cdp-compat-"));
const chrome = spawn(
chromeBin,
["--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`,
"--no-first-run", "--no-default-browser-check", "--allow-file-access-from-files", "about:blank"],
{ stdio: "ignore" },
);
let port = null;
for (let i = 0; i < 100 && !port; i++) {
await sleep(100);
const f = join(profile, "DevToolsActivePort");
if (existsSync(f)) port = +readFileSync(f, "utf8").split("\n")[0];
}
assert.ok(port, "chrome did not expose a DevTools port");
const { webSocketDebuggerUrl } = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
const ws = new WebSocket(webSocketDebuggerUrl);
await new Promise((res, rej) => {
ws.addEventListener("open", res, { once: true });
ws.addEventListener("error", () => rej(new Error("CDP connect failed")), { once: true });
});
let mid = 0;
const pending = new Map();
const handlers = new Set();
ws.addEventListener("message", (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) {
const { res, rej } = pending.get(m.id);
pending.delete(m.id);
m.error ? rej(new Error(m.error.message)) : res(m.result);
} else for (const h of handlers) h(m);
});
const send = (method, params = {}, sessionId) =>
new Promise((res, rej) => {
const id = ++mid;
pending.set(id, { res, rej });
ws.send(JSON.stringify({ id, method, params, sessionId }));
});
try {
for (const file of fixtures) {
const spec = readSpec(file);
const { targetId } = await send("Target.createTarget", { url: "about:blank" });
const { sessionId } = await send("Target.attachToTarget", { targetId, flatten: true });
await send("Page.enable", {}, sessionId);
if (fontCss)
await send("Page.addScriptToEvaluateOnNewDocument", { source: `
document.addEventListener('DOMContentLoaded', () => {
const s = document.createElement('style');
s.textContent = ${JSON.stringify(fontCss)};
document.head.prepend(s);
});` }, sessionId);
const evaljs = async (expression) => {
const r = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, sessionId);
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text);
return r.result.value;
};
for (const width of spec.viewports ?? [1280]) {
await send("Emulation.setDeviceMetricsOverride",
{ width, height: 900, deviceScaleFactor: 1, mobile: width < 768 }, sessionId);
const loaded = new Promise((res) => {
const h = (m) => { if (m.method === "Page.loadEventFired" && m.sessionId === sessionId) { handlers.delete(h); res(); } };
handlers.add(h);
});
await send("Page.navigate", { url: pathToFileURL(join(fixtureDir, file)).href }, sessionId);
await loaded;
// Force the theme font to load before measuring (font-display: swap
// would otherwise measure the fallback), then give layout one frame.
await evaljs(`document.fonts.load('12px "Lato"').then(() => document.fonts.ready)
.then(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))))`);
const fontOk = await evaljs(`!${JSON.stringify(!!fontCss)} || document.fonts.check('12px "Lato"')`);
assert.ok(fontOk, `${file}: theme font did not load from ${output}/fonts`);
const sheets = await evaljs("[...document.styleSheets].map(s => { try { return s.cssRules.length } catch { return -1 } })");
assert.ok(sheets.some((n) => n > 100), `${file}: main.css did not load (rules per sheet: ${sheets}) — run pnpm build`);
for (const check of spec.checks) {
if (check.viewports && !check.viewports.includes(width)) continue;
const [kind, arg] = Object.entries(check).find(([k]) => k in CHECKS) ?? [];
assert.ok(kind, `${file}: unknown check ${JSON.stringify(check)}`);
const bad = await evaljs(CHECKS[kind](arg));
assert.deepEqual(bad, [], `${file} @${width} ${kind} ${JSON.stringify(arg)} (ledger ${spec.ledger}): ${bad.join("; ")}`);
}
}
await send("Target.closeTarget", { targetId });
}
} finally {
ws.close();
chrome.kill();
await sleep(200);
rmSync(profile, { recursive: true, force: true });
}
});
@@ -172,7 +172,14 @@ test("content dropdowns stay above the closed header and below the open mega-men
const overlayRule =
overlay.match(/& \.desktop-menu-overlay\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
assert.ok(messageRule.includes(layer(30)), `message layer changed: ${messageRule}`);
// The alert banner stays in flow (d747e5f): ui.addNotification() inserts
// banners as siblings, so a sticky one would stack on top of the next and
// pin opaque surface over scroll targets. In flow it needs no layer at all.
assert.ok(messageRule, "alert-message rule not found");
assert.ok(
!/\b(sticky|fixed)\b/.test(messageRule) && !/\bz-/.test(messageRule),
`alert banner must stay in flow with no layer: ${messageRule}`,
);
assert.ok(headerRule.includes(layer(40)), `closed header layer changed: ${headerRule}`);
assert.ok(dropdownRule.includes(layer(50)), `dropdown layer changed: ${dropdownRule}`);
assert.ok(overlayRule.includes(layer(60)), `menu overlay layer changed: ${overlayRule}`);
+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.3.2
PKG_RELEASE:=79
PKG_VERSION:=1.3.3
PKG_RELEASE:=80
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--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-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-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)}}@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-button{margin-inline-end:4px}::placeholder{opacity:1Line 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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--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-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-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)}}@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-button{margin-inline-end:4px}::placeholder{opacity:1Line truncated