🌴 Sync 2026-06-12 00:46:55

This commit is contained in:
github-actions[bot] committed 2026-06-12 00:46:55 +08:00
1 parent b23950c545
commit dbba6b0100
29 files changed
+315 -90

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2
PKG_RELEASE:=159
PKG_RELEASE:=160
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -390,7 +390,7 @@ function gen_outbound(flag, node, tag, proxy_table)
}
if node.protocol == "wireguard" then
result.settings.kernelMode = false
result.settings.noKernelTun = true
if node.finalmask and node.finalmask ~= "" then
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
if ok and type(fm) == "table" then
+182
View File
@@ -0,0 +1,182 @@
# Performance — Animation & Runtime
How to verify and quantify the theme's runtime (animation/interaction) performance.
> The essence of an animation optimization is moving work **off the main thread**: from
> per-frame **Layout/Paint** onto the **compositor** (`transform` / `opacity` / `clip-path` /
> `scale`). So the single most important question this guide answers is:
>
> **"Does the main thread still run `Layout` events while the animation plays?"**
## Where this fits
Frontend performance splits into two unrelated buckets — don't conflate them:
| Bucket | Concern | Relevant here? |
|--------|---------|----------------|
| **Load** | First paint, bundle size (LCP, CSS/JS bytes) | Only for `main.css` size |
| **Runtime** | Interaction & animation smoothness (INP, dropped frames) | ✅ This doc |
Two more axes the industry tracks:
- **Lab vs Field** — *lab* is your local, repeatable profiling (DevTools, Lighthouse).
*Field* is real users (RUM). This doc is lab-only; for field data see
[Preventing regressions](#preventing-regressions).
- **Reality check** — LuCI's assets ship from the router, but **rendering happens in the
user's browser**. The bottleneck is the *client* GPU/CPU. Most users open LuCI on a
**phone**, so always profile under CPU throttle, never on a bare fast desktop.
---
## 0. Setup (one-time)
1. Run the dev server (`cd .dev && pnpm dev`) and open a logged-in page in Chrome/Edge,
e.g. `http://localhost:5173/cgi-bin/luci/admin/status/overview`.
2. Open DevTools (`F12` / `Cmd+Opt+I`).
3. Have two panels ready:
- **Rendering**: `Cmd+Shift+P` → type `Show Rendering`.
- **Performance**: the top tab.
4. Confirm you're serving the optimized CSS: **Network** → reload → click `main.css` →
**Response** → search `prefers-reduced-motion`. If present, it's the new build.
---
## 1. Core evidence — Performance recording (the Layout track)
This is where the real numbers come from. Generic recipe, used for every animation:
1. Performance panel ⚙️ → **CPU: 4× slowdown** (emulates a low-end phone/tablet admin device).
2. ● Record → trigger the animation once → let it finish → Stop.
3. Read the timeline:
- **Purple `Layout` / `Recalculate Style` bars** = reflow. Fewer is better.
- **FPS green bar** collapsing = dropped frames.
- **Summary → Rendering** time at the bottom.
> **Verdict rule:** watch the `Layout` row. The ideal result is that **after the first frame,
> the entire animation produces no new `Layout` events** — it runs purely on the compositor.
### Component → technique → what to check
| Component | Trigger | Technique | Pass criteria |
|-----------|---------|-----------|---------------|
| **Desktop mega-menu** | Hover a top nav item with a submenu (e.g. *System*) | `clip-path` (not `height`) | No continuous `Layout` bars while expanding (old height-anim reflowed every frame → should be ≈0) |
| **Mobile submenu** | Device mode (`Cmd+Shift+M`) → hamburger → tap a parent item | `grid-template-rows` (not JS `scrollHeight`) | No `Layout` bars, **and no single forced-reflow spike** on expand (old code read `scrollHeight`) |
| **Main view (`#view`) hover** | Desktop width, move cursor in/out of the main card repeatedly | hover shadow repaint removed | No `Paint` flashing during the recording (old hover shadow repainted the whole block) |
| **Tooltip** | Hover then mouse-out of a `[data-tooltip]` element | `scale` + `opacity` transition | `scale` and `opacity` fade *together* on hide — no instant size snap |
| **Theme switch** | Footer theme toggle | View Transitions API (not body `transition-all`) | No body-wide repaint storm on the Main track |
---
## 2. Fastest visual evidence — Paint flashing (no recording)
1. Rendering panel → enable **Paint flashing**.
2. Trigger each animation; repainted regions flash **green**.
| Trigger | Pass criteria |
|---------|---------------|
| Open/close mega-menu | The frosted panel should **not** flash green as a solid block continuously (ideally only a faint flash at the reveal edge) |
| Mobile drawer slide in/out | Drawer body **doesn't** flash green continuously (transform slide is repaint-free) |
| Tooltip appear | Only the small tooltip region flashes once |
| Toggle floating toolbar | Button area doesn't flash green continuously |
> ⚠️ **Honest caveat:** elements with `backdrop-blur` (mega-menu panel, modal scrim) **will**
> show some green flashing while animating — that's the inherent cost of a blur layer, not a
> regression. Judge the **reflow-class** animations (height / shadow) on whether they still
> flash, *not* whether blur reaches zero flash.
---
## 3. Accessibility & fallback — `prefers-reduced-motion`
1. Rendering panel → **Emulate CSS media feature prefers-reduced-motion** → `reduce`.
2. Trigger animations:
| Check | Pass criteria |
|-------|---------------|
| Mega-menu / drawer / tooltip | Snap into place, **no transition** |
| Theme switch | Instant, no crossfade |
| Page navigation | `#view` has no entry animation |
| **Functionality** | All menus/modals **still open and close** — nothing stuck hidden |
Reset to `No emulation` when done.
---
## 4. View Transitions — theme switch
1. **Chrome/Edge** (supports View Transitions): click the footer theme toggle (device / light / dark).
- Pass: the **whole page** crossfades smoothly (every element transitions together).
2. **No first-paint flash:** reload the page.
- Pass: **no** theme crossfade on load (the first set is guarded by `prev !== null`).
3. **Graceful degradation:** switch theme in **Firefox** / Safari.
- Pass: instant switch, **no console errors**.
---
## 5. Behavior regression (animation rework didn't break function)
- [ ] Mega-menu: hover opens, leave closes; sweeping between menus closes the old and opens the new with no residue.
- [ ] Mega-menu height: submenu content **fully visible, not clipped** (`--mega-menu-height` computed correctly).
- [ ] Mobile drawer: closes via `Esc` / scrim tap / close button; **scrim fades out** on close.
- [ ] Mobile submenu: opening B auto-closes A; reopening the drawer shows all submenus collapsed.
- [ ] Modal (e.g. *Save & Apply*): scrim still blurs the background; modal content stays sharp.
- [ ] Tooltip: appears on hover, disappears on leave, ~150 ms, feels responsive.
---
## 6. Before/after A/B comparison (real numbers)
Absolute numbers on a fast machine prove nothing — **the delta vs `master` is the result.**
```bash
cd /path/to/luci-theme-aurora
git stash # if you have uncommitted work
git checkout master
cd .dev && pnpm build # old assets
# Hard-refresh the browser (Cmd+Shift+R), record the mega-menu open (§1),
# note the Layout event count / Rendering time.
cd .. && git checkout perf/compositor-animations
cd .dev && pnpm build
# Record again — Layout count should drop noticeably.
```
> The dev server re-proxies the new build automatically; **hard-refresh** each time to clear cache.
---
## Pass criteria summary
| Signal | Tool | Pass criteria |
|--------|------|---------------|
| Main-thread reflow during animation | Performance → Main track | Near-zero `Layout` / `Recalculate Style` after frame 1 |
| Dropped frames | Performance → Frames / FPS meter | No frame > 16.7 ms; steady ~60 fps under 4× throttle |
| Repaint area | Rendering → Paint flashing | Minimal green on reflow-class animations |
| Compositor offload | Rendering → Layer borders | Animated elements promoted to their own layer |
| Interaction latency (INP) | Lighthouse / Performance | No regression; improved where `scrollHeight` was removed |
---
## Preventing regressions
Verifying once isn't enough — keep it from drifting back:
- **CSS size budget**: gate `main.css` size in CI (e.g. [`size-limit`](https://github.com/ai/size-limit))
so a PR that bloats the bundle fails.
- **Field data (optional)**: ship the [`web-vitals`](https://github.com/GoogleChrome/web-vitals)
library to report INP/LCP/CLS from real users, and watch the **P75** (not the average).
- **Rule of thumb for new CSS**: animate only `transform` / `opacity` / `clip-path` / `scale`.
Never animate `height` / `width` / `top` / `box-shadow` / `background-color` (they reflow or
repaint). Avoid blanket `transition-all` — list explicit properties.
- **Accepted exception — `.cbi-progressbar`**: the inner bar's `width` is set via inline style
by LuCI core's `Progressbar` widget, so a `transform: scaleX()` swap would need a JS observer
to mirror that value into a custom property (plus RTL-aware `transform-origin`). Given the bar
updates infrequently (firmware/package install progress, not a 60fps animation), the single
explicit `transition-[width]` is left as-is rather than adding that infrastructure.
---
## The 3 fastest checks (when short on time)
1. **§1** — record a mega-menu open → is the `Layout` row clear?
2. **§2** — Paint flashing → does the mega-menu / mobile drawer still flash as a solid block?
3. **§3** — emulate `reduce` → are animations fully off *and* everything still functional?
+1 -1
View File
@@ -7,7 +7,7 @@ html {
@apply bg-background relative h-full font-sans;
body {
@apply text-foreground relative flex flex-col bg-transparent text-sm leading-relaxed font-normal transition-all duration-300;
@apply text-foreground relative flex flex-col bg-transparent text-sm leading-relaxed font-normal;
&.modal-overlay-active {
@apply h-screen overflow-hidden;
}
+14 -14
View File
@@ -7,16 +7,16 @@ header {
[data-nav-type="mega-menu"] & {
& .desktop-menu-container {
@apply bg-header-bg/40 dark:bg-header-bg/90 pointer-events-none absolute inset-x-0 top-0 z-30 h-0 w-full overflow-hidden opacity-0 backdrop-blur-xl transition-all duration-150 ease-in-out max-md:hidden;
@apply bg-header-bg/40 dark:bg-header-bg/90 pointer-events-none absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden opacity-0 backdrop-blur-xl transition-[clip-path,opacity] duration-200 ease-out [clip-path:inset(0_0_100%_0)] [will-change:clip-path,opacity] max-md:hidden;
&.active {
@apply pointer-events-auto h-(--mega-menu-height,0) opacity-100;
@apply pointer-events-auto opacity-100 [clip-path:inset(0)];
}
}
}
.brand {
@apply hover:text-primary text-foreground inline-block shrink-0 text-xl font-semibold tracking-tight no-underline transition-all duration-200 hover:-translate-y-0.5 max-md:flex-1 max-md:text-lg;
@apply hover:text-primary text-foreground inline-block shrink-0 text-xl font-semibold tracking-tight no-underline transition-[color,transform] duration-200 hover:-translate-y-0.5 max-md:flex-1 max-md:text-lg;
}
.nav {
@@ -26,7 +26,7 @@ header {
@apply relative;
.menu {
@apply text-foreground block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-all duration-150;
@apply text-foreground block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-colors duration-150;
&.menu-active {
@apply text-foreground;
@@ -40,7 +40,7 @@ header {
}
& .desktop-nav {
@apply pointer-events-none left-0 z-40 px-0 pt-0 opacity-0 transition-all duration-150;
@apply pointer-events-none left-0 z-40 px-0 pt-0 opacity-0 transition-opacity duration-150;
[data-nav-type="mega-menu"] & {
@apply fixed top-14 left-1/2 -translate-x-1/2 pb-6;
}
@@ -57,7 +57,7 @@ header {
}
[data-nav-type="boxed-dropdown"] & {
@apply border-border bg-header-bg/90 flex flex-col gap-y-1 rounded-3xl border py-2 shadow-lg backdrop-blur-md backdrop-saturate-150 transition-all duration-200;
@apply border-border bg-header-bg/90 flex flex-col gap-y-1 rounded-3xl border py-2 shadow-lg backdrop-blur-md backdrop-saturate-150;
}
& > li {
@apply m-0 list-none;
@@ -86,7 +86,7 @@ header {
}
& .navigation-toggle {
@apply cursor-pointer transition-all duration-200 hover:scale-105 active:scale-95;
@apply cursor-pointer transition-transform duration-200 hover:scale-105 active:scale-95;
& svg {
@apply text-foreground size-5;
@@ -94,7 +94,7 @@ header {
& .navigation-toggle-line {
transform-box: fill-box;
@apply origin-center transition-[transform,opacity] duration-300 ease-in-out motion-reduce:transition-none;
@apply origin-center transition-[transform,opacity] duration-300 ease-in-out;
}
&.is-expanded {
@@ -152,7 +152,7 @@ body[data-nav-type="sidebar"] {
so collapsing the sidebar never stretches it — lets the sidebar column
animate to zero, and lets #maincontent fluidly reclaim the freed space.
overflow-x-clip keeps the sliding sidebar out of horizontal overflow. */
@apply motion-reduce:transition-none md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip md:transition-[grid-template-columns] md:duration-300 md:ease-out;
@apply md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip md:transition-[grid-template-columns] md:duration-300 md:ease-out;
&.sidebar-collapsed {
@apply md:grid-cols-[0_minmax(0,1fr)];
@@ -187,7 +187,7 @@ body[data-nav-type="sidebar"] {
}
& .sidebar-panel {
@apply border-border/60 sticky top-14 hidden h-[calc(100vh-3.5rem)] w-full overflow-hidden rounded-none border-r transition-[visibility] duration-300 motion-reduce:transition-none md:block;
@apply border-border/60 sticky top-14 hidden h-[calc(100vh-3.5rem)] w-full overflow-hidden rounded-none border-r transition-[visibility] duration-300 md:block;
}
/* Visibility flips at the end of the slide so collapsed controls leave the
@@ -197,7 +197,7 @@ body[data-nav-type="sidebar"] {
}
& .sidebar-panel-inner {
@apply bg-header-bg flex h-full w-68 flex-col overflow-hidden transition-[translate,opacity] duration-300 motion-reduce:transition-none;
@apply bg-header-bg flex h-full w-68 flex-col overflow-hidden transition-[translate,opacity] duration-300;
}
&.sidebar-collapsed .sidebar-panel-inner {
@@ -217,7 +217,7 @@ body[data-nav-type="sidebar"] {
}
& .sidebar-section {
@apply grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none;
@apply grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-200 ease-out;
}
& .sidebar-group-open > .sidebar-section {
@@ -253,7 +253,7 @@ body[data-nav-type="sidebar"] {
@apply max-w-max-width mx-auto min-h-[calc(100vh-4rem)] w-23/24 px-4 max-md:w-full max-md:px-3;
#view {
@apply animate-in fade-in-0 slide-in-from-top-2 fill-mode-both md:border-border/30 md:bg-page-bg md:hover:border-border/50 mx-0 w-full p-0 shadow-none transition-all duration-300 empty:hidden md:rounded-4xl md:border md:p-6 md:shadow-xl md:hover:shadow-2xl;
@apply animate-in fade-in-0 slide-in-from-top-2 fill-mode-both md:border-border/30 md:bg-page-bg mx-0 w-full p-0 shadow-none empty:hidden md:rounded-4xl md:border md:p-6 md:shadow-xl;
.cbi-title-section {
@apply mb-6 leading-relaxed max-md:mx-2 max-md:mb-3;
@@ -336,7 +336,7 @@ footer {
}
& .breadcrumb {
@apply bg-page-bg/60 hover:bg-page-bg/80 flex items-center gap-1 rounded-full px-3 py-1.5 text-xs shadow-sm backdrop-blur-sm transition-all duration-200 hover:shadow-md;
@apply bg-page-bg/60 hover:bg-page-bg/80 flex items-center gap-1 rounded-full px-3 py-1.5 text-xs shadow-sm backdrop-blur-sm transition-colors duration-200;
& li {
@apply list-none;
@@ -0,0 +1,13 @@
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
@apply transition-none! animate-none!;
}
/* Loading indicators communicate in-progress state, not decoration —
keep them spinning so the page doesn't read as frozen. */
.spinning::before {
@apply animate-spin!;
}
}
@@ -19,7 +19,7 @@
}
.zonebadge {
@apply border-secondary bg-secondary text-secondary-foreground inline-flex items-center gap-1.5 overflow-visible rounded-full border px-1 py-1.5 text-sm font-medium shadow-sm hover:shadow-md max-md:gap-1 max-md:text-xs;
@apply border-secondary bg-secondary text-secondary-foreground inline-flex items-center gap-1.5 overflow-visible rounded-full border px-1 py-1.5 text-sm font-medium shadow-sm transition-[box-shadow] duration-200 hover:shadow-md max-md:gap-1 max-md:text-xs;
&[style*="--zone-color-rgb"] {
@apply bg-[rgb(var(--zone-color-rgb),.7)]!;
@@ -35,7 +35,7 @@
}
.ifacebadge {
@apply border-border bg-label-surface text-default-foreground hover:border-border hover:bg-label-surface inline-flex cursor-default flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs;
@apply border-border bg-label-surface text-default-foreground hover:border-border hover:bg-label-surface inline-flex cursor-default flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[box-shadow,border-color,background-color] duration-200 hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs;
.cbi-dropdown &,
.cbi-tooltip & {
@@ -4,7 +4,9 @@ input[type="submit"],
input[type="reset"],
.btn,
.cbi-button {
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-2xl border px-3 py-1.5 text-sm font-medium antialiased shadow-sm max-md:text-base max-md:font-medium;
/* transform stays in this list for the .cbi-button active:scale-95
variant in _form.css, which relies on this base transition. */
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-2xl border px-3 py-1.5 text-sm font-medium antialiased shadow-sm transition-[background-color,border-color,transform] duration-200 max-md:text-base max-md:font-medium;
&[disabled] {
@apply cursor-not-allowed opacity-40 dark:opacity-30;
}
@@ -70,7 +70,7 @@
}
& .ifacebox-head {
@apply border-border bg-label-surface text-foreground max-md:border-border/80 w-full rounded-t-2xl border-b px-4 py-3 text-center font-semibold tracking-tight transition-transform duration-200 max-md:rounded-t-3xl max-md:px-3 max-md:py-2.5 max-md:text-sm max-md:leading-5 max-md:font-medium;
@apply border-border bg-label-surface text-foreground max-md:border-border/80 w-full rounded-t-2xl border-b px-4 py-3 text-center font-semibold tracking-tight transition-[background-color,border-color,color] duration-200 max-md:rounded-t-3xl max-md:px-3 max-md:py-2.5 max-md:text-sm max-md:leading-5 max-md:font-medium;
&[style*="--zone-color-rgb"] {
@apply bg-[rgb(var(--zone-color-rgb),.75)]!;
@@ -2,7 +2,7 @@
@apply fixed right-4 bottom-4 z-40 flex flex-col items-center max-md:right-3 max-md:bottom-3;
.toolbar-list {
@apply visible mb-2 grid origin-bottom grid-rows-[1fr] opacity-100 transition-all duration-400 ease-in-out max-md:mb-1.5;
@apply visible mb-2 grid origin-bottom grid-rows-[1fr] opacity-100 transition-[grid-template-rows,transform,opacity,visibility] duration-300 ease-in-out max-md:mb-1.5;
.toolbar-list-inner {
@apply border-border/70 bg-page-bg/90 flex min-h-0 flex-col items-center gap-2 overflow-hidden rounded-full border p-1 shadow-sm max-md:gap-1.5 max-md:p-0.5;
@@ -10,10 +10,10 @@
}
.toolbar-btn {
@apply flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full p-2 transition-all duration-300 ease-in-out active:scale-95 max-md:h-9 max-md:w-9 max-md:p-1.5;
@apply flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full p-2 transition-[background-color,transform,opacity] duration-200 ease-in-out active:scale-95 max-md:h-9 max-md:w-9 max-md:p-1.5;
.icon {
@apply h-5 w-5 shrink-0 transition-all duration-300 ease-in-out max-md:h-4 max-md:w-4;
@apply h-5 w-5 shrink-0 transition-transform duration-300 ease-in-out max-md:h-4 max-md:w-4;
}
&:not(.toggle) {
@@ -26,14 +26,14 @@
&.toggle {
@apply bg-page-bg border-0 shadow-sm;
.icon {
@apply transition-transform duration-400 ease-out;
@apply transition-transform duration-300 ease-out;
}
}
}
&.collapsed {
.toolbar-list {
@apply pointer-events-none invisible mb-0 translate-y-2 scale-90 grid-rows-[0fr] opacity-0;
@apply pointer-events-none invisible translate-y-2 scale-90 grid-rows-[0fr] opacity-0;
}
.toolbar-btn {
@@ -43,7 +43,7 @@
@apply inline-flex w-max max-w-120 flex-col items-start gap-3 max-md:max-w-full!;
& .item {
@apply border-border/40 bg-panel-bg pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-2xl border py-3 pr-10 pl-4 break-all shadow-sm select-text max-md:w-full max-md:py-2.5 max-md:pr-8 max-md:pl-3;
@apply border-border/40 bg-panel-bg pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-2xl border py-3 pr-10 pl-4 break-all shadow-sm transition-[transform,box-shadow,opacity,border-color,background-color] duration-200 select-text max-md:w-full max-md:py-2.5 max-md:pr-8 max-md:pl-3;
@apply after:bg-muted after:text-muted-foreground after:absolute after:top-2.5 after:right-2 after:inline-flex after:h-6 after:w-6 after:shrink-0 after:cursor-pointer after:items-center after:justify-center after:rounded-xl after:text-sm after:font-medium after:content-['\00D7'];
&.dragging {
@@ -65,7 +65,7 @@
}
& .add-item {
@apply border-border/70 hover:border-border hover:bg-page-bg/50 inline-flex items-center gap-1.5 self-stretch rounded-2xl border border-dashed bg-transparent px-1 py-1;
@apply border-border/70 hover:border-border hover:bg-page-bg/50 inline-flex items-center gap-1.5 self-stretch rounded-2xl border border-dashed bg-transparent px-1 py-1 transition-[border-color,background-color] duration-200;
& > input {
@apply min-w-61 flex-1;
@@ -6,7 +6,7 @@ input[type="text"],
input[type="password"],
.cbi-input-text,
.cbi-input {
@apply text-foreground border-border/70 bg-page-bg placeholder-muted-foreground/70 focus:border-primary focus:ring-primary/20 relative rounded-2xl border px-3 py-1.5 text-sm font-normal shadow-sm focus:ring-2 focus:outline-none;
@apply text-foreground border-border/70 bg-page-bg placeholder-muted-foreground/70 focus:border-primary focus:ring-primary/20 relative rounded-2xl border px-3 py-1.5 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
.table.cbi-section-table & {
@apply w-full;
@@ -23,7 +23,7 @@ input[type="password"],
input[type="radio"],
input[type="checkbox"] {
@apply focus:before:border-primary focus:before:ring-primary/20 checked:before:border-primary checked:before:bg-primary before:border-border/70 before:bg-page-bg after:bg-input-checked hover:before:border-border relative mr-3 inline-block h-4 w-4 cursor-pointer appearance-none before:absolute before:top-0 before:left-0 before:h-4 before:w-4 before:border after:absolute after:top-0.5 after:left-0.5 after:h-3 after:w-3 after:opacity-0 after:transition-opacity after:duration-200 checked:after:opacity-100 focus:before:ring-2 focus:before:outline-none disabled:cursor-not-allowed;
@apply focus:before:border-primary focus:before:ring-primary/20 checked:before:border-primary checked:before:bg-primary before:border-border/70 before:bg-page-bg after:bg-input-checked hover:before:border-border relative mr-3 inline-block h-4 w-4 cursor-pointer appearance-none before:absolute before:top-0 before:left-0 before:h-4 before:w-4 before:border before:transition-[background-color,border-color,box-shadow] before:duration-200 after:absolute after:top-0.5 after:left-0.5 after:h-3 after:w-3 after:opacity-0 after:transition-opacity after:duration-200 checked:after:opacity-100 focus:before:ring-2 focus:before:outline-none disabled:cursor-not-allowed;
}
input[type="radio"] {
@@ -6,7 +6,7 @@
}
& > .modal {
@apply border-border/70 bg-page-bg flex w-5xl flex-col gap-4 rounded-3xl border p-6 wrap-break-word whitespace-normal shadow-2xl backdrop-blur-lg backdrop-saturate-150 max-md:w-full max-md:gap-3 max-md:p-4;
@apply border-border/70 bg-page-bg relative flex w-5xl flex-col gap-4 rounded-3xl border p-6 wrap-break-word whitespace-normal shadow-2xl max-md:w-full max-md:gap-3 max-md:p-4;
& h4 {
@apply mb-0 text-center;
@@ -25,7 +25,7 @@
/* Trailing chevron shares arrow-right.svg with the collapsible cards:
points right when closed, rotates to point down when open. */
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-200 after:content-[''] after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] hover:after:opacity-100 motion-reduce:after:transition-none;
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-200 after:content-[''] after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] hover:after:opacity-100;
}
.nav-category-label {
@@ -1,5 +1,5 @@
.mobile-menu-overlay {
@apply max-md:bg-header-bg max-md:invisible max-md:fixed max-md:top-14 max-md:right-0 max-md:bottom-0 max-md:left-0 max-md:z-60 max-md:opacity-0 max-md:transition-opacity max-md:duration-300 md:hidden;
@apply max-md:bg-header-bg max-md:invisible max-md:fixed max-md:top-14 max-md:right-0 max-md:bottom-0 max-md:left-0 max-md:z-60 max-md:opacity-0 max-md:transition-[visibility,opacity] max-md:duration-300 md:hidden;
&.mobile-menu-open {
@apply max-md:visible max-md:opacity-100;
@@ -23,7 +23,7 @@
@apply max-md:shrink-0;
& .mobile-nav-link {
@apply max-md:text-foreground max-md:hover:text-primary max-md:flex max-md:min-h-12 max-md:w-full max-md:items-center max-md:justify-between max-md:gap-4 max-md:px-0 max-md:py-2 max-md:text-lg max-md:leading-tight max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200 max-md:motion-reduce:transition-none;
@apply max-md:text-foreground max-md:hover:text-primary max-md:flex max-md:min-h-12 max-md:w-full max-md:items-center max-md:justify-between max-md:gap-4 max-md:px-0 max-md:py-2 max-md:text-lg max-md:leading-tight max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200;
}
& .mobile-nav-link.nav-link-active {
@@ -40,7 +40,7 @@
via mask, pointing right when collapsed, down when expanded.
justify-between on the link pushes it to the row's end. */
&.has-submenu > .mobile-nav-link {
@apply max-md:after:size-4 max-md:after:shrink-0 max-md:after:bg-current max-md:after:opacity-55 max-md:after:transition-transform max-md:after:duration-200 max-md:after:content-[''] max-md:after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] max-md:motion-reduce:after:transition-none;
@apply max-md:after:size-4 max-md:after:shrink-0 max-md:after:bg-current max-md:after:opacity-55 max-md:after:transition-transform max-md:after:duration-200 max-md:after:content-[''] max-md:after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat];
}
&.submenu-expanded > .mobile-nav-link {
@@ -56,14 +56,22 @@
}
& .mobile-nav-submenu {
@apply max-md:m-0 max-md:list-none max-md:overflow-hidden max-md:p-0 max-md:transition-[max-height,opacity] max-md:duration-300 max-md:ease-in-out max-md:motion-reduce:transition-none;
@apply max-md:grid max-md:grid-rows-[0fr] max-md:opacity-0 max-md:transition-[grid-template-rows,opacity] max-md:duration-300 max-md:ease-in-out;
& .mobile-nav-submenu-list {
@apply max-md:m-0 max-md:min-h-0 max-md:list-none max-md:overflow-hidden max-md:p-0;
}
& .mobile-nav-subitem {
& .mobile-nav-sublink {
@apply max-md:text-default-foreground max-md:hover:text-primary max-md:flex max-md:min-h-10 max-md:w-full max-md:items-center max-md:py-2 max-md:pr-0 max-md:pl-4 max-md:text-[0.9375rem] max-md:font-normal max-md:no-underline max-md:transition-colors max-md:duration-200 max-md:motion-reduce:transition-none;
@apply max-md:text-default-foreground max-md:hover:text-primary max-md:flex max-md:min-h-10 max-md:w-full max-md:items-center max-md:py-2 max-md:pr-0 max-md:pl-4 max-md:text-[0.9375rem] max-md:font-normal max-md:no-underline max-md:transition-colors max-md:duration-200;
}
}
}
&.submenu-expanded .mobile-nav-submenu {
@apply max-md:grid-rows-[1fr] max-md:opacity-100;
}
}
}
@@ -75,7 +83,7 @@
}
& .mobile-nav-logout {
@apply max-md:text-foreground max-md:hover:text-primary max-md:text-sm max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200 max-md:motion-reduce:transition-none;
@apply max-md:text-foreground max-md:hover:text-primary max-md:text-sm max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200;
}
& .theme-switcher {
@@ -5,6 +5,6 @@
}
& > div {
@apply from-progress-bar-start to-progress-bar-end h-full bg-linear-to-r transition-all duration-300;
@apply from-progress-bar-start to-progress-bar-end h-full bg-linear-to-r transition-[width] duration-300;
}
}
@@ -1,5 +1,5 @@
select {
@apply text-foreground border-border/70 bg-page-bg focus:border-primary focus:ring-primary/20 appearance-none rounded-2xl border px-3 py-1.5 pr-10 text-sm font-normal shadow-sm focus:ring-2 focus:outline-none;
@apply text-foreground border-border/70 bg-page-bg focus:border-primary focus:ring-primary/20 appearance-none rounded-2xl border px-3 py-1.5 pr-10 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
@apply bg-[url('@assets/icons/arrow-down.svg')] bg-size-[16px] bg-position-[right_0.75rem_center] bg-no-repeat dark:bg-[url('@assets/icons/arrow-down-dark.svg')];
&[disabled] {
@apply cursor-not-allowed opacity-40 dark:opacity-30;
@@ -15,7 +15,7 @@
}
& .theme-icon {
@apply text-muted-foreground flex items-center justify-center transition-transform duration-300;
@apply text-muted-foreground flex items-center justify-center transition-colors duration-300;
& svg {
@apply size-4 transition-transform duration-300;
@@ -35,7 +35,7 @@ table.table,
& tr,
& .tr {
@apply hover:bg-label-surface max-md:border-border max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
@apply hover:bg-label-surface transition-colors duration-200 max-md:border-border max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
@apply has-[.cbi-progressbar]:max-md:flex has-[.cbi-progressbar]:max-md:items-center has-[.cbi-progressbar]:max-md:justify-center;
&:first-child {
@@ -1,5 +1,5 @@
textarea {
@apply text-foreground border-border/70 bg-page-bg placeholder-muted-foreground/70 focus:border-primary focus:ring-primary/20 min-h-24 w-full resize-y rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm focus:ring-2 focus:outline-none;
@apply text-foreground border-border/70 bg-page-bg placeholder-muted-foreground/70 focus:border-primary focus:ring-primary/20 min-h-24 w-full resize-y rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
&[disabled] {
@apply cursor-not-allowed opacity-40 dark:opacity-30;
}
@@ -1,5 +1,5 @@
.cbi-tooltip {
@apply bg-tooltip-bg text-foreground border-border/60 absolute z-110 max-w-xs scale-95 transform rounded-xl border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-lg backdrop-blur-xl transition-all duration-300 ease-out;
@apply bg-tooltip-bg text-foreground border-border/60 absolute z-110 max-w-xs scale-95 rounded-xl border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-lg transition-[visibility,opacity,scale] duration-150 ease-out;
&.error {
@apply border-error/40 bg-error text-error-foreground;
}
@@ -16,10 +16,10 @@
.cbi-tooltip-container,
[data-tooltip] {
@apply relative inline-block cursor-help whitespace-nowrap underline decoration-current decoration-dotted underline-offset-2 transition-all duration-200;
@apply relative inline-block cursor-help whitespace-nowrap underline decoration-current decoration-dotted underline-offset-2;
& .cbi-tooltip {
@apply invisible scale-95 transform opacity-0 transition-all duration-300 ease-out;
@apply invisible scale-95 opacity-0 transition-[visibility,opacity,scale] duration-150 ease-out;
}
&:hover .cbi-tooltip {
@@ -6,6 +6,7 @@
@import "tailwindcss";
@import "./_tokens.css";
@import "./_reduced-motion.css";
@custom-variant dark (&:where([data-darkmode=true], [data-darkmode=true] *));
@@ -40,3 +40,4 @@
@import "./_utilities.css";
@import "./_patches.css";
@import "./_reduced-motion.css";
@@ -36,28 +36,13 @@ return baseclass.extend({
const resetMobileSubmenus = () => {
document
.querySelectorAll(".mobile-nav-item.submenu-expanded")
.forEach((item) => {
item.classList.remove("submenu-expanded");
const submenu = item.querySelector(".mobile-nav-submenu");
if (submenu) {
submenu.style.maxHeight = "0";
submenu.style.opacity = "0";
}
});
.forEach((item) => item.classList.remove("submenu-expanded"));
};
// Relies on the drawer being hidden via visibility/opacity (not
// display:none): the submenu stays laid out, so scrollHeight is
// measurable in the same tick the drawer opens.
const expandActiveMobileGroup = () => {
const item = document.querySelector(".mobile-nav-item.has-active");
const submenu = item?.querySelector(".mobile-nav-submenu");
if (!item || !submenu) return;
item.classList.add("submenu-expanded");
submenu.style.maxHeight = `${submenu.scrollHeight}px`;
submenu.style.opacity = "1";
document
.querySelector(".mobile-nav-item.has-active")
?.classList.add("submenu-expanded");
};
const updateToggleState = (expanded) => {
@@ -165,21 +150,10 @@ return baseclass.extend({
document
.querySelectorAll(".mobile-nav-item.submenu-expanded")
.forEach((i) => {
if (i !== item) {
i.classList.remove("submenu-expanded");
const s = i.querySelector(".mobile-nav-submenu");
if (s) {
s.style.maxHeight = "0";
s.style.opacity = "0";
}
}
if (i !== item) i.classList.remove("submenu-expanded");
});
item.classList.toggle("submenu-expanded", !isExpanded);
submenu.style.maxHeight = isExpanded
? "0"
: `${submenu.scrollHeight}px`;
submenu.style.opacity = isExpanded ? "0" : "1";
}
});
},
@@ -237,10 +211,8 @@ return baseclass.extend({
if (hasSubmenu) {
li.appendChild(this.buildCategoryPreview(child.name, submenu));
const ul = E("ul", {
class: "mobile-nav-submenu",
style: "max-height: 0; opacity: 0;",
});
const wrap = E("div", { class: "mobile-nav-submenu" });
const ul = E("ul", { class: "mobile-nav-submenu-list" });
submenu.forEach((item) => {
const subActive = this.isActivePath(child.name, item.name);
@@ -261,7 +233,8 @@ return baseclass.extend({
);
});
li.appendChild(ul);
wrap.appendChild(ul);
li.appendChild(wrap);
}
list.appendChild(li);
@@ -687,10 +660,46 @@ return baseclass.extend({
hideDesktopNav() {
this.deactivateDesktopNavExcept(null, null);
document
.querySelector(".desktop-menu-container")
?.classList.remove("active");
const container = document.querySelector(".desktop-menu-container");
document.querySelector(".desktop-menu-overlay")?.classList.remove("active");
if (!container) return;
const wasActive = container.classList.contains("active");
container.classList.remove("active");
// --mega-menu-height is set per-submenu in initMegaMenu and otherwise
// never cleared. Drop it back to the h-0 fallback once the container is
// fully hidden, so a closed menu doesn't leave invisible scrollable
// space below the header on pages shorter than the last submenu.
const resetHeight = () => {
if (!container.classList.contains("active")) {
container.style.removeProperty("--mega-menu-height");
}
};
// If the menu was never open, removing "active" is a no-op: clip-path
// never changes, so transitionend/transitioncancel would never fire and
// these listeners would pile up on every header mouseleave.
if (
!wasActive ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
resetHeight();
return;
}
const onClipPathSettled = (event) => {
if (event.target !== container || event.propertyName !== "clip-path") {
return;
}
container.removeEventListener("transitionend", onClipPathSettled);
container.removeEventListener("transitioncancel", onClipPathSettled);
resetHeight();
};
container.addEventListener("transitionend", onClipPathSettled);
container.addEventListener("transitioncancel", onClipPathSettled);
},
renderModeMenu(tree) {
+1 -1
View File
@@ -9,7 +9,7 @@ LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CS
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.12.6
PKG_RELEASE:=23
PKG_RELEASE:=24
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@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-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--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-font-weight:initial;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base)*2);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,: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:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-Line truncated
@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-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:"";--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base)*2);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,: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:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkitLine truncated
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@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-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-tracking:initial;--tw-border-style:solid;--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-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:"";--tw-space-y-reverse:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-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}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-2xl:calc(var(--radius-base)*2);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,: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{lLine truncated
@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-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-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-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:"";--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}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-2xl:calc(var(--radius-base)*2);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,: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}Line truncated
@@ -1 +1 @@
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const resetMobileSubmenus=()=>{document.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(item=>{item.classList.remove("submenu-expanded");const submenu=item.querySelector(".mobile-nav-submenu");if(submenu){submenu.style.maxHeight="0";submenu.style.opacity="0"}})};const expandActiveMobileGroup=()=>{const item=document.querySelector(".mobile-nav-item.has-active");const submenu=item?.querySelector(".mobile-nav-submenu");if(!item||!submenu)return;item.classList.add("submenu-expanded");submenu.style.maxHeight=`${submenu.scrollHeight}px`;submenu.style.opacity="1"};const updateToggleState=expanded=>{navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.style.overflow="";resetMobileSubmenus()};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)expandActiveMobileGroup();else resetMobileSubmenus();updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const link=e.target.closest(".mobile-nav-link");if(!link)return;const item=link.closest(".mobile-nav-item");const submenu=item?.querySelector(".mobile-nav-submenu");if(submenu){e.preventDefault();e.stopPropagation();const isExpanded=item.classList.contains("submenu-expanded");document.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(i=>{if(i!==item){i.classList.remove("submenu-expanded");const s=i.querySelector(".mobile-nav-submenu");if(s){s.style.maxHeight="0";s.style.opacity="0"}}});item.classList.toggle("submenu-expanded",!isExpanded);submenu.style.maxHeight=isExpanded?"0":`${submenu.scrollHeight}px`;submenu.style.opacity=isExpanded?"0":"1"}})},renderMobileMenu(tree,url){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");const children=ui.menu.getChildren(tree);if(!list||!footerAction||!children.length)return;list.innerHTML="";footerAction.innerHTML="";children.forEach(child=>{const submenu=ui.menu.getChildren(child);const hasSubmenu=submenu.length>0;const isActive=this.isActivePath(child.name);if(child.name==="logout"){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:L.url(url,child.name)},[_(child.title)]));return}const itemClasses=["mobile-nav-item",hasSubmenu?"has-submenu":"",isActive&&hasSubmenu?"has-active":""].filter(Boolean);const li=E("li",{class:itemClasses.join(" ")},[E("a",{class:`mobile-nav-link${isActive&&!hasSubmenu?" nav-link-active":""}`,href:hasSubmenu?"#":L.url(url,child.name)},[_(child.title)])]);if(hasSubmenu){li.appendChild(this.buildCategoryPreview(child.name,submenu));const ul=E("ul",{class:"mobile-nav-submenu",style:"max-height: 0; opacity: 0;"});submenu.forEach(item=>{const subActive=this.isActivePath(child.name,item.name);ul.appendChild(E("li",{class:"mobileLine truncated
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const resetMobileSubmenus=()=>{document.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(item=>item.classList.remove("submenu-expanded"))};const expandActiveMobileGroup=()=>{document.querySelector(".mobile-nav-item.has-active")?.classList.add("submenu-expanded")};const updateToggleState=expanded=>{navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.style.overflow="";resetMobileSubmenus()};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)expandActiveMobileGroup();else resetMobileSubmenus();updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const link=e.target.closest(".mobile-nav-link");if(!link)return;const item=link.closest(".mobile-nav-item");const submenu=item?.querySelector(".mobile-nav-submenu");if(submenu){e.preventDefault();e.stopPropagation();const isExpanded=item.classList.contains("submenu-expanded");document.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(i=>{if(i!==item)i.classList.remove("submenu-expanded")});item.classList.toggle("submenu-expanded",!isExpanded)}})},renderMobileMenu(tree,url){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");const children=ui.menu.getChildren(tree);if(!list||!footerAction||!children.length)return;list.innerHTML="";footerAction.innerHTML="";children.forEach(child=>{const submenu=ui.menu.getChildren(child);const hasSubmenu=submenu.length>0;const isActive=this.isActivePath(child.name);if(child.name==="logout"){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:L.url(url,child.name)},[_(child.title)]));return}const itemClasses=["mobile-nav-item",hasSubmenu?"has-submenu":"",isActive&&hasSubmenu?"has-active":""].filter(Boolean);const li=E("li",{class:itemClasses.join(" ")},[E("a",{class:`mobile-nav-link${isActive&&!hasSubmenu?" nav-link-active":""}`,href:hasSubmenu?"#":L.url(url,child.name)},[_(child.title)])]);if(hasSubmenu){li.appendChild(this.buildCategoryPreview(child.name,submenu));const wrap=E("div",{class:"mobile-nav-submenu"});const ul=E("ul",{class:"mobile-nav-submenu-list"});submenu.forEach(item=>{const subActive=this.isActivePath(child.name,item.name);ul.appendChild(E("li",{class:"mobile-nav-subitem"},[E("a",{class:`mobile-nav-sublink${subActive?" nav-link-active":""}`,href:L.url(url,child.name,item.name)},[_(item.title)])]))});wrap.appendChild(ul);li.appendChild(wrap)}list.appendChild(li)})},render(tree){this.renderModeMenu(tree);if(L.env.dispatchpath.length>=3){let node=tree;let url="";for(let i=0;i<3&&node;i++){const segment=L.env.dispatchpath[i];node=node.children?.[segment];url+=(url?"/":"")+segment}if(node)this.renderTabMenu(node,url)}},renderTabMenu(tree,url,Line truncated
@@ -71,8 +71,17 @@
const setTheme = (theme) => {
const current = ['light', 'dark', 'device'].includes(theme) ? theme : 'device';
const isDark = current === 'dark' || (current === 'device' && window.matchMedia('(prefers-color-scheme: dark)').matches);
const next = String(isDark);
const prev = document.documentElement.getAttribute('data-darkmode');
const apply = () => document.documentElement.setAttribute('data-darkmode', next);
if (prev !== null && prev !== next && document.startViewTransition &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.startViewTransition(apply);
} else {
apply();
}
document.documentElement.setAttribute('data-darkmode', isDark);
localStorage.setItem('aurora.theme', current);
};