🍓 Sync 2026-07-02 20:41:59

This commit is contained in:
github-actions[bot]
2026-07-02 20:41:59 +08:00
parent e4a278f0db
commit 6ad3744c4c
21 changed files with 474 additions and 114 deletions
+5 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=daed PKG_NAME:=daed
PKG_VERSION:=2026.06.14 PKG_VERSION:=2026.06.14
PKG_RELEASE:=16 PKG_RELEASE:=17
PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
@@ -132,6 +132,10 @@ define Package/daed/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR)) $(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin $(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/daed $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/daed
$(INSTALL_BIN) $(CURDIR)/files/daed-guard $(1)/usr/bin/daed-guard
$(INSTALL_DIR) $(1)/usr/share/daed
$(INSTALL_DATA) $(CURDIR)/files/daed-cleanup.sh $(1)/usr/share/daed/cleanup.sh
$(INSTALL_DIR) $(1)/etc/config $(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/daed.config $(1)/etc/config/daed $(INSTALL_CONF) $(CURDIR)/files/daed.config $(1)/etc/config/daed
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
daed_cleanup_runtime() {
local pid
for pid in $(ip netns pids daens 2>/dev/null); do
kill "$pid" 2>/dev/null
done
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
sleep 1
for pid in $(ip netns pids daens 2>/dev/null); do
kill -9 "$pid" 2>/dev/null
done
fi
if ! ip netns del daens 2>/dev/null; then
umount /run/netns/daens 2>/dev/null
rm -f /run/netns/daens
fi
ip link del dae0 2>/dev/null || true
}
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
. /usr/share/daed/cleanup.sh
daed_cleanup_runtime
exec /usr/bin/daed "$@"
+4 -14
View File
@@ -5,9 +5,11 @@ USE_PROCD=1
START=99 START=99
CONF="daed" CONF="daed"
PROG="/usr/bin/daed" PROG="/usr/bin/daed-guard"
LOG="/var/log/daed/daed.log" LOG="/var/log/daed/daed.log"
. /usr/share/daed/cleanup.sh
start_service() { start_service() {
config_load "$CONF" config_load "$CONF"
@@ -40,19 +42,7 @@ start_service() {
stop_service() { stop_service() {
rm -f "$LOG" rm -f "$LOG"
# kill lingering procs in daens: TERM first, then KILL for stubborn ones daed_cleanup_runtime
for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null; done
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
sleep 1
for pid in $(ip netns pids daens 2>/dev/null); do kill -9 "$pid" 2>/dev/null; done
fi
# cleanup daens netns; ip-full needed for netns, busybox fallback via umount
if ! ip netns del daens 2>/dev/null; then
umount /run/netns/daens 2>/dev/null
rm -f /run/netns/daens
fi
# clean orphaned dae0
ip link del dae0 2>/dev/null || true
} }
service_triggers() { service_triggers() {
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=dnsproxy PKG_NAME:=dnsproxy
PKG_VERSION:=0.82.0 PKG_VERSION:=0.82.1
PKG_RELEASE:=11 PKG_RELEASE:=12
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/dnsproxy/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/dnsproxy/tar.gz/v$(PKG_VERSION)?
+74 -26
View File
@@ -1,28 +1,39 @@
header { header {
@apply bg-bg sticky top-0 z-40 mb-2 transition-colors duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)]; @apply bg-bg sticky top-0 z-40 mb-2 transition-colors duration-(--mega-menu-duration,300ms) ease-[cubic-bezier(0.4,0,0.6,1)];
& .header-content { & .header-content {
@apply relative flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2; @apply relative z-10 flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2;
} }
[data-nav-type="mega-menu"] & { [data-nav-type="mega-menu"] & {
/* Apple's layer split: the panel that animates GEOMETRY must not carry a /* Compositor-only curtain (supersedes the height transition, which paid
backdrop-filter, or every height frame re-rasterises the blur (the old main-thread layout + repaint on every frame and janked low-end devices):
clip-path flicker). So this is a fully-opaque SOLID surface that retracts the container is a STATIC clip frame rooted at the bar, sized by
via height the drawer close — while the frost lives entirely on the --mega-menu-height (the tallest panel, measured once in menu-aurora.js
separate .desktop-menu-overlay curtain (opacity/visibility only). Height and kept across open/close), and the reveal is a pair of opposite
is driven by --mega-menu-height (set on open, dropped on close in translateY transitions — .desktop-menu-sheet (the opaque panel surface,
menu-aurora.js); overflow-hidden clips the absolutely-positioned content overflow-hidden) slides down from behind the bar while
as the canvas grows and collapses. .desktop-menu-canvas inside it counter-slides by the same distance, so
the content nets to a fixed position and the sheet's bottom edge wipes
it in, visually identical to the old drawer but animated entirely on
the compositor thread.
Opacity is NOT animated — it's gated by `visibility` (like the curtain). Apple's layer split still applies: the surface that animates must not
Fading the panel in left it half-transparent for the whole open, so the carry a backdrop-filter, or every frame re-rasterises the blur. The
header brand/icons showed THROUGH it until opacity settled (the flash). sheet is fully opaque; the frost lives on the separate
Opaque-from-the-first-frame means the growing height wipes solidly down .desktop-menu-overlay curtain.
over the header instead. The closing state keeps the panel visible and
above the curtain while height animates down, without re-enabling input. */ Opacity is not animated on open — visibility gates the panel so the
descending sheet is opaque from the first visible frame. Closing is the
opposite: the whole menu surface fades out immediately while the sheet
retracts, so the frost, scrim and menu colour do not disappear in
separate stages. */
/* Apple-style clip frame: the opened surface starts at the top of the
header, not below it. The header content floats above this layer. The
header-height band is part of the moving sheet, so open and close share
the same page-top origin. */
& .desktop-menu-container { & .desktop-menu-container {
@apply border-hairline bg-mega-menu-bg pointer-events-none invisible absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden border-b shadow-xl transition-[height,visibility] duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)] [will-change:height] motion-reduce:transition-none max-md:hidden; @apply pointer-events-none invisible absolute inset-x-0 top-0 z-0 h-[calc(var(--mega-menu-height,0px)+3.5rem+3rem)] w-full overflow-clip max-md:hidden;
&.active, &.active,
&.closing { &.closing {
@@ -33,12 +44,46 @@ header {
@apply pointer-events-auto; @apply pointer-events-auto;
} }
/* Category panels live inside the container, not the nav items: the &.closing {
transformed #topmenu would otherwise become the containing block of @apply opacity-0 transition-opacity duration-[220ms] ease-[cubic-bezier(0.4,0,0.6,1)] motion-reduce:transition-none;
a position:fixed panel and shrink the canvas to the menu's width. }
/* The sheet is the whole opened surface, including the header-height
band. Its closed bottom edge is page top (not the header bottom), so
the close stops at the same origin that the reveal starts from. The
canvas counter-translates by the same travel distance so the menu
content remains visually fixed.
Duration is distance-adaptive via --mega-menu-duration, set on the
header by menu-aurora.js from apple.com's flyout pacing (2px/ms over
the visible travel, clamped to 240480ms). */
& .desktop-menu-sheet {
@apply bg-mega-menu-bg absolute inset-x-0 top-0 h-[calc(var(--mega-menu-height,0px)+3.5rem)] -translate-y-full overflow-hidden transition-[translate] duration-(--mega-menu-duration,300ms) ease-[cubic-bezier(0.4,0,0.6,1)] motion-reduce:transition-none;
@apply after:border-hairline after:pointer-events-none after:absolute after:inset-0 after:border-b after:shadow-xl after:transition-opacity after:duration-(--mega-menu-duration,300ms) after:content-[''] motion-reduce:after:transition-none;
}
/* The counter-transform must mirror the sheet's endpoints and timing
exactly or the content drifts during the wipe — keep both in
lockstep. */
& .desktop-menu-canvas {
@apply absolute inset-0 translate-y-full transition-[translate] duration-(--mega-menu-duration,300ms) ease-[cubic-bezier(0.4,0,0.6,1)] motion-reduce:transition-none;
}
&.active .desktop-menu-sheet,
&.active .desktop-menu-canvas {
@apply translate-y-0;
}
&.closing .desktop-menu-sheet {
@apply after:opacity-0;
}
/* Category panels live inside .desktop-menu-canvas, 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.
Each panel is the same centered three-column grid (max-width fixed), Each panel is the same centered three-column grid (max-width fixed),
so switching categories is a pure cross-fade with no width shift. so switching categories is a pure cross-fade with no width shift.
The entering panel floats up 6px — transform + opacity only. */ The entering panel floats up 6px — translate + opacity only. */
/* The server-rendered board is only a template that menu-aurora.js /* The server-rendered board is only a template that menu-aurora.js
clones into each panel's right column — the original stays hidden. */ clones into each panel's right column — the original stays hidden. */
& > .desktop-menu-board { & > .desktop-menu-board {
@@ -51,7 +96,11 @@ header {
is a pure cross-fade. Below lg the side tracks drop and only the is a pure cross-fade. Below lg the side tracks drop and only the
links column remains (tablet → just the submenu). The panel scrolls links column remains (tablet → just the submenu). The panel scrolls
internally when its content exceeds the viewport below the header. */ 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-13 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; /* Tailwind v4 translate-* utilities emit the native `translate`
property, so that is what must be transitioned — transitioning
`transform` here left the 6px entrance float snapping instead of
animating (only the opacity ever faded). */
@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-13 pb-8 opacity-0 transition-[opacity,translate] duration-[250ms] ease-out max-lg:max-w-[44rem] max-lg:grid-cols-1 max-lg:px-8;
&.active { &.active {
@apply pointer-events-auto translate-y-0 opacity-100; @apply pointer-events-auto translate-y-0 opacity-100;
@@ -156,12 +205,11 @@ header {
} }
} }
/* When a category is open the bar lifts to the panel's opened colour so the /* The bar must sit above the z-60 curtain for the whole open AND the
header and the mega-menu read as one continuous surface — no tonal seam whole retract, or the scrim would dim the closing panel. */
where the bar (bg) meets the panel (mega-menu-bg). */
[data-nav-type="mega-menu"] [data-nav-type="mega-menu"]
&:has(.desktop-menu-container:is(.active, .closing)) { &:has(.desktop-menu-container:is(.active, .closing)) {
@apply bg-mega-menu-bg z-70; @apply z-70;
} }
.brand { .brand {
@@ -69,15 +69,26 @@ body.mobile-navigation-open > header {
} }
[data-nav-type="mega-menu"] { [data-nav-type="mega-menu"] {
/* Apple's curtain technique: backdrop-filter is DECLARED here permanently and /* Apple's curtain: the frost is DECLARED here permanently and never
never toggled. Gating the blur on `.active` snapped the page from blurred toggled — it fades in and out WITH the dim, so the page blurs
to sharp the instant the menu closed, while the dim was still fading — the progressively as the sheet descends (apple.com's globalnav curtain runs
flash. Instead opacity + visibility carry it: closing keeps the layer its blur exactly like this while the flyout animates). Gating the blur
visible (visibility flips only at the end of the transition) so the blur on a post-transition "settled" state was tried and rejected: the frost
fades out together with the dim; once hidden, visibility:hidden fully popped in only after the menu landed, visibly off-design.
disables the filter so an invisible full-screen blur costs nothing. */
The cost is per-frame blur re-compositing for the 300ms of fade — but it
is now the ONLY per-frame work: the reveal itself runs entirely on the
compositor (see _layout.css). What janked low-end devices was the old
overlap of main-thread height layout + live blur, not the blur alone.
If the fade still janks a device, reduce the radius or rely on
_reduced-transparency.css, which strips the filter outright.
Closing starts immediately and uses the same 220ms exit as the menu
surface. Delaying the opacity transition left the page frosted after the
sheet had already collapsed into the header, which read as a flash. Once
hidden, visibility:hidden makes the invisible filter cost nothing. */
& .desktop-menu-overlay { & .desktop-menu-overlay {
@apply bg-mega-menu-scrim pointer-events-none invisible fixed inset-0 z-60 opacity-0 backdrop-blur-lg backdrop-saturate-150 transition-[opacity,visibility] delay-[80ms] duration-[300ms] ease-[cubic-bezier(0.4,0,0.6,1)] max-md:hidden; @apply bg-mega-menu-scrim pointer-events-none invisible fixed inset-0 z-60 opacity-0 backdrop-blur-lg backdrop-saturate-150 transition-[opacity,visibility] duration-[220ms] ease-[cubic-bezier(0.4,0,0.6,1)] max-md:hidden;
&.active { &.active {
@apply pointer-events-auto visible opacity-100; @apply pointer-events-auto visible opacity-100;
@@ -570,6 +570,7 @@ return baseclass.extend({
initMegaMenu(children, url, ul) { initMegaMenu(children, url, ul) {
const container = document.querySelector(".desktop-menu-container"); const container = document.querySelector(".desktop-menu-container");
const canvas = container?.querySelector(".desktop-menu-canvas");
const overlay = document.querySelector(".desktop-menu-overlay"); const overlay = document.querySelector(".desktop-menu-overlay");
const header = document.querySelector("header"); const header = document.querySelector("header");
@@ -579,28 +580,43 @@ return baseclass.extend({
let hideTimer = null; let hideTimer = null;
// Constant canvas: every category opens at the same height — the // Constant canvas: every category opens at the same height — the
// tallest submenu wins. Measured lazily on first open (after fonts // tallest submenu wins. --mega-menu-height is that panel height alone
// settle) and cached, so switching categories is a pure cross-fade // (the visible travel; the bar is not part of the wipe distance).
// with zero layout work. A resize invalidates the cache. // Pre-measured at idle (after fonts settle) so the first hover pays no
// synchronous reflow, and cached, so switching categories is a pure
// cross-fade with zero layout work. The height stays set across
// open/close — it is the sheet's translate reference, not the animated
// property. A resize invalidates the cache.
let canvasHeight = 0; let canvasHeight = 0;
let revealDuration = 300;
const applyCanvasHeight = () => { const applyCanvasHeight = () => {
if (!canvasHeight) { if (!canvasHeight) {
const headerHeight =
header.querySelector(".header-content")?.offsetHeight || 56;
let maxPanel = 0;
header header
.querySelectorAll(".desktop-nav") .querySelectorAll(".desktop-nav")
.forEach((nav) => (maxPanel = Math.max(maxPanel, nav.offsetHeight))); .forEach(
canvasHeight = headerHeight + maxPanel; (nav) => (canvasHeight = Math.max(canvasHeight, nav.offsetHeight)),
);
// apple.com's flyout pacing: a constant 2px/ms over the visible
// travel, clamped to 240480ms. Short panels open snappily, tall
// ones don't blink.
revealDuration = Math.min(
480,
Math.max(240, Math.round(canvasHeight / 2)),
);
} }
container?.style.setProperty("--mega-menu-height", `${canvasHeight}px`); // Both vars live on the header, not the container: the container
// inherits the height, and the header's own transition-colors reads
// the duration so the bar colour fades in lockstep with the wipe.
header.style.setProperty("--mega-menu-height", `${canvasHeight}px`);
header.style.setProperty("--mega-menu-duration", `${revealDuration}ms`);
}; };
// Re-measure on resize even while closed — leaving the cache cold would
// push the reflow back onto the next hover's open path.
let resizeTimer = null; let resizeTimer = null;
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
canvasHeight = 0; canvasHeight = 0;
if (!container?.classList.contains("active")) return;
clearTimeout(resizeTimer); clearTimeout(resizeTimer);
resizeTimer = setTimeout(applyCanvasHeight, 150); resizeTimer = setTimeout(applyCanvasHeight, 150);
}); });
@@ -651,12 +667,12 @@ return baseclass.extend({
); );
if (!hasSubmenu) return; if (!hasSubmenu) return;
// Reparent the panel into the canvas container: the transformed // Reparent the panel into the counter-transformed canvas: the
// #topmenu would otherwise become the containing block of the // transformed #topmenu would otherwise become the containing block of
// absolutely-positioned panel and shrink the canvas to the menu's // the absolutely-positioned panel and shrink the canvas to the menu's
// width. Inside the container the panel is also clipped by the same // width. Inside the canvas the panel rides the sheet's compositor
// clip-path reveal as the rest of the canvas. // reveal (and is clipped by the sheet's overflow) for free.
if (container) container.appendChild(nav); if (canvas) canvas.appendChild(nav);
li.addEventListener("mouseenter", () => { li.addEventListener("mouseenter", () => {
if (hideTimer) { if (hideTimer) {
@@ -683,6 +699,7 @@ return baseclass.extend({
if (container) { if (container) {
applyCanvasHeight(); applyCanvasHeight();
container.dataset.closeToken = "";
container.classList.add("active"); container.classList.add("active");
container.classList.remove("closing"); container.classList.remove("closing");
overlay.classList.add("active"); overlay.classList.add("active");
@@ -699,6 +716,16 @@ return baseclass.extend({
}); });
}); });
// Pre-measure the canvas off the interaction path: fonts change panel
// heights, so wait for them, then measure when the main thread is idle.
// A hover that beats this still falls back to the lazy measure above.
const idle = window.requestIdleCallback || ((fn) => setTimeout(fn, 1));
(document.fonts?.ready || Promise.resolve()).then(() =>
idle(() => {
if (!canvasHeight) applyCanvasHeight();
}),
);
const hideMenu = () => { const hideMenu = () => {
if (showTimer) { if (showTimer) {
clearTimeout(showTimer); clearTimeout(showTimer);
@@ -777,41 +804,61 @@ return baseclass.extend({
) )
return; return;
// Retract the curtain: removing `.active` sends the sheet back to
// translateY(-100%) and the canvas to its counter-position — the drawer
// close, run entirely on the compositor. --mega-menu-height stays put;
// it is the translate reference, not the animated property. `.closing`
// keeps the container in the tree only while its opacity fades out and
// the sheet returns to the closed translate position.
container.classList.add("closing"); container.classList.add("closing");
container.classList.remove("active"); container.classList.remove("active");
// Retract the canvas: dropping --mega-menu-height lets the container's const sheet = container.querySelector(".desktop-menu-sheet");
// height fall to its 0 fallback, and because the container transitions const closeToken = `${Date.now()}-${Math.random()}`;
// height (see _layout.css) it animates the collapse — the drawer close — container.dataset.closeToken = closeToken;
// instead of the height vanishing in one frame after the fade. overflow
// -hidden clips the content as it goes; the end state is already h-0 so no
// post-transition reset is needed.
container.style.removeProperty("--mega-menu-height");
let closeFallback = null; let closeFallback = null;
const finishClosing = (event) => { const finishClosing = (event) => {
if (event?.target && event.target !== container) return; if (event?.target && event.target !== sheet) return;
if (event?.propertyName && event.propertyName !== "height") return; if (container.dataset.closeToken !== closeToken) {
if (closeFallback !== null) {
clearTimeout(closeFallback);
closeFallback = null;
}
sheet?.removeEventListener("transitionend", finishClosing);
return;
}
if (closeFallback !== null) { if (closeFallback !== null) {
clearTimeout(closeFallback); clearTimeout(closeFallback);
closeFallback = null; closeFallback = null;
} }
container.removeEventListener("transitionend", finishClosing); sheet?.removeEventListener("transitionend", finishClosing);
if (!container.classList.contains("active")) { if (!container.classList.contains("active")) {
container.classList.remove("closing"); container.classList.remove("closing");
delete container.dataset.closeToken;
} }
}; };
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) { if (
!sheet ||
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
) {
finishClosing(); finishClosing();
return; return;
} }
container.addEventListener("transitionend", finishClosing); sheet.addEventListener("transitionend", finishClosing);
closeFallback = setTimeout(finishClosing, 350); // The retract duration is distance-adaptive (see applyCanvasHeight);
// read it back so the safety net always outlasts the real transition.
const duration =
parseInt(
document
.querySelector("header")
?.style.getPropertyValue("--mega-menu-duration"),
) || 300;
closeFallback = setTimeout(finishClosing, duration + 50);
}, },
renderModeMenu(tree) { renderModeMenu(tree) {
@@ -71,6 +71,7 @@ class FakeElement {
this.style = { this.style = {
display: "", display: "",
properties: new Map(), properties: new Map(),
getPropertyValue: (name) => this.style.properties.get(name) ?? "",
removeProperty: (name) => this.style.properties.delete(name), removeProperty: (name) => this.style.properties.delete(name),
setProperty: (name, value) => this.style.properties.set(name, value), setProperty: (name, value) => this.style.properties.set(name, value),
}; };
@@ -228,6 +229,7 @@ const loadMenuModule = ({
getChildren = getMenuChildren, getChildren = getMenuChildren,
requestpath = [], requestpath = [],
translate = (value) => value, translate = (value) => value,
window = {},
} = {}) => { } = {}) => {
const baseclass = { const baseclass = {
extend(module) { extend(module) {
@@ -250,7 +252,7 @@ const loadMenuModule = ({
"window", "window",
"localStorage", "localStorage",
source, source,
)(baseclass, ui, E, L, translate, document, {}, {}); )(baseclass, ui, E, L, translate, document, window, {});
}; };
const textContent = (element) => const textContent = (element) =>
@@ -768,15 +770,20 @@ test("renders an active group expanded when an open mobile list was initially em
assert.equal(region.hasAttribute("inert"), false); assert.equal(region.hasAttribute("inert"), false);
}); });
test("keeps mega-menu stacking state while the close height transition runs", () => { test("starts the mega-menu overlay and surface exit together", () => {
const container = new FakeElement("div", { const sheet = new FakeElement("div", { class: "desktop-menu-sheet" });
class: "desktop-menu-container active", const container = new FakeElement(
}); "div",
{ class: "desktop-menu-container active" },
[sheet],
);
const overlay = new FakeElement("div", { const overlay = new FakeElement("div", {
class: "desktop-menu-overlay active", class: "desktop-menu-overlay active",
}); });
const header = new FakeElement("header");
const document = createFakeDocument({ const document = createFakeDocument({
elements: { elements: {
header,
".desktop-menu-container": container, ".desktop-menu-container": container,
".desktop-menu-overlay": overlay, ".desktop-menu-overlay": overlay,
}, },
@@ -784,19 +791,142 @@ test("keeps mega-menu stacking state while the close height transition runs", ()
const menu = loadMenuModule({ document }); const menu = loadMenuModule({ document });
container.style.setProperty("--mega-menu-height", "320px"); container.style.setProperty("--mega-menu-height", "320px");
header.style.setProperty("--mega-menu-duration", "1ms");
menu.hideDesktopNav(); menu.hideDesktopNav();
// Removing .active starts the scrim/blur fade immediately. The container
// remains in .closing so its own opacity can fade while the sheet retracts.
assert.equal(overlay.classList.contains("active"), false); assert.equal(overlay.classList.contains("active"), false);
assert.equal(container.classList.contains("active"), false); assert.equal(container.classList.contains("active"), false);
assert.equal(container.classList.contains("closing"), true); assert.equal(container.classList.contains("closing"), true);
assert.equal(container.style.properties.has("--mega-menu-height"), false); // The canvas height is the sheet's translate reference, not the animated
// property — it must survive the close for the retract to have a distance.
assert.equal(container.style.properties.get("--mega-menu-height"), "320px");
container.dispatchEvent({ sheet.dispatchEvent({
type: "transitionend", type: "transitionend",
target: container, target: sheet,
propertyName: "height", propertyName: "translate",
}); });
assert.equal(container.classList.contains("closing"), false); assert.equal(container.classList.contains("closing"), false);
}); });
test("ignores stale mega-menu close timers after reopen and second close", async () => {
const sheet = new FakeElement("div", { class: "desktop-menu-sheet" });
const container = new FakeElement(
"div",
{ class: "desktop-menu-container active" },
[sheet],
);
const overlay = new FakeElement("div", {
class: "desktop-menu-overlay active",
});
const header = new FakeElement("header");
const document = createFakeDocument({
elements: {
header,
".desktop-menu-container": container,
".desktop-menu-overlay": overlay,
},
});
const menu = loadMenuModule({ document });
header.style.setProperty("--mega-menu-duration", "300ms");
menu.hideDesktopNav();
await new Promise((resolve) => setTimeout(resolve, 120));
container.classList.add("active");
container.classList.remove("closing");
overlay.classList.add("active");
menu.hideDesktopNav();
await new Promise((resolve) => setTimeout(resolve, 260));
assert.equal(container.classList.contains("closing"), true);
await new Promise((resolve) => setTimeout(resolve, 120));
assert.equal(container.classList.contains("closing"), false);
});
test("the hover path opens, switches, closes and reopens the mega-menu", async () => {
// The first-open dwell is 100ms; category switches on an open menu fire
// with no dwell but still through a timer.
const openDwell = () => new Promise((resolve) => setTimeout(resolve, 120));
const canvas = new FakeElement("div", { class: "desktop-menu-canvas" });
const sheet = new FakeElement("div", { class: "desktop-menu-sheet" }, [
canvas,
]);
const container = new FakeElement(
"div",
{ class: "desktop-menu-container" },
[sheet],
);
const overlay = new FakeElement("div", { class: "desktop-menu-overlay" });
const header = new FakeElement("header", {}, [
new FakeElement("div", { class: "header-content" }),
container,
]);
const document = createFakeDocument({
elements: {
header,
".desktop-menu-container": container,
".desktop-menu-overlay": overlay,
},
});
const ul = E("ul", { id: "topmenu" });
// deactivateDesktopNavExcept sweeps document-wide; give the fake document
// a real tree to sweep or stale `.active` panels trip the wasActive guard.
const root = new FakeElement("html", {}, [header, ul]);
document.querySelectorAll = (selector) => root.querySelectorAll(selector);
const menu = loadMenuModule({
document,
window: { addEventListener() {} },
});
menu.initMegaMenu(
[
{ name: "network", title: "Network", children: { wifi: {} } },
{ name: "system", title: "System", children: { admin: {} } },
],
"admin",
ul,
);
const [firstItem, secondItem] = ul.children;
const navFor = (item) => item.querySelector(".desktop-nav");
// First open: hover dwell activates the container, the overlay and the
// hovered category's panel together.
firstItem.dispatchEvent({ type: "mouseenter" });
await openDwell();
assert.equal(container.classList.contains("active"), true);
assert.equal(overlay.classList.contains("active"), true);
assert.equal(navFor(firstItem).classList.contains("active"), true);
// Switching categories on the open menu swaps the active panel without
// dropping the container out of its open state.
secondItem.dispatchEvent({ type: "mouseenter" });
await openDwell();
assert.equal(container.classList.contains("active"), true);
assert.equal(navFor(firstItem).classList.contains("active"), false);
assert.equal(navFor(secondItem).classList.contains("active"), true);
// Close, then reopen BEFORE the retract finishes: the interrupted close
// must not leave a stale `.active` panel that trips the wasActive guard
// and blocks the container from re-activating.
menu.hideDesktopNav();
assert.equal(container.classList.contains("active"), false);
assert.equal(container.classList.contains("closing"), true);
firstItem.dispatchEvent({ type: "mouseenter" });
await openDwell();
assert.equal(container.classList.contains("active"), true);
assert.equal(container.classList.contains("closing"), false);
assert.equal(navFor(firstItem).classList.contains("active"), true);
});
@@ -166,22 +166,50 @@ test("mega-menu panels scroll within the viewport", () => {
assertIncludesUtilities(panel, ["overflow-y-auto", "overscroll-contain"]); assertIncludesUtilities(panel, ["overflow-y-auto", "overscroll-contain"]);
}); });
test("mega-menu closing state keeps the panel above the curtain", () => { test("mega-menu reveal and retract share the page-top origin", () => {
const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &'); const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &');
const headerContent = getBlock(layoutStyles, "& .header-content");
const container = getBlock(megaMenu, "& .desktop-menu-container"); const container = getBlock(megaMenu, "& .desktop-menu-container");
const sheet = getBlock(container, "& .desktop-menu-sheet");
const canvas = getBlock(container, "& .desktop-menu-canvas");
const panel = getBlock(megaMenu, "& .desktop-nav");
const headerLift = layoutStyles.match( const headerLift = layoutStyles.match(
/When a category is open[\s\S]*?(\[data-nav-type="mega-menu"\][\s\S]*?)\n\s*\.brand/, /The bar must sit above[\s\S]*?(\[data-nav-type="mega-menu"\][\s\S]*?)\n\s*\.brand/,
)?.[1]; )?.[1];
assertIncludesUtilities(headerContent, ["z-10"]);
assertIncludesUtilities(container, ["top-0", "z-0"]);
assert.doesNotMatch(
container,
/@apply bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 h-14/,
);
assert.match( assert.match(
container, container,
/&\.active,\s*&\.closing\s*\{[\s\S]*@apply[^;]*\bvisible\b/, /&\.active,\s*&\.closing\s*\{[\s\S]*@apply[^;]*\bvisible\b/,
); );
assert.match(
container,
/&\.closing\s*\{[\s\S]*@apply[^;]*\bopacity-0\b[^;]*\btransition-opacity\b[^;]*\bduration-\[220ms\]/,
);
assert.match( assert.match(
container, container,
/&\.active\s*\{[\s\S]*@apply[^;]*pointer-events-auto/, /&\.active\s*\{[\s\S]*@apply[^;]*pointer-events-auto/,
); );
assert.match(headerLift ?? "", /desktop-menu-container[\s\S]*closing/); assertIncludesUtilities(sheet, ["top-0", "-translate-y-full"]);
assert.match(
sheet,
/h-\[calc\(var\(--mega-menu-height,0px\)\+3\.5rem\)\]/,
);
assertIncludesUtilities(canvas, ["translate-y-full"]);
assertIncludesUtilities(panel, ["top-14"]);
// z-70 must span the retract too (or the curtain dims the closing panel).
assert.match(
headerLift ?? "",
/desktop-menu-container:is\(\.active, \.closing\)\)\s*\{\s*@apply[^;]*z-70/,
);
// The bar no longer performs its own background transition. The flyout sheet
// owns the opened surface and retracts fully to page top before hiding.
assert.doesNotMatch(headerLift ?? "", /bg-mega-menu-bg/);
}); });
test("mega-menu category masks use Tailwind arbitrary utilities", () => { test("mega-menu category masks use Tailwind arbitrary utilities", () => {
+69 -10
View File
@@ -49,25 +49,84 @@ test("dark layering: page < card, sunken between page and card", () => {
assert.ok(sunk >= bg && sunk < surf, `sunken not between page and card: ${d.surface_sunken}`); assert.ok(sunk >= bg && sunk < surf, `sunken not between page and card: ${d.surface_sunken}`);
}); });
test("mega-menu frost lives on the curtain, not the height-animating panel", () => { test("mega-menu reveal is compositor-only and the frost never overlaps it", () => {
const layout = read("../src/media/_layout.css"); const layout = read("../src/media/_layout.css");
const overlay = read("../src/media/components/_overlay.css"); const overlay = read("../src/media/components/_overlay.css");
// The panel animates geometry (height) for the drawer open/close, so it must // The sheet is the moving surface, so it must carry NO backdrop-filter —
// carry NO backdrop-filter — blurring a resizing box re-rasterises every // blurring an animating box re-rasterises every frame (the old clip-path
// frame (the old clip-path flicker). The frost moved entirely to the curtain. // flicker) — and must animate translate, not height: a height transition
const panel = layout.split("\n").find((l) => l.includes("bg-mega-menu-bg")); // pays main-thread layout + repaint per frame (the low-end hover jank).
assert.ok(!panel?.includes("backdrop-blur"), `panel must not blur: ${panel}`); const sheet =
layout.match(/& \.desktop-menu-sheet\s*\{([\s\S]*?)\n\s*\}/)?.[1] ?? "";
assert.ok(!sheet?.includes("backdrop-blur"), `sheet must not blur: ${sheet}`);
assert.ok( assert.ok(
panel?.includes("transition-[height,visibility]"), !layout.includes(
`panel animates height: ${panel}`, "@apply bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 h-14",
),
"header band must not be static",
);
// Apple-style geometry: the sheet is the whole opened surface, including the
// header-height band. Its closed bottom edge is page top, matching the
// reveal origin.
assert.ok(
sheet?.includes("transition-[translate]") &&
sheet?.includes("top-0") &&
sheet?.includes("h-[calc(var(--mega-menu-height,0px)+3.5rem)]") &&
sheet?.includes("-translate-y-full"),
`sheet must move from and to page top: ${sheet}`,
);
const containerRule =
layout.match(/& \.desktop-menu-container\s*\{\s*@apply\s+([^;]+);/)?.[1] ??
"";
assert.ok(
!containerRule.includes("transition") &&
!containerRule.includes("will-change"),
`container is a static frame, it must not animate: ${containerRule}`,
);
// A translated sheet moves inside a static clip frame rooted at the page top.
// The container adds shadow slack below the travel but does not move itself.
assert.ok(
containerRule.includes("top-0") &&
containerRule.includes("h-[calc(var(--mega-menu-height,0px)+3.5rem+3rem)]") &&
containerRule.includes("overflow-clip"),
`container must clip the page-top retract: ${containerRule}`,
);
assert.match(
layout,
/&\.closing\s*\{[\s\S]*?@apply[^;]*opacity-0[^;]*transition-opacity[^;]*duration-\[220ms\]/,
);
// The counter-transformed canvas must mirror the sheet's endpoints and
// timing exactly or the content drifts during the wipe; both take the
// distance-adaptive duration from the same variable.
const canvasRule =
layout.match(/& \.desktop-menu-canvas\s*\{([\s\S]*?)\n\s*\}/)?.[1] ?? "";
for (const token of [
"transition-[translate]",
"duration-(--mega-menu-duration,300ms)",
"translate-y-full",
]) {
assert.ok(canvasRule.includes(token), `canvas missing ${token}: ${canvasRule}`);
}
assert.ok(
sheet?.includes("duration-(--mega-menu-duration,300ms)"),
`sheet must share the adaptive duration: ${sheet}`,
); );
// The curtain carries the blur permanently (Apple's globalnav-curtain) and // The curtain carries the blur permanently (Apple's globalnav-curtain) and
// fades it with opacity/visibility, never snapping it on/off via .active. // fades it with opacity/visibility. Close must start that fade immediately;
// delaying the curtain left blur visible after the sheet had collapsed.
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur"); assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
const curtain = overlay const curtain = overlay
.split("\n") .split("\n")
.find((l) => l.includes("bg-mega-menu-scrim")); .find((l) => l.includes("bg-mega-menu-scrim"));
assert.ok(curtain?.includes("backdrop-blur-lg"), `desktop curtain blur: ${curtain}`); assert.ok(
curtain?.includes("backdrop-blur-lg"),
`desktop curtain blur: ${curtain}`,
);
assert.ok(
curtain?.includes("duration-[220ms]") && !curtain.includes("delay-"),
`desktop curtain exit must be immediate: ${curtain}`,
);
assert.ok(!overlay.includes(".settled"), "settle-gated frost must stay gone");
}); });
test("maincontent cards use a hairline border, not heavy shadow", () => { test("maincontent cards use a hairline border, not heavy shadow", () => {
+3 -1
View File
@@ -215,8 +215,10 @@ function buildSshArgs(cfg: ScpConfig): string {
return args.join(" "); return args.join(" ");
} }
// Stream the file over ssh stdin instead of scp: OpenSSH 9+ scp defaults to
// the SFTP protocol, which Dropbear on OpenWrt does not ship a server for.
function buildScpCommand(localPath: string, remotePath: string, cfg: ScpConfig): string { function buildScpCommand(localPath: string, remotePath: string, cfg: ScpConfig): string {
return `scp ${buildSshArgs(cfg)} "${localPath}" "${cfg.host}:${remotePath}"`; return `ssh ${buildSshArgs(cfg)} "${cfg.host}" "cat > '${remotePath}'" < "${localPath}"`;
} }
function parseHost(sshHost: string): string { function parseHost(sshHost: string): string {
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS) LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.0.4 PKG_VERSION:=1.0.5
PKG_RELEASE:=37 PKG_RELEASE:=38
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -232,6 +232,14 @@
trim(`${boardinfo.release?.distribution ?? ''} ${boardinfo.release?.version ?? ''}`); trim(`${boardinfo.release?.distribution ?? ''} ${boardinfo.release?.version ?? ''}`);
-%} -%}
<div class="desktop-menu-container"> <div class="desktop-menu-container">
{#
Curtain-reveal layer pair: .desktop-menu-sheet is the opaque
surface that slides down (translateY -100% → 0) and clips its
content; .desktop-menu-canvas counter-translates (100% → 0) so
the panels menu-aurora.js reparents into it appear static while
the sheet edge wipes over them — see _layout.css.
#}
<div class="desktop-menu-sheet"><div class="desktop-menu-canvas"></div></div>
<div class="desktop-menu-board" aria-hidden="true"><span class="board-label">{{ _('Device') }}</span> <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> <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): %} {% if (board_model): %}
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow PKG_NAME:=natflow
PKG_VERSION:=20260531 PKG_VERSION:=20260531
PKG_RELEASE:=28 PKG_RELEASE:=29
PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=e3c6601723677e8c97199f68b8075257ec8d64e8 PKG_SOURCE_VERSION:=3700ee2266c74690f1f43fc547bdefb0b8d6090f
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com> PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0 PKG_LICENSE:=GPL-2.0
Regular → Executable
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=pcie_mhi PKG_NAME:=pcie_mhi
PKG_VERSION:=1.3.8 PKG_VERSION:=1.3.8
PKG_RELEASE:=25 PKG_RELEASE:=26
include $(INCLUDE_DIR)/kernel.mk include $(INCLUDE_DIR)/kernel.mk
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=watchcat PKG_NAME:=watchcat
PKG_VERSION:=1 PKG_VERSION:=1
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_MAINTAINER:=Roger D <rogerdammit@gmail.com> PKG_MAINTAINER:=Roger D <rogerdammit@gmail.com>
PKG_LICENSE:=GPL-2.0 PKG_LICENSE:=GPL-2.0
+5 -1
View File
@@ -169,6 +169,8 @@ watchcat_monitor_network() {
done done
[ "$((time_now - time_lastcheck_withinternet))" -ge "$failure_period" ] && { [ "$((time_now - time_lastcheck_withinternet))" -ge "$failure_period" ] && {
recovery_started="$time_now"
if [ "$script" != "" ]; then if [ "$script" != "" ]; then
watchcat_run_script "$script" "$iface" watchcat_run_script "$script" "$iface"
else else
@@ -188,8 +190,10 @@ watchcat_monitor_network() {
time_now="$(cat /proc/uptime)" time_now="$(cat /proc/uptime)"
time_now="${time_now%%.*}" time_now="${time_now%%.*}"
time_lastcheck="$time_now" time_lastcheck="$time_now"
time_lastcheck_withinternet="$time_now"
else
time_lastcheck_withinternet="$recovery_started"
fi fi
time_lastcheck_withinternet="$time_now"
} }
done done