mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 09:54:06 +08:00
🍕 Sync 2026-09-14 23:30:37
This commit is contained in:
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:
|
||||
|
||||
@@ -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.
|
||||
+30
@@ -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>
|
||||
+47
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
Generated
+158
@@ -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 & {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user