🎈 Sync 2026-06-16 01:15:58
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.12.0
|
||||
PKG_RELEASE:=9
|
||||
PKG_RELEASE:=10
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
|
||||
@ -139,9 +139,9 @@ return view.extend({
|
||||
]);
|
||||
};
|
||||
|
||||
// run a backgrounded script, stream its log, then toast the result.
|
||||
// Scripts end their log with "done (rc=N)"; poll until that appears.
|
||||
const runJob = function(script, arg, btn, logPath, label) {
|
||||
// run a backgrounded script and stream its log into the log pane until it
|
||||
// logs "done (rc=N)"; then refresh the badges. Result shows inline (no popup).
|
||||
const runJob = function(script, arg, btn, logPath) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
@ -149,20 +149,13 @@ return view.extend({
|
||||
const poll = function() {
|
||||
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
||||
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
|
||||
const m = c.match(/done \(rc=(\d+)\)/);
|
||||
if (m) {
|
||||
const ok = m[1] === '0';
|
||||
ui.addNotification(null, E('p', label + ' · ' + (ok ? _('done') : _('failed'))), ok ? null : 'danger');
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
if (/done \(rc=\d+\)/.test(c)) { refresh(); return; }
|
||||
if (tries++ > 90) return;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
||||
});
|
||||
};
|
||||
return fs.exec('/usr/share/luci-app-daede/' + script, [arg]).then(function(res) {
|
||||
if (res.code !== 0) { ui.addNotification(null, E('p', label + ' · ' + _('failed to start')), 'danger'); return; }
|
||||
return poll();
|
||||
if (res.code === 0) return poll();
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
@ -171,16 +164,12 @@ return view.extend({
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
return runJob('update-geo.sh', kind, btn,
|
||||
'/tmp/luci-app-daede.' + kind + '.log', kind === 'geoip' ? 'GeoIP' : 'GeoSite');
|
||||
return runJob('update-geo.sh', kind, btn, '/tmp/luci-app-daede.' + kind + '.log');
|
||||
};
|
||||
|
||||
// === Package updates (dae|daed / luci-app-daede) ===
|
||||
const upgradePkg = function(pkg, btn) {
|
||||
if (!confirm(_('Upgrade %s now? This may restart the active backend.').format(pkg)))
|
||||
return;
|
||||
return runJob('update-pkg.sh', pkg, btn,
|
||||
'/tmp/luci-app-daede.pkg-' + pkg + '.log', pkg);
|
||||
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
|
||||
};
|
||||
|
||||
const refresh = function() {
|
||||
|
||||
@ -10,8 +10,9 @@ fi
|
||||
: > "$LOCK"
|
||||
|
||||
(
|
||||
# -n: skip if a package op holds the shared apk lock (no need to refresh then)
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk update
|
||||
flock -n /tmp/luci-app-daede.apk.lock apk update
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
opkg update
|
||||
fi
|
||||
|
||||
@ -38,15 +38,22 @@ fi
|
||||
echo "$(date '+%F %T') begin upgrade: $PKG"
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo "--- apk update ---"
|
||||
apk update 2>&1
|
||||
echo "--- apk add $PKG ---"
|
||||
# Target only $PKG (not `apk upgrade`, which re-solves the whole world).
|
||||
apk add "$PKG" 2>&1
|
||||
# Don't trust apk's exit code: apk-tools 3 returns non-zero whenever ANY
|
||||
# unrelated installed package has an unavailable .apk in the configured
|
||||
# feeds, even when $PKG itself upgraded fine. Judge success by state —
|
||||
# $PKG must be installed and have no pending upgrade left.
|
||||
# shared apk lock with the bg index refresh (avoid "Unable to lock database")
|
||||
(
|
||||
flock 9
|
||||
apk update 2>&1
|
||||
# pin exact latest version + --force-broken-world so unrelated broken
|
||||
# packages can't block this upgrade (cf. clashoo component_update)
|
||||
ver=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '$1 ~ "^" p "-[0-9]" { v=$1; sub("^" p "-", "", v); print v }' | sort -V | tail -1)
|
||||
if [ -n "$ver" ]; then
|
||||
echo "--- apk add $PKG=$ver ---"
|
||||
apk add "$PKG=$ver" --force-broken-world 2>&1
|
||||
else
|
||||
echo "--- apk add $PKG ---"
|
||||
apk add "$PKG" --force-broken-world 2>&1
|
||||
fi
|
||||
) 9>/tmp/luci-app-daede.apk.lock
|
||||
# apk's exit code is unreliable (broken-world noise); judge by state instead
|
||||
if ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
|
||||
echo "result: $PKG is not installed"
|
||||
rc=1
|
||||
|
||||
1
luci-theme-aurora/.dev/src/assets/icons/activity.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3 8l4 -16l3 8h4" /></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
1
luci-theme-aurora/.dev/src/assets/icons/adjustments.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M6 4v4" /><path d="M6 12v8" /><path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M12 4v10" /><path d="M12 18v2" /><path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M18 4v1" /><path d="M18 9v11" /></svg>
|
||||
|
After Width: | Height: | Size: 452 B |
1
luci-theme-aurora/.dev/src/assets/icons/apps.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /><path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /><path d="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /><path d="M14 7l6 0" /><path d="M17 4l0 6" /></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
1
luci-theme-aurora/.dev/src/assets/icons/category.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h6v6h-6l0 -6" /><path d="M14 4h6v6h-6l0 -6" /><path d="M4 14h6v6h-6l0 -6" /><path d="M14 17a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
1
luci-theme-aurora/.dev/src/assets/icons/cpu.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1l0 -12" /><path d="M9 9h6v6h-6l0 -6" /><path d="M3 10h2" /><path d="M3 14h2" /><path d="M10 3v2" /><path d="M14 3v2" /><path d="M21 10h-2" /><path d="M21 14h-2" /><path d="M14 21v-2" /><path d="M10 21v-2" /></svg>
|
||||
|
After Width: | Height: | Size: 478 B |
1
luci-theme-aurora/.dev/src/assets/icons/package.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" /><path d="M12 12l8 -4.5" /><path d="M12 12l0 9" /><path d="M12 12l-8 -4.5" /><path d="M16 5.25l-8 4.5" /></svg>
|
||||
|
After Width: | Height: | Size: 347 B |
1
luci-theme-aurora/.dev/src/assets/icons/router.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -4" /><path d="M17 17l0 .01" /><path d="M13 17l0 .01" /><path d="M15 13l0 -2" /><path d="M11.75 8.75a4 4 0 0 1 6.5 0" /><path d="M8.5 6.5a8 8 0 0 1 13 0" /></svg>
|
||||
|
After Width: | Height: | Size: 430 B |
1
luci-theme-aurora/.dev/src/assets/icons/server-2.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-2" /><path d="M3 15a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3l0 -2" /><path d="M7 8l0 .01" /><path d="M7 16l0 .01" /><path d="M11 8h6" /><path d="M11 16h6" /></svg>
|
||||
|
After Width: | Height: | Size: 457 B |
1
luci-theme-aurora/.dev/src/assets/icons/settings.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg>
|
||||
|
After Width: | Height: | Size: 770 B |
1
luci-theme-aurora/.dev/src/assets/icons/tag.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 7.5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M3 6v5.172a2 2 0 0 0 .586 1.414l7.71 7.71a2.41 2.41 0 0 0 3.408 0l5.592 -5.592a2.41 2.41 0 0 0 0 -3.408l-7.71 -7.71a2 2 0 0 0 -1.414 -.586h-5.172a3 3 0 0 0 -3 3" /></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
1
luci-theme-aurora/.dev/src/assets/icons/world.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@ -1,5 +1,5 @@
|
||||
header {
|
||||
@apply bg-bg sticky top-0 z-60 mb-2;
|
||||
@apply bg-bg sticky top-0 z-40 mb-2;
|
||||
|
||||
& .header-content {
|
||||
@apply relative flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2;
|
||||
@ -16,84 +16,130 @@ header {
|
||||
/* Category panels live inside the container, not the nav items: the
|
||||
transformed #topmenu would otherwise become the containing block of
|
||||
a position:fixed panel and shrink the canvas to the menu's width.
|
||||
Fixed coordinates shared by every category, so switching panels
|
||||
never shifts anything: link columns start at 50% - 13rem (just
|
||||
under the centered nav triggers), the anchor column sits at a
|
||||
fixed slot left of them. The entering panel floats up 6px —
|
||||
transform + opacity only. */
|
||||
Each panel is the same centered three-column grid (max-width fixed),
|
||||
so switching categories is a pure cross-fade with no width shift.
|
||||
The entering panel floats up 6px — transform + opacity only. */
|
||||
/* The server-rendered board is only a template that menu-aurora.js
|
||||
clones into each panel's right column — the original stays hidden. */
|
||||
& > .desktop-menu-board {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
& .desktop-nav {
|
||||
@apply pointer-events-none absolute inset-x-0 top-14 min-h-60 translate-y-1.5 pt-7 pb-8 pl-[calc(50%-13rem)] opacity-0 transition-[opacity,transform] duration-[250ms] ease-out;
|
||||
/* Centered three-column canvas: anchor / links / device board.
|
||||
Fixed max-width keeps every category the same width, so switching
|
||||
is a pure cross-fade. Below lg the side tracks drop and only the
|
||||
links column remains (tablet → just the submenu). The panel scrolls
|
||||
internally when its content exceeds the viewport below the header. */
|
||||
@apply pointer-events-none absolute inset-x-0 top-14 mx-auto grid max-h-[calc(100dvh-3.5rem)] max-w-[72rem] translate-y-1.5 grid-cols-[12rem_minmax(0,1fr)_13rem] gap-x-8 overflow-y-auto overscroll-contain px-10 pt-7 pb-8 opacity-0 transition-[opacity,transform] duration-[250ms] ease-out max-lg:max-w-[44rem] max-lg:grid-cols-1 max-lg:px-8;
|
||||
|
||||
&.active {
|
||||
@apply pointer-events-auto translate-y-0 opacity-100;
|
||||
}
|
||||
|
||||
& .desktop-nav-anchor {
|
||||
@apply absolute top-7 left-[calc(50%-30rem)] w-56 max-lg:hidden;
|
||||
@apply self-start max-lg:hidden;
|
||||
|
||||
& .desktop-nav-title {
|
||||
@apply text-text text-2xl font-semibold tracking-tight;
|
||||
@apply text-text flex items-center gap-2.5 text-2xl font-semibold tracking-tight;
|
||||
|
||||
/* First-level icon: mask + currentColor so it follows the theme.
|
||||
--menu-icon is set per node name below; unmapped → default. */
|
||||
&::before {
|
||||
@apply size-6 shrink-0 bg-current content-[''] [mask:var(--menu-icon,url('@assets/icons/category.svg'))_center/contain_no-repeat];
|
||||
}
|
||||
}
|
||||
|
||||
/* name → icon map (the only maintenance point). Keyed on the LuCI
|
||||
node name, not the translated title. Add new first-level
|
||||
sections here; anything unmapped keeps the default icon. */
|
||||
& .desktop-nav-title[data-section="status"] {
|
||||
@apply [--menu-icon:url('@assets/icons/activity.svg')];
|
||||
}
|
||||
& .desktop-nav-title[data-section="system"] {
|
||||
@apply [--menu-icon:url('@assets/icons/settings.svg')];
|
||||
}
|
||||
& .desktop-nav-title[data-section="services"] {
|
||||
@apply [--menu-icon:url('@assets/icons/apps.svg')];
|
||||
}
|
||||
& .desktop-nav-title[data-section="nas"] {
|
||||
@apply [--menu-icon:url('@assets/icons/server-2.svg')];
|
||||
}
|
||||
& .desktop-nav-title[data-section="control"] {
|
||||
@apply [--menu-icon:url('@assets/icons/adjustments.svg')];
|
||||
}
|
||||
& .desktop-nav-title[data-section="network"] {
|
||||
@apply [--menu-icon:url('@assets/icons/world.svg')];
|
||||
}
|
||||
}
|
||||
|
||||
& .desktop-nav-list {
|
||||
/* Vertical fill: columns of equal height, no orphan rows. Row
|
||||
count is set per submenu via --menu-rows in menu-aurora.js.
|
||||
w-max is load-bearing: without it the block-level list fills
|
||||
the viewport and justify-content:normal stretches the auto
|
||||
column tracks across all of it. */
|
||||
@apply grid w-max grid-flow-col grid-rows-[repeat(var(--menu-rows,6),auto)] content-start gap-x-8 gap-y-1;
|
||||
/* Vertical fill, content-width columns. Row count per submenu via
|
||||
--menu-rows (menu-aurora.js); more items grow rows, not width,
|
||||
so the list stays inside the middle track. */
|
||||
@apply grid grid-flow-col grid-rows-[repeat(var(--menu-rows,6),auto)] content-start justify-start gap-x-8 gap-y-1;
|
||||
|
||||
& > li {
|
||||
@apply m-0 list-none;
|
||||
|
||||
& > a {
|
||||
@apply text-text block w-fit py-2.5 text-sm whitespace-nowrap no-underline hover:underline hover:decoration-2 hover:underline-offset-4;
|
||||
@apply text-text hover:bg-hover-faint block w-fit rounded-lg px-3 py-2 text-sm whitespace-nowrap no-underline transition-colors duration-150;
|
||||
|
||||
&.is-active-page {
|
||||
@apply bg-brand-subtle text-brand font-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Constant part of the canvas, Apple-flyout style: the device
|
||||
summary lives once at container level, pinned just below the
|
||||
fixed category-title slot (header 3.5rem + panel pt 1.75rem +
|
||||
title line 2rem + 1.25rem gap = top-34). Its position never
|
||||
depends on an individual panel's height, and switching categories
|
||||
never re-fades it. */
|
||||
& .desktop-menu-board {
|
||||
@apply absolute top-34 left-[calc(50%-30rem)] flex w-56 flex-col gap-2 max-lg:hidden;
|
||||
/* Right column: device summary, cloned per panel. self-start so it
|
||||
sits at the top regardless of how tall the links column grows. */
|
||||
& .desktop-menu-board {
|
||||
@apply border-hairline flex flex-col gap-2 self-start border-l pl-7 max-lg:hidden;
|
||||
|
||||
& .board-line {
|
||||
@apply text-text-muted flex max-w-full items-center gap-2.5 text-xs;
|
||||
|
||||
&::before {
|
||||
@apply size-4 shrink-0 bg-current content-[''];
|
||||
& .board-label {
|
||||
@apply text-text-subtle mb-0.5 text-[11px] font-medium tracking-wide uppercase;
|
||||
}
|
||||
|
||||
& .board-text {
|
||||
@apply min-w-0 truncate;
|
||||
& .board-line {
|
||||
/* items-start keeps the icon aligned to the FIRST text line when a
|
||||
long value wraps. Icon height (size-4) == text leading (leading-4)
|
||||
so a single line still reads as vertically centered. */
|
||||
@apply text-text-muted flex max-w-full items-start gap-2.5 text-xs;
|
||||
|
||||
&::before {
|
||||
@apply size-4 shrink-0 bg-current content-[''];
|
||||
}
|
||||
|
||||
& .board-text {
|
||||
@apply min-w-0 leading-4 [overflow-wrap:anywhere];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .board-line-host::before {
|
||||
@apply [mask:url('@assets/icons/server.svg')_center/contain_no-repeat];
|
||||
}
|
||||
& .board-line-host::before {
|
||||
@apply [mask:url('@assets/icons/router.svg')_center/contain_no-repeat];
|
||||
}
|
||||
|
||||
& .board-line-model::before {
|
||||
@apply [mask:url('@assets/icons/cpu-chip.svg')_center/contain_no-repeat];
|
||||
}
|
||||
& .board-line-model::before {
|
||||
@apply [mask:url('@assets/icons/cpu.svg')_center/contain_no-repeat];
|
||||
}
|
||||
|
||||
& .board-line-firmware::before {
|
||||
@apply [mask:url('@assets/icons/cube.svg')_center/contain_no-repeat];
|
||||
}
|
||||
& .board-line-firmware::before {
|
||||
@apply [mask:url('@assets/icons/package.svg')_center/contain_no-repeat];
|
||||
}
|
||||
|
||||
& .board-line-kernel::before {
|
||||
@apply [mask:url('@assets/icons/cog.svg')_center/contain_no-repeat];
|
||||
& .board-line-kernel::before {
|
||||
@apply [mask:url('@assets/icons/tag.svg')_center/contain_no-repeat];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-nav-type="mega-menu"] &:has(.desktop-menu-container.active) {
|
||||
@apply z-70;
|
||||
}
|
||||
|
||||
.brand {
|
||||
@apply hover:text-brand text-text inline-block shrink-0 text-xl font-semibold tracking-tight no-underline transition-[color,transform] duration-150 hover:-translate-y-0.5 max-md:flex-1 max-md:text-lg;
|
||||
}
|
||||
@ -105,16 +151,12 @@ header {
|
||||
@apply relative;
|
||||
|
||||
.menu {
|
||||
@apply text-text block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-colors duration-150;
|
||||
@apply text-text hover:bg-hover-faint block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-colors duration-150;
|
||||
|
||||
/* Unified pill for both dropdown modes — the open/active category is a
|
||||
filled brand pill, matching the submenu links and the sidebar. */
|
||||
&.menu-active {
|
||||
@apply text-text;
|
||||
[data-nav-type="mega-menu"] & {
|
||||
@apply underline decoration-2 underline-offset-4;
|
||||
}
|
||||
[data-nav-type="boxed-dropdown"] & {
|
||||
@apply bg-brand-subtle text-brand;
|
||||
}
|
||||
@apply bg-brand-subtle text-brand;
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +182,13 @@ header {
|
||||
@apply text-text block whitespace-nowrap no-underline;
|
||||
|
||||
[data-nav-type="boxed-dropdown"] & {
|
||||
@apply hover:bg-hover-faint mx-2 rounded-xl px-4 py-2;
|
||||
@apply hover:bg-hover-faint mx-2 rounded-xl px-4 py-2 transition-colors duration-150;
|
||||
}
|
||||
|
||||
/* Same active pill as the mega submenu and the top-level
|
||||
trigger — one unified vocabulary across both dropdown modes. */
|
||||
&.is-active-page {
|
||||
@apply bg-brand-subtle text-brand font-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -310,7 +358,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 mx-0 w-full bg-transparent p-0 shadow-none empty:hidden md:p-0;
|
||||
@apply mx-0 w-full bg-transparent p-0 shadow-none empty:hidden md:p-0;
|
||||
|
||||
.cbi-title-section {
|
||||
@apply mb-6 leading-relaxed max-md:mx-2 max-md:mb-3;
|
||||
|
||||
25
luci-theme-aurora/.dev/src/media/_reduced-transparency.css
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Reduced-transparency fallback.
|
||||
*
|
||||
* When the user asks for less transparency, drop the most expensive layer —
|
||||
* backdrop-filter blur — everywhere, and turn the one surface that relies on
|
||||
* "translucent + blur" to stay readable (the mega-menu backdrop) opaque, so
|
||||
* removing the blur doesn't let the page content bleed through it.
|
||||
*
|
||||
* surface-overlay / surface are already opaque, so dropping their blur is
|
||||
* harmless; scrim keeps its translucency on purpose (it's a dimming layer).
|
||||
* --mega-menu-bg points at surface-overlay via var() so it tracks light/dark
|
||||
* automatically (resolved at the point of use, not here).
|
||||
*/
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
/* standard property only — lightningcss adds the -webkit- prefix per browserslist */
|
||||
@apply [backdrop-filter:none]!;
|
||||
}
|
||||
|
||||
:root {
|
||||
--mega-menu-bg: var(--surface-overlay);
|
||||
}
|
||||
}
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
@apply border-hairline bg-surface-overlay absolute left-0 z-60 w-fit min-w-full overflow-y-auto rounded-lg border shadow-xl backdrop-blur-md backdrop-saturate-150;
|
||||
@apply border-hairline bg-surface-overlay absolute left-0 z-50 w-fit min-w-full overflow-y-auto rounded-lg border shadow-xl backdrop-blur-md backdrop-saturate-150;
|
||||
|
||||
& > li {
|
||||
@apply text-text-muted hover:bg-brand-subtle min-h-9 w-full cursor-pointer px-3 py-1.5 font-medium;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.alert-message {
|
||||
@apply bg-surface-sunken text-text border-hairline sticky top-14 z-40 mb-4 rounded-4xl border p-6 max-md:mx-0 max-md:mb-3 max-md:rounded-3xl max-md:p-4;
|
||||
@apply bg-surface-sunken text-text border-hairline sticky top-14 z-30 mb-4 rounded-4xl border p-6 max-md:mx-0 max-md:mb-3 max-md:rounded-3xl max-md:p-4;
|
||||
|
||||
&.modal {
|
||||
@apply static top-auto;
|
||||
|
||||
@ -70,7 +70,7 @@ body.mobile-navigation-open > header {
|
||||
|
||||
[data-nav-type="mega-menu"] {
|
||||
& .desktop-menu-overlay {
|
||||
@apply bg-scrim pointer-events-none fixed inset-0 z-50 opacity-0 transition-opacity duration-[250ms] max-md:hidden;
|
||||
@apply bg-scrim pointer-events-none fixed inset-0 z-60 opacity-0 transition-opacity duration-[250ms] max-md:hidden;
|
||||
|
||||
&.active {
|
||||
@apply pointer-events-auto opacity-100 backdrop-blur-lg backdrop-saturate-150;
|
||||
|
||||
@ -42,3 +42,4 @@
|
||||
@import "./_utilities.css";
|
||||
@import "./_patches.css";
|
||||
@import "./_reduced-motion.css";
|
||||
@import "./_reduced-transparency.css";
|
||||
|
||||
@ -255,11 +255,17 @@ return baseclass.extend({
|
||||
if (!children.length) return E([]);
|
||||
|
||||
children.forEach((child) => {
|
||||
ul.appendChild(
|
||||
E("li", {}, [
|
||||
E("a", { href: L.url(url, child.name) }, [_(child.title)]),
|
||||
]),
|
||||
);
|
||||
// Mark the current page so it gets the active pill — same vocabulary
|
||||
// as the top-level trigger (.menu-active). tree is the section node,
|
||||
// so tree.name is its dispatch segment.
|
||||
const isActive = this.isActivePath(tree.name, child.name);
|
||||
const attributes = {
|
||||
class: isActive ? "is-active-page" : "",
|
||||
href: L.url(url, child.name),
|
||||
};
|
||||
if (isActive) attributes["aria-current"] = "page";
|
||||
|
||||
ul.appendChild(E("li", {}, [E("a", attributes, [_(child.title)])]));
|
||||
});
|
||||
}
|
||||
|
||||
@ -515,17 +521,32 @@ return baseclass.extend({
|
||||
const children = [list];
|
||||
|
||||
if (document.body?.dataset?.navType === "mega-menu") {
|
||||
// Constant canvas: links fill top-to-bottom; capping at 4 columns
|
||||
// keeps the canvas height stable until a submenu exceeds 24 items.
|
||||
// Constant canvas: links fill top-to-bottom. Base column count is 4;
|
||||
// more items grow the row count, not the width, so the list stays
|
||||
// inside the three-column canvas middle track.
|
||||
list.style.setProperty(
|
||||
"--menu-rows",
|
||||
Math.max(6, Math.ceil(submenu.length / 4)),
|
||||
);
|
||||
// data-section keys the first-level icon off the node name (stable,
|
||||
// language-independent) — see _layout.css. Unmapped names fall back
|
||||
// to the default icon via var(--menu-icon, …).
|
||||
children.unshift(
|
||||
E("div", { class: "desktop-nav-anchor" }, [
|
||||
E("span", { class: "desktop-nav-title" }, [_(child.title)]),
|
||||
E(
|
||||
"span",
|
||||
{ class: "desktop-nav-title", "data-section": child.name },
|
||||
[_(child.title)],
|
||||
),
|
||||
]),
|
||||
);
|
||||
// Right column: clone the server-rendered device board into each
|
||||
// panel so the grid can lay it out as the third track. The original
|
||||
// stays hidden as the template (see _layout.css).
|
||||
const board = document.querySelector(
|
||||
".desktop-menu-container > .desktop-menu-board",
|
||||
);
|
||||
if (board) children.push(board.cloneNode(true));
|
||||
}
|
||||
|
||||
nav = E("div", { class: "desktop-nav" }, children);
|
||||
@ -570,7 +591,7 @@ return baseclass.extend({
|
||||
let maxPanel = 0;
|
||||
header
|
||||
.querySelectorAll(".desktop-nav")
|
||||
.forEach((nav) => (maxPanel = Math.max(maxPanel, nav.scrollHeight)));
|
||||
.forEach((nav) => (maxPanel = Math.max(maxPanel, nav.offsetHeight)));
|
||||
canvasHeight = headerHeight + maxPanel;
|
||||
}
|
||||
container?.style.setProperty("--mega-menu-height", `${canvasHeight}px`);
|
||||
|
||||
@ -385,6 +385,30 @@ test("renders an active group expanded with active page semantics", () => {
|
||||
assert.equal(activeLink.getAttribute("aria-current"), "page");
|
||||
});
|
||||
|
||||
test("renders the active desktop submenu link with current-page semantics", () => {
|
||||
const menu = loadMenuModule({
|
||||
dispatchpath: ["admin", "network", "wireless"],
|
||||
});
|
||||
const list = menu.renderMainMenu(
|
||||
{
|
||||
name: "network",
|
||||
children: {
|
||||
interfaces: { title: "Interfaces" },
|
||||
wireless: { title: "Wireless" },
|
||||
},
|
||||
},
|
||||
"admin/network",
|
||||
1,
|
||||
);
|
||||
const [inactiveItem, activeItem] = list.children;
|
||||
const inactiveLink = inactiveItem.children[0];
|
||||
const activeLink = activeItem.children[0];
|
||||
|
||||
assert.equal(inactiveLink.hasAttribute("aria-current"), false);
|
||||
assert.equal(activeLink.getAttribute("class"), "is-active-page");
|
||||
assert.equal(activeLink.getAttribute("aria-current"), "page");
|
||||
});
|
||||
|
||||
test("renders an inactive group collapsed and inert", () => {
|
||||
const menu = loadMenuModule();
|
||||
const item = menu.renderNavigationItem(
|
||||
@ -506,6 +530,13 @@ test("skips mega-menu initialization when the top menu is missing", () => {
|
||||
assert.ok(result instanceof FakeElement);
|
||||
});
|
||||
|
||||
test("measures mega-menu canvas from the viewport-bounded panel height", () => {
|
||||
const initMegaMenu = getMethodSource("initMegaMenu");
|
||||
|
||||
assert.match(initMegaMenu, /nav\.offsetHeight/);
|
||||
assert.doesNotMatch(initMegaMenu, /nav\.scrollHeight/);
|
||||
});
|
||||
|
||||
test("skips boxed-dropdown initialization when the top menu is missing", () => {
|
||||
const menu = loadMenuModule({
|
||||
document: createFakeDocument({ navType: "boxed-dropdown" }),
|
||||
|
||||
@ -157,3 +157,22 @@ test("mobile drawer styles only provide mobile navigation density", () => {
|
||||
);
|
||||
assert.doesNotMatch(drawer, /bg-brand-subtle/);
|
||||
});
|
||||
|
||||
test("mega-menu panels scroll within the viewport", () => {
|
||||
const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &');
|
||||
const panel = getBlock(megaMenu, "& .desktop-nav");
|
||||
|
||||
assert.ok(panel.includes("max-h-[calc(100dvh-3.5rem)]"));
|
||||
assertIncludesUtilities(panel, ["overflow-y-auto", "overscroll-contain"]);
|
||||
});
|
||||
|
||||
test("mega-menu category masks use Tailwind arbitrary utilities", () => {
|
||||
const title = getBlock(layoutStyles, "& .desktop-nav-title");
|
||||
const icon = getBlock(title, "&::before");
|
||||
|
||||
assert.match(
|
||||
icon,
|
||||
/@apply[^;]*\[mask:var\(--menu-icon,url\(["']@assets\/icons\/category\.svg["']\)\)_center\/contain_no-repeat\]/,
|
||||
);
|
||||
assert.doesNotMatch(layoutStyles, /^\s*mask\s*:/m);
|
||||
});
|
||||
|
||||
@ -70,6 +70,47 @@ test("maincontent cards use a hairline border, not heavy shadow", () => {
|
||||
assert.ok(!rule.includes("shadow-lg"), `still shadow-lg: ${rule}`);
|
||||
});
|
||||
|
||||
test("main view does not create an animation stacking context", () => {
|
||||
const layout = read("../src/media/_layout.css");
|
||||
const rule = layout.match(/#view\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
|
||||
for (const utility of [
|
||||
"animate-in",
|
||||
"fade-in-0",
|
||||
"slide-in-from-top-2",
|
||||
"fill-mode-backwards",
|
||||
"fill-mode-both",
|
||||
]) {
|
||||
assert.ok(!rule.includes(utility), `view still uses ${utility}: ${rule}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("content dropdowns stay above the closed header and below the open mega-menu", () => {
|
||||
const layer = (value) => ["z", value].join("-");
|
||||
const layout = read("../src/media/_layout.css");
|
||||
const dropdown = read("../src/media/components/_dropdown.css");
|
||||
const message = read("../src/media/components/_message.css");
|
||||
const overlay = read("../src/media/components/_overlay.css");
|
||||
const headerRule = layout.match(/^header\s*\{\s*@apply\s+([^;]+);/m)?.[1] ?? "";
|
||||
const activeHeaderRule =
|
||||
layout.match(
|
||||
/\[data-nav-type="mega-menu"\] &:has\(\.desktop-menu-container\.active\)\s*\{\s*@apply\s+([^;]+);/,
|
||||
)?.[1] ?? "";
|
||||
const dropdownRule = dropdown.match(/&\.dropdown\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
const messageRule = message.match(/\.alert-message\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
const overlayRule =
|
||||
overlay.match(/& \.desktop-menu-overlay\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
|
||||
assert.ok(messageRule.includes(layer(30)), `message layer changed: ${messageRule}`);
|
||||
assert.ok(headerRule.includes(layer(40)), `closed header layer changed: ${headerRule}`);
|
||||
assert.ok(dropdownRule.includes(layer(50)), `dropdown layer changed: ${dropdownRule}`);
|
||||
assert.ok(overlayRule.includes(layer(60)), `menu overlay layer changed: ${overlayRule}`);
|
||||
assert.ok(
|
||||
activeHeaderRule.includes(layer(70)),
|
||||
`open mega-menu layer changed: ${activeHeaderRule}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("tables get a hairline frame + sunken header, body stays unclipped", () => {
|
||||
const t = read("../src/media/components/_table.css");
|
||||
const root = t.match(/table\.table,\s*\.table\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
|
||||
@ -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:=0.12.10
|
||||
PKG_RELEASE:=27
|
||||
PKG_VERSION:=0.12.11
|
||||
PKG_RELEASE:=28
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@ -213,7 +213,7 @@
|
||||
trim(`${boardinfo.release?.distribution ?? ''} ${boardinfo.release?.version ?? ''}`);
|
||||
-%}
|
||||
<div class="desktop-menu-container">
|
||||
<div class="desktop-menu-board" aria-hidden="true">
|
||||
<div class="desktop-menu-board" aria-hidden="true"><span class="board-label">{{ _('Device') }}</span>
|
||||
<span class="board-line board-line-host" title="{{ entityencode(striptags(boardinfo.hostname ?? '?'), true) }}"><span class="board-text">{{ striptags(boardinfo.hostname ?? '?') }}</span></span>
|
||||
{% if (board_model): %}
|
||||
<span class="board-line board-line-model" title="{{ entityencode(striptags(board_model), true) }}"><span class="board-text">{{ striptags(board_model) }}</span></span>
|
||||
|
||||