mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-02 07:29:28 +08:00
🍓 Sync 2026-07-27 08:34:56
This commit is contained in:
parent
bf03fd23d8
commit
076b725e5a
@ -113,7 +113,7 @@ Third-party compatibility patches are **not** bundled into `main.css` — they a
|
||||
- New UI component → create `components/_<name>.css` and add an `@import` line to `main.css`. Each file is its own organizational unit — don't add `@layer` wrappers: theme partials stay unlayered, so they outrank Tailwind's layered base/utilities regardless of specificity.
|
||||
- Compatibility fix for a third-party LuCI app/page → add a new file under `media/patches/` (see below).
|
||||
|
||||
All rules use `@apply` with Tailwind utilities and CSS Nesting — no raw CSS properties.
|
||||
All rules use `@apply` with Tailwind utilities and CSS Nesting — no raw CSS properties. The one deliberate exception is `media/patches/*.css`, which is plain native CSS by design (see below).
|
||||
|
||||
### On-Demand Third-Party Patches
|
||||
|
||||
@ -123,9 +123,23 @@ Some third-party LuCI apps ship markup that doesn't adapt to the theme and needs
|
||||
|
||||
1. **One file per page, named by `data-page`.** Each patch lives at `media/patches/<page>.css`, where `<page>` is the value of `<body data-page="…">` for the target page — i.e. the request path segments joined by `-` (e.g. `admin-services-openclash-config`). `header.ut` computes the same string at render time from `ctx.request_path`, falling back to `ctx.path` when `request_path` is empty (`join('-', length(ctx.request_path) ? ctx.request_path : ctx.path)`) so default landings reached without an explicit path still resolve their patch.
|
||||
2. **Build splits, not bundles.** `vite.config.ts` adds every `media/patches/*.css` as its own Rollup entry, so each compiles to `htdocs/luci-static/aurora/patches/<page>.css`. They are no longer part of `main.css`.
|
||||
3. **`@reference`, not `@import`.** Every patch file starts with `@reference "../main.css";`. This loads the theme context (tokens, custom utilities like `bg-surface`, the `dark:` variant) so `@apply` resolves — **without** re-emitting `main.css` into the patch output. Using `@import` here would inline all of `main.css` into every patch (hundreds of KB); `@reference` keeps each patch to just its own rules.
|
||||
3. **Plain CSS, not `@apply`.** Patches are the one place that writes native declarations instead of Tailwind utilities. Each patch is its own build entry, and the old `@reference "../main.css";` + `@apply` setup made every file carry its own `@property`/helper boilerplate. Rewriting the *same rules* natively — measured on the v1.1.7 → v1.1.8 conversion, built output, identical selectors and values:
|
||||
|
||||
| patch | `@reference` + `@apply` | native CSS | saved |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| admin-dashboard | 6,940 B | 2,639 B | −62% |
|
||||
| admin-modem-modemdata-modempreview | 162 B | 96 B | −41% |
|
||||
| admin-modem-qmodem | 9,483 B | 4,497 B | −53% |
|
||||
| admin-network-network | 160 B | 86 B | −46% |
|
||||
| admin-services-openclash-config | 187 B | 87 B | −53% |
|
||||
| admin-services-openclash-settings | 638 B | 509 B | −20% |
|
||||
| admin-statistics-graphs | 1,609 B | 110 B | −93% |
|
||||
| admin-system-filemanager | 576 B | 184 B | −68% |
|
||||
| **total** | **19,755 B** | **8,208 B** | **−58%** |
|
||||
|
||||
The overhead is per-entry and fixed-ish (`@property` registrations, `--tw-*` helper chains), so the smaller the patch, the worse the ratio — the two-rule statistics patch paid 15× its own weight. uhttpd serves identity bytes (no gzip), so raw size is wire size. Note the runtime `:root` exposes the raw color tokens plus `--radius-base` and `--app-shadow-*` — the `--radius-3xl`-style names only exist inside the Tailwind build, so a patch writes `calc(var(--radius-base) * 3)` for radii, never `var(--radius-3xl)`. The old Tailwind mode is still *supported* for theme-repo patches — a file starting with `@reference "../main.css";` and using `@apply` continues to compile — and it has real upsides worth weighing against the table above: **build-time validation** (a typo'd utility or color name fails the build, while a typo'd `var()` fails silently at runtime — exactly how the `--radius-3xl` regression slipped into the native rewrite), the **same vocabulary** as the component partials with the `dark:`/`md:`/`hover:` variant shorthands, and **automatic tracking** when the theme remaps a utility (a radius-chain or shadow-token change recompiles into Tailwind patches for free; native patches need a hand edit). Native is the default convention for the size numbers, not a hard gate. App-shipped patches (installed straight into the device's `patches/` directory) have never had a choice: they bypass the build entirely, so `@apply` would reach the browser as dead text — plain CSS only, as always. It also means third-party patch authors need no Tailwind knowledge. Reach theme values through the `:root` custom properties (`var(--surface-sunken)`, `var(--hairline)`, `var(--radius-3xl)`, `var(--shadow-lg)`, …) rather than hardcoding colors or radii; the dark variant is a plain selector (`[data-darkmode="true"] & { … }`), breakpoints are plain media queries (`@media (width < 48rem)`), and CSS Nesting still works — the build (lightningcss) minifies and lowers it for the supported browsers.
|
||||
4. **`header.ut` discovers patches at render time.** On each (non-login) page render, `header.ut` lists `/www/luci-static/aurora/patches/` with ucode's `fs.lsdir()` (a readdir of a dozen entries — microseconds, dwarfed by the template's existing `ubus` call) and matches the installed `*.css` filenames against the **cumulative path-segment prefixes** of the request: a patch matches its exact page and any subpage, but only on real segment boundaries — `admin-services-wol.css` covers `admin/services/wol/plus`, yet never a sibling app whose own segment merely starts the same way (`admin/services/wol-plus`). Every matching patch is linked right after `main.css`, in lexical order — so a general patch loads before a more specific one and the specific one cascades on top. Pages with no match get nothing — no extra request, no 404. If the directory is missing or unreadable, the list is empty and the page renders unpatched.
|
||||
5. **The patches directory is a drop-in extension point.** Because discovery is at render time, patches don't have to ship with the theme: **any package may install a `<page-prefix>.css` into `/www/luci-static/aurora/patches/`** and the theme will load it on matching pages. Install/uninstall lifecycle is automatic — the file appears and disappears with the package, no registration or allow-list rebuild. (Patches shipped this way are plain CSS served as-is; the theme's own patches additionally go through the Tailwind build below.)
|
||||
5. **The patches directory is a drop-in extension point.** Because discovery is at render time, patches don't have to ship with the theme: **any package may install a `<page-prefix>.css` into `/www/luci-static/aurora/patches/`** and the theme will load it on matching pages. Install/uninstall lifecycle is automatic — the file appears and disappears with the package, no registration or allow-list rebuild. (Patches shipped this way are plain CSS served as-is; the theme's own patches are written the same way and only pass through the build for minification.)
|
||||
6. **Dynamically generated pages are covered by their fixed prefix.** Some apps mint a page per entity — e.g. QModem's SMS conversations render as `admin-modem-qmodem-sms-conversation-<contact>`. Name the patch after the fixed prefix (`admin-modem-qmodem-sms-conversation.css`) and the prefix match loads it for every conversation page, regardless of the contact name. No wildcard syntax is needed (and `*` in a filename is not supported).
|
||||
|
||||
**Adding a patch:**
|
||||
@ -134,14 +148,14 @@ Some third-party LuCI apps ship markup that doesn't adapt to the theme and needs
|
||||
2. Create `media/patches/<that-string>.css`:
|
||||
```css
|
||||
/* PATCH: <page> (luci-app-foo) */
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="<page>"] {
|
||||
/* narrow, selector-scoped overrides using @apply + CSS Nesting */
|
||||
/* narrow, selector-scoped overrides — native CSS + CSS Nesting,
|
||||
theme values via var(--surface), var(--hairline), … */
|
||||
}
|
||||
```
|
||||
3. Run `pnpm build`. There is no allow-list to regenerate — the loader discovers whatever `.css` files are installed under `patches/` at render time.
|
||||
4. Verify `htdocs/luci-static/aurora/patches/<page>.css` is small (just your rules, not a copy of `main.css`).
|
||||
4. Verify `htdocs/luci-static/aurora/patches/<page>.css` is small (just your rules).
|
||||
|
||||
> Removing a patch is symmetric: delete the file and rebuild — the loader stops linking it because it no longer exists.
|
||||
|
||||
@ -171,7 +185,11 @@ Two rules of thumb that follow from prefix matching:
|
||||
- **A patch applies to its page and all subpages by default.** `admin-services-foo.css` loads on every page under `admin/services/foo/…`. When you need finer targeting, narrow it in either of two ways: scope individual rules inside the file (`[data-page="admin-services-foo-general"] { … }` only affects that one page), or ship an additional, longer-named file (`admin-services-foo-rules.css`) for page-specific rules — on a page matching both, **both load**, shorter name first, so the more specific file wins the cascade.
|
||||
- **Matching respects path-segment boundaries**, so a prefix never leaks onto a lookalike sibling: `admin-services-wol.css` covers `admin/services/wol/plus` but not a different app at `admin/services/wol-plus`. The one unavoidable collision is two paths joining to the same `data-page` string (`wol/plus` vs `wol-plus`) — such a patch loads on both pages. If that matters, key rules to your app's own class names/ids so an accidental load matches nothing.
|
||||
|
||||
> Unlike the `_`-prefixed partials (which are `@import`-only fragments), patch filenames have no `_` prefix — each is a real build entry that ships to `htdocs/`.
|
||||
> Unlike the `_`-prefixed partials (which are `@import`-only fragments), patch filenames have no `_` prefix — each is a real build entry that ships to `htdocs/`. This also holds inside `media/patches/` itself: a `_`-prefixed file there is a shared fragment for other patches to `@import`, skipped by the entry scan and never shipped.
|
||||
|
||||
**JS payloads.** The mechanism is not CSS-specific: the same `lsdir()` sweep also collects `patches/<page>.js` files, emitted as `<script defer src>` right after the patch stylesheet links — same per-page prefix matching, same drop-in lifecycle for third-party packages (ship a plain script; it must not assume LuCI modules are loadable via `L.require` at parse time — run after DOM ready or poll for your target element). Theme-owned JS patches live at `src/resource/patches/<page>.js` and build through the same Terser pass as other resource JS, but land under `aurora/patches/` so one directory listing serves both payload types. First user: the log-viewer enhancement on `admin-status-logs` (parses the read-only `#syslog` textarea into a colored, column-aligned view, with a parse-success gate that falls back to the stock textarea on unknown log formats; the one prefix covers the System Log tab, the Kernel Log tab and the bare `/logs` alias on every supported release). Its CSS is deliberately **not** a patch: the log pages are stock LuCI, so their styling lives in `components/_syslog.css` inside `main.css`, where the full Tailwind token pipeline (reactive radii, shadows, config-app overrides) applies — patches/ stays reserved for third-party compatibility.
|
||||
|
||||
**Aliases — one payload, several pages.** When two unrelated page names need the same payload, naming the file after their shared prefix would over-match (an `admin-status` patch would load on *every* status subpage, including the busiest overview page). For that case `PATCH_ALIASES` in `vite.config.ts` duplicates the built output (CSS and JS) under each alias name, and the dev server resolves alias requests to the shared source. Duplicate CSS *entries* would not work as an alternative: Rollup deduplicates identical-content assets into a single file, so one of the two names would silently never ship. (The map is currently empty — the log viewer turned out to need only `admin-status-logs`, since every supported release mounts both log pages under `admin/status/logs/*`.)
|
||||
|
||||
### Mock Pages
|
||||
|
||||
|
||||
@ -545,7 +545,7 @@ body[data-nav-type="sidebar"] {
|
||||
/* Centred shells (top-bar navigation) clamp here and gutter the remainder.
|
||||
The sidebar shell overrides this — it caps the shell instead, see
|
||||
body[data-nav-type="sidebar"] above. */
|
||||
@apply max-w-(--content-width-centered) mx-auto min-h-[calc(100vh-4rem)] w-23/24 px-4 max-md:w-full max-md:px-3;
|
||||
@apply mx-auto min-h-[calc(100vh-4rem)] w-23/24 max-w-(--content-width-centered) px-4 max-md:w-full max-md:px-3;
|
||||
|
||||
#view {
|
||||
@apply mx-0 w-full bg-transparent p-0 shadow-none empty:hidden md:p-0;
|
||||
@ -616,7 +616,9 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
#syslog {
|
||||
@apply bg-surface-sunken text-text rounded-3xl border p-6 shadow-lg max-md:p-3;
|
||||
/* Explicit border color: bare `border` falls back to currentColor
|
||||
(near-black text) in Tailwind v4. */
|
||||
@apply bg-surface-sunken border-hairline text-text rounded-3xl border p-6 shadow-lg max-md:p-3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
110
luci-theme-aurora/.dev/src/media/components/_syslog.css
Normal file
110
luci-theme-aurora/.dev/src/media/components/_syslog.css
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
Log viewer for the core System/Kernel Log pages (admin/status/logs/*).
|
||||
|
||||
This is theme styling for a stock LuCI page, so it lives in the main.css
|
||||
component pipeline — NOT in patches/ — and consumes the Tailwind token
|
||||
system: radii resolve through the --radius-base chain, shadows through
|
||||
--app-shadow-*, colors through the theme map, so config-app overrides of
|
||||
the base tokens reach this page like any other. (patches/ stays reserved
|
||||
for third-party compatibility, written in plain CSS.)
|
||||
|
||||
The class names are deliberately global, not page-scoped: any package may
|
||||
reuse the same markup contract for its own log UI — .syslog-view wrapping
|
||||
.log-row rows built from .log-time / .log-sev / .log-tag / .log-msg spans,
|
||||
with an lv-{info|notice|warn|err|crit|debug} severity class on the row —
|
||||
and inherit this component wholesale.
|
||||
|
||||
The companion JS ships on-demand as patches/admin-status-logs.js. It
|
||||
parses the read-only #syslog textarea into a .syslog-view sibling and
|
||||
marks #content_syslog with .log-enhanced. Without JS (or when its parse
|
||||
gate trips) none of the viewer markup exists and the stock textarea
|
||||
styling from _layout.css still applies.
|
||||
|
||||
Chosen design (user-picked variant "彩色级别词", Vercel/Axiom style):
|
||||
fixed-width time · level · source columns so messages start flush; the
|
||||
level is a short uppercase word colored per severity (facility lives in
|
||||
its tooltip), source neutral, err-and-above rows tinted, whole lines with
|
||||
sideways scrolling (the stock textarea's wrap="off" model). Fields are
|
||||
inline-blocks inside inline flow (not grid/flex) so the real spaces
|
||||
between them survive into copied text.
|
||||
*/
|
||||
|
||||
#content_syslog.log-enhanced > #syslog {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.syslog-view {
|
||||
/* Direct child of #content_syslog: undo the filter-row flex from
|
||||
_layout.css's `#content_syslog > div` rule. */
|
||||
@apply block w-full;
|
||||
/* Same shell as the stock textarea, with the border color made explicit:
|
||||
a bare `border` falls back to currentColor (near-black) in Tailwind v4. */
|
||||
@apply bg-surface-sunken border-hairline text-text rounded-3xl border p-6 shadow-lg max-md:p-3;
|
||||
@apply font-mono text-xs leading-relaxed;
|
||||
/* Whole lines, sideways scroll (the stock textarea's wrap="off" model). */
|
||||
@apply overflow-x-auto;
|
||||
|
||||
& .log-row {
|
||||
/* max-content width so hover/tint backgrounds span the scrolled width. */
|
||||
@apply w-max min-w-full rounded-md px-2 whitespace-pre;
|
||||
|
||||
&:hover {
|
||||
@apply bg-hover-faint;
|
||||
}
|
||||
}
|
||||
|
||||
/* The browser-default selection highlight turns the dimmed fields to mud;
|
||||
use the theme's info surface pair and restore full text contrast. */
|
||||
& ::selection {
|
||||
@apply bg-info-surface text-text;
|
||||
}
|
||||
|
||||
& .log-time {
|
||||
@apply text-text-subtle;
|
||||
@apply md:inline-block md:w-[15ch] md:overflow-hidden md:align-bottom md:whitespace-nowrap;
|
||||
}
|
||||
|
||||
& .log-sev {
|
||||
@apply font-semibold tracking-wide uppercase;
|
||||
@apply md:inline-block md:w-[7ch] md:overflow-hidden md:align-bottom md:whitespace-nowrap;
|
||||
}
|
||||
|
||||
& .log-tag {
|
||||
@apply text-text-muted;
|
||||
@apply md:inline-block md:max-w-[18ch] md:overflow-hidden md:align-bottom md:text-ellipsis md:whitespace-nowrap;
|
||||
}
|
||||
|
||||
& .lv-info .log-sev {
|
||||
@apply text-info;
|
||||
}
|
||||
|
||||
& .lv-notice .log-sev {
|
||||
@apply text-success;
|
||||
}
|
||||
|
||||
& .lv-warn .log-sev {
|
||||
@apply text-warning;
|
||||
}
|
||||
|
||||
& .lv-debug {
|
||||
@apply text-text-subtle;
|
||||
& .log-sev,
|
||||
& .log-tag {
|
||||
@apply text-text-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
/* err and above are rare and load-bearing — full-row emphasis. */
|
||||
& .lv-err,
|
||||
& .lv-crit {
|
||||
@apply bg-danger-surface;
|
||||
|
||||
&:hover {
|
||||
@apply bg-danger-surface-hover;
|
||||
}
|
||||
|
||||
& .log-sev {
|
||||
@apply text-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,6 +33,7 @@
|
||||
@import "./components/_modal.css";
|
||||
@import "./components/_overlay.css";
|
||||
@import "./components/_table.css";
|
||||
@import "./components/_syslog.css";
|
||||
@import "./components/_form.css";
|
||||
@import "./components/_segmented.css";
|
||||
@import "./components/_tab.css";
|
||||
|
||||
@ -1,71 +1,117 @@
|
||||
/*
|
||||
PATCH: admin-dashboard (luci-mod-dashboard)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-dashboard"] {
|
||||
.Dashboard {
|
||||
& > .section-content {
|
||||
& .dashboard-bg {
|
||||
background: var(--surface-overlay);
|
||||
|
||||
&.tr {
|
||||
@apply rounded-none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:not(.tr) {
|
||||
border: 1px solid var(--hairline);
|
||||
box-shadow: var(--app-shadow-lg);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--hairline);
|
||||
/* The theme maps the xl utility onto --app-shadow-md. */
|
||||
box-shadow: var(--app-shadow-md);
|
||||
}
|
||||
}
|
||||
@apply bg-surface-overlay not-[.tr]:border-hairline not-[.tr]:hover:border-hairline max-md:bg-surface-overlay not-[.tr]:max-md:border-hairline not-[.tr]:border not-[.tr]:shadow-lg not-[.tr]:hover:shadow-xl not-[.tr]:max-md:border;
|
||||
|
||||
/* Dark mode: override custom.css hardcoded light-mode colors on all text nodes */
|
||||
& span {
|
||||
@apply dark:text-text!;
|
||||
}
|
||||
& td {
|
||||
@apply dark:text-text!;
|
||||
}
|
||||
& span,
|
||||
& td,
|
||||
& th {
|
||||
@apply dark:text-text!;
|
||||
[data-darkmode="true"] & {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
img[src*=".svg"] {
|
||||
@apply dark:invert;
|
||||
[data-darkmode="true"] & {
|
||||
filter: invert(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact spacing for assoclist cells (carry .td/.th classes) */
|
||||
& .td {
|
||||
@apply py-2 max-md:py-1;
|
||||
padding-block: 0.5rem;
|
||||
|
||||
@media (width < 48rem) {
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
}
|
||||
& .th {
|
||||
@apply py-2;
|
||||
padding-block: 0.5rem;
|
||||
}
|
||||
|
||||
/* Inline info tables (e.g. DNS): strip card decoration, restore table layout */
|
||||
& .table:not(.assoclist) {
|
||||
@apply mb-0 rounded-none border-0 bg-transparent shadow-none;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
& tr {
|
||||
@apply hover:bg-transparent max-md:table-row max-md:border-0 max-md:px-0 max-md:py-0;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (width < 48rem) {
|
||||
display: table-row;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& td {
|
||||
@apply border-0 px-0 py-0.5 max-md:table-cell;
|
||||
border: 0;
|
||||
padding: 0.125rem 0;
|
||||
|
||||
@media (width < 48rem) {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@apply pr-2 whitespace-nowrap opacity-70;
|
||||
padding-right: 0.5rem;
|
||||
white-space: nowrap;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
h3 {
|
||||
@apply text-text border-0 pb-4 max-md:mx-0 max-md:pb-2;
|
||||
.title h3 {
|
||||
color: var(--text);
|
||||
border: 0;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
@media (width < 48rem) {
|
||||
margin-inline: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.router-status-wifi {
|
||||
.wifi-info {
|
||||
.devices-info {
|
||||
.tr {
|
||||
.td {
|
||||
@apply nth-3:relative nth-3:top-0 nth-3:w-full nth-3:px-0 max-md:flex-[1_1_50%];
|
||||
}
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
/*
|
||||
PATCH: admin-modem-modemdata-modempreview (luci-app-modemdata)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-modem-modemdata-modempreview"] {
|
||||
& .ifacebox-body {
|
||||
& .ifacebadge {
|
||||
@apply w-auto!;
|
||||
}
|
||||
& .ifacebox-body .ifacebadge {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,48 +2,64 @@
|
||||
PATCH: admin-modem-qmodem (luci-app-qmodem)
|
||||
Unified per-application patch — loaded on all admin/modem/qmodem/* pages.
|
||||
Prefix-matched by header.ut: admin-modem-qmodem(-overview|-sms|…).
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page^="admin-modem-qmodem"] {
|
||||
.copyright-section {
|
||||
@apply text-text-muted! text-center! bg-surface-sunken border-hairline border-1;
|
||||
color: var(--text-muted) !important;
|
||||
text-align: center !important;
|
||||
background: var(--surface-sunken);
|
||||
border: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
fieldset.cbi-section > legend {
|
||||
@apply border-hairline text-text float-left mb-4 w-full border-b px-0 pb-3 leading-tight;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
color: var(--text);
|
||||
float: left;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
padding: 0 0 0.75rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
fieldset.cbi-section {
|
||||
@apply flow-root;
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
fieldset.cbi-section > :not(legend) {
|
||||
@apply clear-both;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.cbi-section-table-titles.named {
|
||||
@apply before:bg-surface-sunken before:rounded-tl-2xl before:border-b! before:border-hairline!;
|
||||
|
||||
|
||||
.cbi-section-table-titles.named::before {
|
||||
background: var(--surface-sunken);
|
||||
/* :root exposes only --radius-base; the theme's radius scale is
|
||||
calc multiples of it (kept reactive to config overrides). */
|
||||
border-top-left-radius: calc(var(--radius-base) * 2);
|
||||
border-bottom: 1px solid var(--hairline) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-page^="admin-modem-qmodem-sms-conversation"] {
|
||||
#sms-messages-area {
|
||||
@apply border-hairline! border-1 bg-surface-sunken! text-text!;
|
||||
border: 1px solid var(--hairline) !important;
|
||||
background: var(--surface-sunken) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.sms-message-bubble {
|
||||
@apply border-hairline! border-1 bg-surface-overlay! text-text!;
|
||||
border: 1px solid var(--hairline) !important;
|
||||
background: var(--surface-overlay) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.sms-message-content {
|
||||
@apply text-text!;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.sms-message-meta {
|
||||
@apply text-text-muted!;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,97 +67,140 @@
|
||||
/* NOTE: targets <li> directly (not li > a) because qmodem's tab markup
|
||||
has no <a> inside tabs — so this won't auto-update with the shared
|
||||
_segmented.css conventions. */
|
||||
.cbi-tabmenu {
|
||||
& li {
|
||||
@apply text-text-muted hover:text-text hover:bg-hover-faint cursor-pointer rounded-4xl px-4 py-2 text-center text-sm font-medium whitespace-nowrap transition-colors duration-150;
|
||||
.cbi-tabmenu li {
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: calc(var(--radius-base) * 4);
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background-color 0.15s;
|
||||
|
||||
&.cbi-tab {
|
||||
@apply bg-text text-surface font-semibold shadow-sm;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
background: var(--hover-faint);
|
||||
}
|
||||
|
||||
&.cbi-tab {
|
||||
background: var(--text);
|
||||
color: var(--surface);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--app-shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-page="admin-modem-qmodem-sms"] {
|
||||
.td > div {
|
||||
@apply text-text-muted! mt-1.5! text-xs! leading-relaxed!;
|
||||
color: var(--text-muted) !important;
|
||||
margin-top: 0.375rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1.625 !important;
|
||||
}
|
||||
|
||||
.td > span[style*="margin-left"] {
|
||||
@apply text-text ml-2!;
|
||||
color: var(--text);
|
||||
margin-left: 0.5rem !important;
|
||||
}
|
||||
|
||||
.sms-conversations {
|
||||
@apply mt-5 mb-0;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
& > .cbi-section {
|
||||
@apply m-0! border-0! bg-transparent! p-0! shadow-none!;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
& fieldset.cbi-section {
|
||||
@apply mb-0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .table.cbi-section-table {
|
||||
@apply mb-0 overflow-hidden;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-section-table-row.tr {
|
||||
@apply text-text transition-colors duration-150;
|
||||
color: var(--text);
|
||||
transition:
|
||||
color 0.15s,
|
||||
background-color 0.15s;
|
||||
|
||||
& > .td,
|
||||
& > td {
|
||||
@apply text-text;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
& > .td:nth-child(2),
|
||||
& > td:nth-child(2) {
|
||||
@apply text-text-muted;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-hover-faint! text-text!;
|
||||
background: var(--hover-faint) !important;
|
||||
color: var(--text) !important;
|
||||
|
||||
& > .td,
|
||||
& > td {
|
||||
@apply text-text! bg-transparent!;
|
||||
color: var(--text) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sms-unread-conversation {
|
||||
@apply bg-brand-subtle! font-semibold;
|
||||
background: var(--brand-subtle) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sms-unread-badge {
|
||||
@apply bg-danger! text-on-brand! ml-2! rounded-full! px-2! py-0.5! text-xs! font-semibold!;
|
||||
background: var(--danger) !important;
|
||||
color: var(--on-brand) !important;
|
||||
margin-left: 0.5rem !important;
|
||||
border-radius: 9999px !important;
|
||||
padding: 0.125rem 0.5rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-page="admin-modem-qmodem-sms_sim"] {
|
||||
.cbi-section-table-row.tr {
|
||||
@apply text-text transition-colors duration-150;
|
||||
color: var(--text);
|
||||
transition:
|
||||
color 0.15s,
|
||||
background-color 0.15s;
|
||||
|
||||
& > .td,
|
||||
& > td {
|
||||
@apply text-text;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-hover-faint!;
|
||||
background: var(--hover-faint) !important;
|
||||
|
||||
& > .td,
|
||||
& > td {
|
||||
@apply text-text! bg-transparent!;
|
||||
color: var(--text) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-page="admin-modem-qmodem-settings"] {
|
||||
|
||||
.cbi-section-table-descr.named {
|
||||
@apply before:bg-surface-sunken before:content-['']!;
|
||||
.cbi-section-table-descr.named::before {
|
||||
background: var(--surface-sunken);
|
||||
content: "" !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
/*
|
||||
PATCH: admin-network-network (luci-app-network — bridge VLAN)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-name="bridge-vlan"] > div {
|
||||
@apply max-md:overflow-visible!;
|
||||
@media (width < 48rem) {
|
||||
[data-name="bridge-vlan"] > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
/*
|
||||
PATCH: admin-services-openclash-config (luci-app-openclash)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-services-openclash-config"] {
|
||||
.sub_div {
|
||||
img {
|
||||
@apply h-5 w-5;
|
||||
}
|
||||
.sub_div img {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
/*
|
||||
PATCH: admin-services-openclash-settings (luci-app-openclash)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-services-openclash-settings"] {
|
||||
.cbi-value-field {
|
||||
& .cbi-input-select {
|
||||
@apply max-md:w-full!;
|
||||
@media (width < 48rem) {
|
||||
.cbi-value-field .cbi-input-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.diag-style {
|
||||
@apply max-md:grid max-md:grid-cols-1 max-md:gap-4;
|
||||
& > div {
|
||||
@apply flex flex-col items-center gap-2 max-md:w-full! max-md:gap-1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
/*
|
||||
PATCH: admin-statistics-graphs (luci-app-statistics)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-statistics-graphs"] [data-plugin] img {
|
||||
@apply dark:hue-rotate-150 dark:invert;
|
||||
[data-darkmode="true"] [data-page="admin-statistics-graphs"] [data-plugin] img {
|
||||
filter: hue-rotate(150deg) invert(100%);
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
/*
|
||||
PATCH: admin-system-filemanager (luci-app-filemanager)
|
||||
Loaded on-demand by header.ut only when data-page matches this filename.
|
||||
@reference makes @apply / theme utilities resolvable without re-emitting main.css.
|
||||
Native declarations keep this standalone patch free of Tailwind helper output;
|
||||
theme values come through the :root custom properties.
|
||||
*/
|
||||
@reference "../main.css";
|
||||
|
||||
[data-page="admin-system-filemanager"] {
|
||||
#file-manager-container {
|
||||
@apply overflow-auto;
|
||||
overflow: auto;
|
||||
|
||||
#status-bar {
|
||||
@apply bg-surface border-0;
|
||||
background: var(--surface);
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
256
luci-theme-aurora/.dev/src/resource/patches/admin-status-logs.js
Normal file
256
luci-theme-aurora/.dev/src/resource/patches/admin-status-logs.js
Normal file
@ -0,0 +1,256 @@
|
||||
"use strict";
|
||||
/* Aurora log viewer — enhances the read-only <textarea id="syslog"> on the
|
||||
System Log and Kernel Log pages (both under admin/status/logs/* on every
|
||||
supported release, so this one patch name covers them) into a parsed,
|
||||
column-aligned view. The textarea stays in the DOM as the data source:
|
||||
LuCI's own poll keeps rewriting its .value, which never fires events or
|
||||
mutations, so a cheap dirty-check interval re-reads it instead. When too
|
||||
few lines parse (PARSE_GATE), the enhancement tears itself down and the
|
||||
stock textarea shows again — the worst case is the styled status quo,
|
||||
never a garbled log. Node tests require() the parsing exports below. */
|
||||
(function () {
|
||||
// RFC 5424 short names exactly as LuCI emits them. tools/views.js reads the
|
||||
// untranslated slot of its tables and ubox logread prints C-locale names, so
|
||||
// UI language never changes these. A line whose facility/severity falls
|
||||
// outside the whitelists is not a log format we know — shown raw, uncolored.
|
||||
const FACILITIES = new Set([
|
||||
"kern",
|
||||
"user",
|
||||
"mail",
|
||||
"daemon",
|
||||
"auth",
|
||||
"syslog",
|
||||
"lpr",
|
||||
"news",
|
||||
"uucp",
|
||||
"cron",
|
||||
"authpriv",
|
||||
"ftp",
|
||||
"ntp",
|
||||
"security",
|
||||
"console",
|
||||
"local0",
|
||||
"local1",
|
||||
"local2",
|
||||
"local3",
|
||||
"local4",
|
||||
"local5",
|
||||
"local6",
|
||||
"local7",
|
||||
"unknown",
|
||||
]);
|
||||
const SEVERITIES = new Set([
|
||||
"emerg",
|
||||
"alert",
|
||||
"crit",
|
||||
"err",
|
||||
"warn",
|
||||
"notice",
|
||||
"info",
|
||||
"debug",
|
||||
]);
|
||||
const SEV_CLASS = {
|
||||
emerg: "crit",
|
||||
alert: "crit",
|
||||
crit: "crit",
|
||||
err: "err",
|
||||
warn: "warn",
|
||||
notice: "notice",
|
||||
info: "info",
|
||||
debug: "debug",
|
||||
};
|
||||
// Below this parse-success ratio the value is not a log we understand
|
||||
// (syslog-ng replacement, future format change) — fall back to the textarea.
|
||||
const PARSE_GATE = 0.8;
|
||||
|
||||
function splitTag(rest) {
|
||||
// `tag[pid]: msg` / `tag: msg`; tags with spaces (kernel's
|
||||
// "mt7921e 0000:01:00.0:") deliberately fail into plain msg.
|
||||
const m = rest.match(/^([A-Za-z0-9_.\/-]+(?:\[\d+\])?):\s+([\s\S]*)$/);
|
||||
return m ? { tag: m[1], msg: m[2] } : { tag: "", msg: rest };
|
||||
}
|
||||
|
||||
// Compress "Jul 24, 2026, 10:59:50 PM UTC"-style stamps (master's RPC path
|
||||
// formats with the *browser* locale). Only en-style patterns are recognized;
|
||||
// anything else is shown untouched rather than guessed at.
|
||||
function shortenTime(datestr) {
|
||||
const m = datestr.match(
|
||||
/^(\w{3}) (\d{1,2}), \d{4}, (\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)?/,
|
||||
);
|
||||
if (!m) return datestr;
|
||||
let h = parseInt(m[3], 10);
|
||||
if (m[6] === "PM" && h < 12) h += 12;
|
||||
if (m[6] === "AM" && h === 12) h = 0;
|
||||
const hh = (h < 10 ? "0" : "") + h;
|
||||
return `${m[1]} ${m[2].padStart(2, "0")} ${hh}:${m[4]}:${m[5]}`;
|
||||
}
|
||||
|
||||
/* System log line, two shapes:
|
||||
A (master RPC): [<locale datestr>] fac.sev: tag[pid]: msg
|
||||
B (classic logread, Www Mmm dd hh:mm:ss yyyy fac.sev tag[pid]: msg
|
||||
23.05 + 24.10 syslog-wrapper + master's RPC-failure fallback) */
|
||||
function parseSyslogLine(line) {
|
||||
let m = line.match(/^\[(.+?)\]\s+(\w+)\.(\w+):\s+([\s\S]*)$/);
|
||||
if (m && FACILITIES.has(m[2]) && SEVERITIES.has(m[3]))
|
||||
return {
|
||||
time: shortenTime(m[1]),
|
||||
fullTime: m[1],
|
||||
fac: m[2],
|
||||
sev: m[3],
|
||||
...splitTag(m[4]),
|
||||
};
|
||||
m = line.match(
|
||||
/^\w{3} (\w{3}) +(\d{1,2}) (\d{2}:\d{2}:\d{2}) (\d{4}) (\w+)\.(\w+) ([\s\S]*)$/,
|
||||
);
|
||||
if (m && FACILITIES.has(m[5]) && SEVERITIES.has(m[6]))
|
||||
return {
|
||||
time: `${m[1]} ${m[2].padStart(2, "0")} ${m[3]}`,
|
||||
fullTime: line.slice(0, line.indexOf(` ${m[5]}.${m[6]} `)),
|
||||
fac: m[5],
|
||||
sev: m[6],
|
||||
...splitTag(m[7]),
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Kernel log line: `[ 73412.882110] msg`. No severity survives into the
|
||||
text on any LuCI version (both eras strip/drop the <N> prefix), so dmesg
|
||||
gets time/tag/typography only — no level colors, no keyword guessing. */
|
||||
function parseDmesgLine(line) {
|
||||
const m = line.match(/^\[\s*(\d+\.\d+)\]\s?([\s\S]*)$/);
|
||||
if (!m) return null;
|
||||
return {
|
||||
time: `[${m[1]}]`,
|
||||
fullTime: "",
|
||||
fac: "",
|
||||
sev: "",
|
||||
...splitTag(m[2]),
|
||||
};
|
||||
}
|
||||
|
||||
function evaluate(value, parseLine) {
|
||||
const lines = value ? value.split(/\n/) : [];
|
||||
const rows = lines.map((line) => ({ line, parsed: parseLine(line) }));
|
||||
const parsed = rows.reduce((n, r) => n + (r.parsed ? 1 : 0), 0);
|
||||
return { rows, ratio: rows.length ? parsed / rows.length : 1 };
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = {
|
||||
parseSyslogLine,
|
||||
parseDmesgLine,
|
||||
splitTag,
|
||||
shortenTime,
|
||||
evaluate,
|
||||
PARSE_GATE,
|
||||
};
|
||||
}
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
// Kernel-log page under either menu layout: admin-status-dmesg on
|
||||
// 23.05/24.10, admin-status-logs-dmesg on master's combined Logs menu.
|
||||
const page = document.body?.dataset?.page || "";
|
||||
const parseLine = /(^|-)dmesg(-|$)/.test(page)
|
||||
? parseDmesgLine
|
||||
: parseSyslogLine;
|
||||
|
||||
let viewer = null;
|
||||
let lastValue = null;
|
||||
|
||||
function span(cls, text, title) {
|
||||
const el = document.createElement("span");
|
||||
el.className = cls;
|
||||
el.textContent = text;
|
||||
if (title) el.title = title;
|
||||
return el;
|
||||
}
|
||||
|
||||
function buildRow(entry) {
|
||||
const row = document.createElement("div");
|
||||
if (!entry.parsed) {
|
||||
row.className = "log-row";
|
||||
row.textContent = entry.line;
|
||||
return row;
|
||||
}
|
||||
const p = entry.parsed;
|
||||
row.className = "log-row" + (p.sev ? ` lv-${SEV_CLASS[p.sev]}` : "");
|
||||
// Real space text nodes between plain inline spans: the row wraps like
|
||||
// terminal output and selection copies clean text. The level is the short
|
||||
// severity word (CSS uppercases and colors it; facility lives in its
|
||||
// tooltip); the source keeps its full name in a tooltip for when the
|
||||
// aligned column truncates it.
|
||||
row.appendChild(span("log-time", p.time, p.fullTime));
|
||||
if (p.sev) {
|
||||
row.appendChild(document.createTextNode(" "));
|
||||
row.appendChild(span("log-sev", p.sev, `${p.fac}.${p.sev}`));
|
||||
}
|
||||
if (p.tag) {
|
||||
row.appendChild(document.createTextNode(" "));
|
||||
row.appendChild(span("log-tag", p.tag, p.tag));
|
||||
}
|
||||
row.appendChild(document.createTextNode(" "));
|
||||
row.appendChild(span("log-msg", p.msg));
|
||||
return row;
|
||||
}
|
||||
|
||||
function selectionInside(el) {
|
||||
const sel = window.getSelection && window.getSelection();
|
||||
if (!sel || sel.isCollapsed) return false;
|
||||
return el.contains(sel.anchorNode) || el.contains(sel.focusNode);
|
||||
}
|
||||
|
||||
function teardown(container) {
|
||||
if (viewer) viewer.remove();
|
||||
viewer = null;
|
||||
container.classList.remove("log-enhanced");
|
||||
}
|
||||
|
||||
function render(textarea, container, rows) {
|
||||
if (!viewer) {
|
||||
viewer = document.createElement("div");
|
||||
viewer.className = "syslog-view";
|
||||
textarea.insertAdjacentElement("afterend", viewer);
|
||||
container.classList.add("log-enhanced");
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const entry of rows) frag.appendChild(buildRow(entry));
|
||||
viewer.replaceChildren(frag);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const textarea = document.getElementById("syslog");
|
||||
const container = document.getElementById("content_syslog");
|
||||
if (!textarea || !container) return;
|
||||
const value = textarea.value;
|
||||
if (value === lastValue) return;
|
||||
// Re-rendering would destroy an in-progress selection; retry next tick.
|
||||
if (viewer && selectionInside(viewer)) return;
|
||||
lastValue = value;
|
||||
const { rows, ratio } = evaluate(value, parseLine);
|
||||
if (ratio >= PARSE_GATE) render(textarea, container, rows);
|
||||
else teardown(container);
|
||||
}
|
||||
|
||||
// Content updates: LuCI's poll rewrites .value, which fires nothing — a
|
||||
// cheap dirty-check covers it.
|
||||
setInterval(tick, 1000);
|
||||
|
||||
// First paint: the view (and its textarea) is inserted asynchronously by
|
||||
// LuCI after this script runs. Element insertion IS an observable childList
|
||||
// mutation (unlike .value writes), and the callback runs as a microtask
|
||||
// before the browser paints the inserted textarea — so the viewer takes
|
||||
// over without a flash of the raw log.
|
||||
if (document.getElementById("syslog")) {
|
||||
tick();
|
||||
} else {
|
||||
const arrival = new MutationObserver(function () {
|
||||
if (!document.getElementById("syslog")) return;
|
||||
arrival.disconnect();
|
||||
tick();
|
||||
});
|
||||
arrival.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
})();
|
||||
171
luci-theme-aurora/.dev/tests/log-patch.test.js
Normal file
171
luci-theme-aurora/.dev/tests/log-patch.test.js
Normal file
@ -0,0 +1,171 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
/* The patch ships as a plain deferred script, not a module. Evaluate it with
|
||||
an injected `module` to reach its parsing exports; `document` stays
|
||||
undefined so the DOM bootstrap is skipped. */
|
||||
const source = await readFile(
|
||||
new URL("../src/resource/patches/admin-status-logs.js", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const mod = { exports: {} };
|
||||
new Function("module", "exports", "document", "window", source)(
|
||||
mod,
|
||||
mod.exports,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
const { parseSyslogLine, parseDmesgLine, shortenTime, evaluate, PARSE_GATE } =
|
||||
mod.exports;
|
||||
|
||||
test("classic logread format (23.05 syslog, 24.10 syslog-wrapper) parses", () => {
|
||||
const r = parseSyslogLine(
|
||||
"Sat Jul 26 03:14:15 2026 daemon.info dnsmasq-dhcp[1]: DHCPACK(br-lan) 192.168.1.140",
|
||||
);
|
||||
assert.deepEqual(
|
||||
{ time: r.time, fac: r.fac, sev: r.sev, tag: r.tag },
|
||||
{
|
||||
time: "Jul 26 03:14:15",
|
||||
fac: "daemon",
|
||||
sev: "info",
|
||||
tag: "dnsmasq-dhcp[1]",
|
||||
},
|
||||
);
|
||||
assert.equal(r.msg, "DHCPACK(br-lan) 192.168.1.140");
|
||||
|
||||
// ctime() pads single-digit days with a space.
|
||||
const padded = parseSyslogLine(
|
||||
"Sun Jul 6 09:05:07 2026 daemon.warn odhcpd[2029]: No default route present",
|
||||
);
|
||||
assert.equal(padded.time, "Jul 06 09:05:07");
|
||||
|
||||
// Kernel lines routed through logread; hostapd logs without a pid.
|
||||
assert.equal(
|
||||
parseSyslogLine(
|
||||
"Sat Jul 26 03:14:16 2026 kern.info kernel: [73412.882110] br-lan: port 3(lan3) entered forwarding state",
|
||||
).tag,
|
||||
"kernel",
|
||||
);
|
||||
assert.equal(
|
||||
parseSyslogLine(
|
||||
"Sat Jul 26 03:14:17 2026 daemon.notice hostapd: wlan1: AP-STA-CONNECTED 3c:22:fb:8a:12:9d",
|
||||
).tag,
|
||||
"hostapd",
|
||||
);
|
||||
});
|
||||
|
||||
test("master RPC bracket format parses across browser locales", () => {
|
||||
const en = parseSyslogLine(
|
||||
"[Jul 24, 2026, 10:59:50 PM UTC] daemon.info: dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.140",
|
||||
);
|
||||
assert.equal(en.time, "Jul 24 22:59:50");
|
||||
assert.equal(en.fullTime, "Jul 24, 2026, 10:59:50 PM UTC");
|
||||
|
||||
// timeStyle "full" spells the zone out; still en-shaped, still shortened.
|
||||
assert.equal(
|
||||
parseSyslogLine(
|
||||
"[Jul 24, 2026, 10:59:50 PM Coordinated Universal Time] daemon.warn: odhcpd[2029]: No default route present",
|
||||
).time,
|
||||
"Jul 24 22:59:50",
|
||||
);
|
||||
|
||||
// Non-en datestrs parse fine and display untouched — never guessed at.
|
||||
const zh = parseSyslogLine(
|
||||
"[2026年7月24日 GMT 22:59:50] daemon.err: odhcpd[2029]: Failed to send RA",
|
||||
);
|
||||
assert.equal(zh.sev, "err");
|
||||
assert.equal(zh.time, "2026年7月24日 GMT 22:59:50");
|
||||
});
|
||||
|
||||
test("severity comes from the field, never from message content", () => {
|
||||
const r = parseSyslogLine(
|
||||
"[Jul 24, 2026, 11:00:00 PM UTC] daemon.info: logger: user said daemon.err: fake",
|
||||
);
|
||||
assert.equal(r.sev, "info");
|
||||
assert.equal(r.msg, "user said daemon.err: fake");
|
||||
});
|
||||
|
||||
test("unknown facility/severity and foreign formats are rejected", () => {
|
||||
assert.equal(
|
||||
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] bogus.info: x: y"),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] daemon.bogus: x: y"),
|
||||
null,
|
||||
);
|
||||
// syslog-ng's classic template has no facility.severity field at all.
|
||||
assert.equal(
|
||||
parseSyslogLine("Jul 26 10:00:00 OpenWrt dnsmasq[123]: query answered"),
|
||||
null,
|
||||
);
|
||||
assert.equal(parseSyslogLine(""), null);
|
||||
});
|
||||
|
||||
test("dmesg lines: uptime stamp and safe tag split, no severity", () => {
|
||||
const r = parseDmesgLine(
|
||||
"[ 12.345678] br-lan: port 3(lan3) entered forwarding state",
|
||||
);
|
||||
assert.equal(r.time, "[12.345678]");
|
||||
assert.equal(r.tag, "br-lan");
|
||||
assert.equal(r.sev, "");
|
||||
|
||||
// A device path with spaces must not be mistaken for a tag.
|
||||
const pci = parseDmesgLine(
|
||||
"[75821.004518] mt7921e 0000:01:00.0: Message 00000010 timeout",
|
||||
);
|
||||
assert.equal(pci.tag, "");
|
||||
assert.equal(pci.msg, "mt7921e 0000:01:00.0: Message 00000010 timeout");
|
||||
|
||||
assert.equal(parseDmesgLine("no bracket prefix here"), null);
|
||||
});
|
||||
|
||||
test("shortenTime handles 12-hour edges and leaves unknown shapes alone", () => {
|
||||
assert.equal(shortenTime("Jul 24, 2026, 12:05:01 AM UTC"), "Jul 24 00:05:01");
|
||||
assert.equal(shortenTime("Jul 24, 2026, 12:15:09 PM UTC"), "Jul 24 12:15:09");
|
||||
assert.equal(
|
||||
shortenTime("2026年7月24日 GMT 22:59:50"),
|
||||
"2026年7月24日 GMT 22:59:50",
|
||||
);
|
||||
});
|
||||
|
||||
test("parse gate: mixed content falls back, empty value stays calm", () => {
|
||||
const good = "Sat Jul 26 03:14:15 2026 daemon.info dnsmasq[1]: ok";
|
||||
const bad = "Jul 26 10:00:00 OpenWrt dnsmasq[123]: syslog-ng style";
|
||||
|
||||
const healthy = evaluate(
|
||||
[good, good, good, good, bad].join("\n"),
|
||||
parseSyslogLine,
|
||||
);
|
||||
assert.ok(healthy.ratio >= PARSE_GATE, "4/5 parsed must stay enhanced");
|
||||
|
||||
const foreign = evaluate([bad, bad, bad].join("\n"), parseSyslogLine);
|
||||
assert.ok(foreign.ratio < PARSE_GATE, "foreign log must fall back");
|
||||
|
||||
const empty = evaluate("", parseSyslogLine);
|
||||
assert.equal(empty.rows.length, 0);
|
||||
assert.equal(empty.ratio, 1, "empty log must not trip the gate");
|
||||
});
|
||||
|
||||
test("built log payloads land in their homes", () => {
|
||||
const out = resolve(import.meta.dirname, "../../htdocs/luci-static/aurora");
|
||||
// The JS ships on-demand under one name — every supported release
|
||||
// (23.05/24.10/master) mounts both log pages under admin/status/logs/*.
|
||||
const js = resolve(out, "patches/admin-status-logs.js");
|
||||
assert.ok(existsSync(js), `${js} missing — run pnpm build first`);
|
||||
assert.ok(statSync(js).size <= 6_000, `${js} exceeds 6 KB`);
|
||||
// The CSS is core-page styling and lives in the main bundle, where the
|
||||
// Tailwind token pipeline (radius-base chain, app shadows) applies — a
|
||||
// stray patches/ copy would mean the component migration regressed.
|
||||
const main = readFileSync(resolve(out, "main.css"), "utf8");
|
||||
assert.ok(main.includes(".syslog-view"), "main.css lost the log viewer");
|
||||
assert.ok(
|
||||
main.includes("calc(var(--radius-base)"),
|
||||
"radius utilities no longer resolve through the reactive base chain",
|
||||
);
|
||||
assert.ok(!existsSync(resolve(out, "patches/admin-status-logs.css")));
|
||||
assert.ok(!existsSync(resolve(out, "patches/_log-shared.css")));
|
||||
});
|
||||
@ -9,7 +9,10 @@ import { exec } from "child_process";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { browserslistToTargets, transform as lightningcssTransform } from "lightningcss";
|
||||
import {
|
||||
browserslistToTargets,
|
||||
transform as lightningcssTransform,
|
||||
} from "lightningcss";
|
||||
import { basename, dirname, join, relative, resolve, sep } from "path";
|
||||
import { minify as terserMinify } from "terser";
|
||||
import { promisify } from "util";
|
||||
@ -42,8 +45,8 @@ function createLuciJsCompressPlugin(): Plugin {
|
||||
},
|
||||
async generateBundle() {
|
||||
const srcDir = resolve(CURRENT_DIR, "src/resource");
|
||||
const jsFiles = (await readdir(srcDir, { recursive: true })).filter(
|
||||
(f) => f.endsWith(".js"),
|
||||
const jsFiles = (await readdir(srcDir, { recursive: true })).filter((f) =>
|
||||
f.endsWith(".js"),
|
||||
);
|
||||
await Promise.all(
|
||||
jsFiles.map(async (relPath) => {
|
||||
@ -58,9 +61,24 @@ function createLuciJsCompressPlugin(): Plugin {
|
||||
mangle: true,
|
||||
format: { comments: false, beautify: false },
|
||||
});
|
||||
const outputPath = join(outDir, "resources", normalized);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, compressed.code || sourceCode, "utf-8");
|
||||
// patches/* are payloads of the on-demand patches mechanism and
|
||||
// ship next to the CSS patches (media dir), not under resources/.
|
||||
const stem = normalized.startsWith("patches/")
|
||||
? normalized.slice("patches/".length, -".js".length)
|
||||
: null;
|
||||
const outputPaths = stem
|
||||
? [stem, ...(PATCH_ALIASES[stem] ?? [])].map((p) =>
|
||||
join(outDir, "aurora", "patches", `${p}.js`),
|
||||
)
|
||||
: [join(outDir, "resources", normalized)];
|
||||
for (const outputPath of outputPaths) {
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(
|
||||
outputPath,
|
||||
compressed.code || sourceCode,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`${tag("JS Compress")} src/resource/${normalized}: ${error?.message}`,
|
||||
@ -72,6 +90,30 @@ function createLuciJsCompressPlugin(): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
/* Duplicate built CSS patches under their PATCH_ALIASES names (JS aliases are
|
||||
handled inside the compress plugin). Runs post-bundle because the sources
|
||||
are Rollup entries. */
|
||||
function createPatchAliasPlugin(): Plugin {
|
||||
return {
|
||||
name: "patch-alias",
|
||||
apply: "build",
|
||||
enforce: "post",
|
||||
async closeBundle() {
|
||||
for (const [stem, aliases] of Object.entries(PATCH_ALIASES)) {
|
||||
const source = resolve(BUILD_OUTPUT, `aurora/patches/${stem}.css`);
|
||||
if (!existsSync(source)) continue;
|
||||
const css = await readFile(source, "utf-8");
|
||||
for (const alias of aliases)
|
||||
await writeFile(
|
||||
resolve(BUILD_OUTPUT, `aurora/patches/${alias}.css`),
|
||||
css,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function removeMacOsMetadata(dir: string): Promise<void> {
|
||||
if (!existsSync(dir)) return;
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
@ -105,7 +147,9 @@ function createLoginCssPrunePlugin(): Plugin {
|
||||
for (const [, name, value] of css.matchAll(
|
||||
new RegExp(`(?<=[{;])(--[\\w-]+|[a-zA-Z-]+):(${VALUE})`, "g"),
|
||||
)) {
|
||||
const refs = [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1]);
|
||||
const refs = [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map(
|
||||
(m) => m[1],
|
||||
);
|
||||
if (!name.startsWith("--")) refs.forEach((r) => roots.add(r));
|
||||
else {
|
||||
const deps = edges.get(name) ?? new Set<string>();
|
||||
@ -168,6 +212,27 @@ function createPackageHygienePlugin(): Plugin {
|
||||
const PATCH_PUBLIC_PREFIX = "/luci-static/aurora/patches/";
|
||||
const PATCH_SRC_DIR = resolve(CURRENT_DIR, "src/media/patches");
|
||||
|
||||
// JS payloads of the same on-demand mechanism: src/resource/patches/<page>.js
|
||||
// builds to aurora/patches/<page>.js so header.ut's single lsdir() discovers
|
||||
// CSS and JS patches together.
|
||||
const JS_PATCH_SRC_DIR = resolve(CURRENT_DIR, "src/resource/patches");
|
||||
|
||||
// A built payload (CSS or JS) can serve several pages via aliases, keyed by
|
||||
// source stem. Prefix matching is per-page, and naming one file after a
|
||||
// shorter shared prefix (e.g. `admin-status`) would load it on every status
|
||||
// subpage — including the busiest overview page — so the built output is
|
||||
// duplicated under each alias instead. Two identical CSS *entries* would not
|
||||
// work either: Rollup deduplicates same-content assets into a single file.
|
||||
// Currently empty: the log viewer needs only admin-status-logs — every
|
||||
// supported release (23.05/24.10/master) mounts both log pages under
|
||||
// admin/status/logs/*, so the prefix covers System Log, Kernel Log and the
|
||||
// bare /logs alias with one name.
|
||||
const PATCH_ALIASES: Record<string, string[]> = {};
|
||||
const patchAliasSource = (stem: string): string | undefined =>
|
||||
Object.entries(PATCH_ALIASES).find(([, aliases]) =>
|
||||
aliases.includes(stem),
|
||||
)?.[0];
|
||||
|
||||
// Theme assets (public/aurora/**: images, fonts): serve the copy in this
|
||||
// checkout at /luci-static/aurora/<path>. Without this, header.ut's logo,
|
||||
// favicon, webmanifest and font references fall through to the OpenWrt proxy
|
||||
@ -230,13 +295,44 @@ function createLocalServePlugin(): Plugin {
|
||||
pathname.startsWith(PATCH_PUBLIC_PREFIX) &&
|
||||
pathname.endsWith(".css")
|
||||
) {
|
||||
const file = basename(pathname);
|
||||
if (existsSync(join(PATCH_SRC_DIR, file))) {
|
||||
// Aliases resolve to their shared source, mirroring the build-time
|
||||
// duplication.
|
||||
const stem = basename(pathname, ".css");
|
||||
const source = existsSync(join(PATCH_SRC_DIR, `${stem}.css`))
|
||||
? stem
|
||||
: patchAliasSource(stem);
|
||||
if (source && existsSync(join(PATCH_SRC_DIR, `${source}.css`))) {
|
||||
req.url =
|
||||
`/src/media/patches/${file}` + (search ? `?${search}` : "");
|
||||
`/src/media/patches/${source}.css` + (search ? `?${search}` : "");
|
||||
return next();
|
||||
}
|
||||
}
|
||||
if (
|
||||
pathname.startsWith(PATCH_PUBLIC_PREFIX) &&
|
||||
pathname.endsWith(".js")
|
||||
) {
|
||||
const stem = basename(pathname, ".js");
|
||||
const source = existsSync(join(JS_PATCH_SRC_DIR, `${stem}.js`))
|
||||
? stem
|
||||
: patchAliasSource(stem);
|
||||
if (source && existsSync(join(JS_PATCH_SRC_DIR, `${source}.js`))) {
|
||||
try {
|
||||
const code = await readFile(
|
||||
join(JS_PATCH_SRC_DIR, `${source}.js`),
|
||||
"utf-8",
|
||||
);
|
||||
res.setHeader("Content-Type", "text/javascript");
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.statusCode = 200;
|
||||
res.end(code);
|
||||
return;
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
`${tag("Serve")} ${pathname} → cannot read patches/${source}.js: ${err?.message ?? err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith(ASSET_PUBLIC_PREFIX)) {
|
||||
const asset = localAsset(pathname.slice(ASSET_PUBLIC_PREFIX.length));
|
||||
if (asset) {
|
||||
@ -266,7 +362,10 @@ function createLocalServePlugin(): Plugin {
|
||||
const nf = file.replace(/\\/g, "/");
|
||||
const isThemeCss =
|
||||
nf.startsWith(MEDIA_SRC_DIR + "/") && nf.endsWith(".css");
|
||||
if (isThemeCss || reloadJsFiles.has(nf)) {
|
||||
const isJsPatch =
|
||||
nf.startsWith(JS_PATCH_SRC_DIR.replace(/\\/g, "/") + "/") &&
|
||||
nf.endsWith(".js");
|
||||
if (isThemeCss || isJsPatch || reloadJsFiles.has(nf)) {
|
||||
console.log(
|
||||
`${tag("Serve")} ${relative(CURRENT_DIR, file)} → full reload`,
|
||||
);
|
||||
@ -377,7 +476,10 @@ function neutralizeLuciRuntime(html: string): string {
|
||||
"",
|
||||
);
|
||||
return stripped.includes("</head>")
|
||||
? stripped.replace(/<head\b[^>]*>/i, (m) => `${m}\n ${MOCK_RUNTIME_GUARD}`)
|
||||
? stripped.replace(
|
||||
/<head\b[^>]*>/i,
|
||||
(m) => `${m}\n ${MOCK_RUNTIME_GUARD}`,
|
||||
)
|
||||
: MOCK_RUNTIME_GUARD + stripped;
|
||||
}
|
||||
|
||||
@ -884,6 +986,7 @@ export default defineConfig(({ mode }) => {
|
||||
createUtSyncPlugin(OPENWRT_SSH_HOST),
|
||||
createLuciJsCompressPlugin(),
|
||||
createLoginCssPrunePlugin(),
|
||||
createPatchAliasPlugin(),
|
||||
createPackageHygienePlugin(),
|
||||
],
|
||||
css: {
|
||||
@ -902,9 +1005,11 @@ export default defineConfig(({ mode }) => {
|
||||
// On-demand third-party patches: one entry per page, output to
|
||||
// aurora/patches/<page>.css (the `patches/` key prefix lands them there
|
||||
// via assetFileNames below). header.ut links the matching one per page.
|
||||
// `_`-prefixed files are shared partials @imported by entries, not
|
||||
// entries themselves (they'd otherwise ship as never-matching patches).
|
||||
...Object.fromEntries(
|
||||
(existsSync(PATCH_SRC_DIR) ? readdirSync(PATCH_SRC_DIR) : [])
|
||||
.filter((f) => f.endsWith(".css"))
|
||||
.filter((f) => f.endsWith(".css") && !f.startsWith("_"))
|
||||
.map((f) => [
|
||||
`patches/${f.slice(0, -4)}`,
|
||||
join(PATCH_SRC_DIR, f),
|
||||
|
||||
@ -27,7 +27,7 @@ No linter CLI. Formatting uses Prettier with format-on-save (`.vscode/settings.j
|
||||
- `.dev/public/aurora/` → `htdocs/luci-static/aurora/` (copied as-is)
|
||||
- `ucode/template/themes/aurora/*.ut` — server-side templates, not processed by Vite
|
||||
|
||||
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/` — `main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_elements.css`, `_layout.css`, `components/*.css`, `_utilities.css`) and `login.css` (standalone login page; imports `_base.css`). Third-party compatibility patches are not bundled into `main.css`; each lives in `media/patches/<data-page>.css`, builds to its own `htdocs/luci-static/aurora/patches/<data-page>.css`, and is linked on demand by `header.ut`, which discovers installed patches at render time via `fs.lsdir()` and matches them on path-segment-boundary prefixes (dynamic subpages inherit their prefix's patch; third-party packages may drop their own patch into the directory) — see "On-Demand Third-Party Patches" in `.dev/docs/DEVELOPMENT.md`. All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. Built CSS keeps Tailwind's native `@layer` structure (theme partials are unlayered, so they outrank layered utilities); the OKLCH requirement already gates browsers to ones with `@layer` support. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
|
||||
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/` — `main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_elements.css`, `_layout.css`, `components/*.css`, `_utilities.css`) and `login.css` (standalone login page; imports `_base.css`). Third-party compatibility patches are not bundled into `main.css`; each lives in `media/patches/<data-page>.css`, builds to its own `htdocs/luci-static/aurora/patches/<data-page>.css`, and is linked on demand by `header.ut`, which discovers installed patches at render time via `fs.lsdir()` and matches them on path-segment-boundary prefixes (dynamic subpages inherit their prefix's patch; third-party packages may drop their own patch into the directory). The same sweep also loads `patches/<data-page>.js` payloads as deferred scripts (theme-owned JS patches live in `.dev/src/resource/patches/`; `PATCH_ALIASES` in `vite.config.ts` duplicates one built payload under several page names) — see "On-Demand Third-Party Patches" in `.dev/docs/DEVELOPMENT.md`. All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). The one deliberate exception is `media/patches/*.css`, which is plain native CSS by design (smaller standalone output, and third-party patch authors need no Tailwind) — reach theme values through the `:root` custom properties (`var(--surface)`, `var(--radius-base)`, …). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. Built CSS keeps Tailwind's native `@layer` structure (theme partials are unlayered, so they outrank layered utilities); the OKLCH requirement already gates browsers to ones with `@layer` support. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
|
||||
|
||||
**Design tokens**: `_tokens.css` is GENERATED — do not hand-edit (see its header comment). Source of truth is `.dev/tokens/`: `defaults.js` holds the 10 editable input colors per mode (`bg`, `surface`, `text`, `brand`, `on_brand`, `link`, `info`, `warning`, `success`, `danger`); `spec.js` declares how every other token derives from those via `mix`/`shade`/`set`/`alpha`/`const` operators plus `FIXED` literals (shadows); `engine.js`/`resolve.js` do the OKLCH math and flatten everything to plain `oklch(...)` values. After changing inputs or derivations, run `pnpm gen:tokens` to rewrite `_tokens.css`, and `pnpm test` to check the color-math and token invariants.
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=1.1.6
|
||||
PKG_RELEASE:=59
|
||||
PKG_VERSION:=1.1.8
|
||||
PKG_RELEASE:=60
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,2 +1 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
[data-page=admin-modem-modemdata-modempreview] .ifacebox-body .ifacebadge{width:auto!important}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,2 +1 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
@media not all and (min-width:48rem){[data-name=bridge-vlan]>div{overflow:visible!important}}
|
||||
@media not (min-width:48rem){[data-name=bridge-vlan]>div{overflow:visible!important}}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
[data-page=admin-services-openclash-config] .sub_div img{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}
|
||||
[data-page=admin-services-openclash-config] .sub_div img{width:1.25rem;height:1.25rem}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
@media not all and (min-width:48rem){[data-page=admin-services-openclash-settings] .cbi-value-field .cbi-input-select{width:100%!important}[data-page=admin-services-openclash-settings] .diag-style{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}}[data-page=admin-services-openclash-settings] .diag-style>div{align-items:center;gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}@media not all and (min-width:48rem){[data-page=admin-services-openclash-settings] .diag-style>div{gap:var(--spacing);width:100%!important}}
|
||||
@media not (min-width:48rem){[data-page=admin-services-openclash-settings] .cbi-value-field .cbi-input-select{width:100%!important}}[data-page=admin-services-openclash-settings] .diag-style>div{flex-direction:column;align-items:center;gap:.5rem;display:flex}@media not (min-width:48rem){[data-page=admin-services-openclash-settings] .diag-style{grid-template-columns:repeat(1,minmax(0,1fr));gap:1rem;display:grid}[data-page=admin-services-openclash-settings] .diag-style>div{gap:.25rem;width:100%!important}}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
/*! 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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}[data-page=admin-statistics-graphs] [data-plugin] img:where([data-darkmode=true],[data-darkmode=true] *){--tw-hue-rotate:hue-rotate(150deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);--tw-invert:invert(100%)}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
||||
[data-darkmode=true] [data-page=admin-statistics-graphs] [data-plugin] img{filter:hue-rotate(150deg)invert()}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"use strict";!function(){const e=new Set(["kern","user","mail","daemon","auth","syslog","lpr","news","uucp","cron","authpriv","ftp","ntp","security","console","local0","local1","local2","local3","local4","local5","local6","local7","unknown"]),t=new Set(["emerg","alert","crit","err","warn","notice","info","debug"]),n={emerg:"crit",alert:"crit",crit:"crit",err:"err",warn:"warn",notice:"notice",info:"info",debug:"debug"};function o(e){const t=e.match(/^([A-Za-z0-9_.\/-]+(?:\[\d+\])?):\s+([\s\S]*)$/);return t?{tag:t[1],msg:t[2]}:{tag:"",msg:e}}function s(e){const t=e.match(/^(\w{3}) (\d{1,2}), \d{4}, (\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)?/);if(!t)return e;let n=parseInt(t[3],10);"PM"===t[6]&&n<12&&(n+=12),"AM"===t[6]&&12===n&&(n=0);const o=(n<10?"0":"")+n;return`${t[1]} ${t[2].padStart(2,"0")} ${o}:${t[4]}:${t[5]}`}function c(n){let c=n.match(/^\[(.+?)\]\s+(\w+)\.(\w+):\s+([\s\S]*)$/);return c&&e.has(c[2])&&t.has(c[3])?{time:s(c[1]),fullTime:c[1],fac:c[2],sev:c[3],...o(c[4])}:(c=n.match(/^\w{3} (\w{3}) +(\d{1,2}) (\d{2}:\d{2}:\d{2}) (\d{4}) (\w+)\.(\w+) ([\s\S]*)$/),c&&e.has(c[5])&&t.has(c[6])?{time:`${c[1]} ${c[2].padStart(2,"0")} ${c[3]}`,fullTime:n.slice(0,n.indexOf(` ${c[5]}.${c[6]} `)),fac:c[5],sev:c[6],...o(c[7])}:null)}function l(e){const t=e.match(/^\[\s*(\d+\.\d+)\]\s?([\s\S]*)$/);return t?{time:`[${t[1]}]`,fullTime:"",fac:"",sev:"",...o(t[2])}:null}function a(e,t){const n=(e?e.split(/\n/):[]).map(e=>({line:e,parsed:t(e)})),o=n.reduce((e,t)=>e+(t.parsed?1:0),0);return{rows:n,ratio:n.length?o/n.length:1}}if("undefined"!=typeof module&&module.exports&&(module.exports={parseSyslogLine:c,parseDmesgLine:l,splitTag:o,shortenTime:s,evaluate:a,PARSE_GATE:.8}),"undefined"==typeof document)return;const r=document.body?.dataset?.page||"",d=/(^|-)dmesg(-|$)/.test(r)?l:c;let i=null,u=null;function m(e,t,n){const o=document.createElement("span");return o.className=e,o.textContent=t,n&&(o.title=n),o}function g(e){const t=document.createElement("div");if(!e.parsed)return t.className="log-row",t.textContent=e.line,t;const o=e.parsed;return t.className="log-row"+(o.sev?` lv-${n[o.sev]}`:""),t.appendChild(m("log-time",o.time,o.fullTime)),o.sev&&(t.appendChild(document.createTextNode(" ")),t.appendChild(m("log-sev",o.sev,`${o.fac}.${o.sev}`))),o.tag&&(t.appendChild(document.createTextNode(" ")),t.appendChild(m("log-tag",o.tag,o.tag))),t.appendChild(document.createTextNode(" ")),t.appendChild(m("log-msg",o.msg)),t}function f(){const e=document.getElementById("syslog"),t=document.getElementById("content_syslog");if(!e||!t)return;const n=e.value;if(n===u)return;if(i&&function(e){const t=window.getSelection&&window.getSelection();return!(!t||t.isCollapsed)&&(e.contains(t.anchorNode)||e.contains(t.focusNode))}(i))return;u=n;const{rows:o,ratio:s}=a(n,d);s>=.8?function(e,t,n){i||(i=document.createElement("div"),i.className="syslog-view",e.insertAdjacentElement("afterend",i),t.classList.add("log-enhanced"));const o=document.createDocumentFragment();for(const e of n)o.appendChild(g(e));i.replaceChildren(o)}(e,t,o):function(e){i&&i.remove(),i=null,e.classList.remove("log-enhanced")}(t)}if(setInterval(f,1e3),document.getElementById("syslog"))f();else{const e=new MutationObserver(function(){document.getElementById("syslog")&&(e.disconnect(),f())});e.observe(document.documentElement,{childList:!0,subtree:!0})}}();
|
||||
@ -1,2 +1 @@
|
||||
/*! 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-border-style:solid}}}[data-page=admin-system-filemanager] #file-manager-container{overflow:auto}[data-page=admin-system-filemanager] #file-manager-container #status-bar{border-style:var(--tw-border-style);background-color:var(--surface);border-width:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}
|
||||
[data-page=admin-system-filemanager] #file-manager-container{overflow:auto}[data-page=admin-system-filemanager] #file-manager-container #status-bar{background:var(--surface);border:0}
|
||||
|
||||
@ -22,15 +22,19 @@
|
||||
const page_segs = length(ctx?.request_path) ? ctx.request_path : (ctx?.path || []);
|
||||
const page = join('-', page_segs);
|
||||
// On-demand patches — see "On-Demand Third-Party Patches" in DEVELOPMENT.md.
|
||||
let patch_files = [];
|
||||
let patch_files = [], patch_scripts = [];
|
||||
if (!blank_page) {
|
||||
let seg_prefixes = {}, acc = null;
|
||||
for (let seg in page_segs)
|
||||
seg_prefixes[acc = (acc == null ? seg : `${acc}-${seg}`)] = true;
|
||||
for (let f in lsdir(`/www${media}/patches`) ?? [])
|
||||
for (let f in lsdir(`/www${media}/patches`) ?? []) {
|
||||
if (substr(f, -4) == '.css' && exists(seg_prefixes, substr(f, 0, -4)))
|
||||
push(patch_files, substr(f, 0, -4));
|
||||
else if (substr(f, -3) == '.js' && exists(seg_prefixes, substr(f, 0, -3)))
|
||||
push(patch_scripts, substr(f, 0, -3));
|
||||
}
|
||||
sort(patch_files);
|
||||
sort(patch_scripts);
|
||||
}
|
||||
let token_css_light = '', token_css_dark = '';
|
||||
for (let key, val in tokens) {
|
||||
@ -179,6 +183,9 @@
|
||||
{% for (let patch in patch_files): %}
|
||||
<link rel="stylesheet" href="{{ media }}/patches/{{ patch }}.css">
|
||||
{% endfor %}
|
||||
{% for (let patch in patch_scripts): %}
|
||||
<script defer src="{{ media }}/patches/{{ patch }}.js"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if (font_preload): %}
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="{{ font_preload }}">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user