mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 04:15:06 +08:00
🍓 Sync 2026-07-02 20:41:59
This commit is contained in:
+5
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /usr/share/daed/cleanup.sh
|
||||||
|
|
||||||
|
daed_cleanup_runtime
|
||||||
|
exec /usr/bin/daed "$@"
|
||||||
+4
-14
@@ -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
@@ -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)?
|
||||||
|
|||||||
@@ -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 240–480ms). */
|
||||||
|
& .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 240–480ms. 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", () => {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user