diff --git a/istoreenhance/Makefile b/istoreenhance/Makefile index 1ac9da589..56275d391 100644 --- a/istoreenhance/Makefile +++ b/istoreenhance/Makefile @@ -12,7 +12,7 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH) PKG_NAME:=istoreenhance # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=0.7.15 -PKG_RELEASE:=4 +PKG_RELEASE:=5 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) ARCH_HEXCODE=8664 @@ -25,7 +25,7 @@ ARCH_HEXCODE=1b0c endif PKG_SOURCE_VERSION:=c74312de1d50b9de65d0044fcf7c4d88a1f9917c PKG_SOURCE:=iStoreEnhance-binary-$(PKG_SOURCE_DATE).tar.gz -PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v0.7.15/ +PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_SOURCE_DATE)/ PKG_HASH:=skip PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE) diff --git a/linkease-common-bin/Makefile b/linkease-common-bin/Makefile index 056cc8289..05726d3fb 100644 --- a/linkease-common-bin/Makefile +++ b/linkease-common-bin/Makefile @@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkease-common-bin # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=1.7.5 -PKG_RELEASE:=9 +PKG_RELEASE:=10 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel) ARCH_HEXCODE=1b0c endif -PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd +PKG_SOURCE_VERSION:=2f7e306c71c36126c320b4e2f0130fda4f3c15bf PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/ PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE) diff --git a/linkease/Makefile b/linkease/Makefile index 1295650b9..8910e3cb6 100644 --- a/linkease/Makefile +++ b/linkease/Makefile @@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkease # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=1.7.5 -PKG_RELEASE:=12 +PKG_RELEASE:=13 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel) ARCH_HEXCODE=1b0c endif -PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd +PKG_SOURCE_VERSION:=2f7e306c71c36126c320b4e2f0130fda4f3c15bf PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/ PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE) diff --git a/linkeasefull/Makefile b/linkeasefull/Makefile index 03ed9fbff..68af05885 100644 --- a/linkeasefull/Makefile +++ b/linkeasefull/Makefile @@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkeasefull # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=3.0.11 -PKG_RELEASE:=11 +PKG_RELEASE:=12 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64 PKG_HASH:=skip endif -PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd +PKG_SOURCE_VERSION:=2f7e306c71c36126c320b4e2f0130fda4f3c15bf PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/ PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH) diff --git a/luci-app-librespeed/htdocs/luci-static/resources/view/librespeed/history.js b/luci-app-librespeed/htdocs/luci-static/resources/view/librespeed/history.js index 88a67afee..c2e3eaf72 100644 --- a/luci-app-librespeed/htdocs/luci-static/resources/view/librespeed/history.js +++ b/luci-app-librespeed/htdocs/luci-static/resources/view/librespeed/history.js @@ -240,11 +240,21 @@ return view.extend({ return [ fam, pr ].filter(x => x).join(' · ') || '–'; }; - /* Sort the whole range here, not just the page: ui.Table only ever - * orders the rows it was handed, so paging first would turn the - * headers into a per-page sort while the count below still speaks - * for every measurement. */ - let rows = entries.slice().reverse(); + /* Build the cells first, then sort them: the sort key comes out of a + * cell, and ui.Table only ever orders the rows it was handed, so + * paging first would turn the headers into a per-page sort while the + * count below still speaks for every measurement. */ + let rows = entries.slice().reverse().map(e => [ + [ e.epoch ?? 0, when(e) ], + (e.server && e.server.name) || '–', + e.interface || '–', + protoCell(e), + [ num(e.download_mbps), fmtNum(e.download_mbps, 2) ], + [ num(e.upload_mbps), fmtNum(e.upload_mbps, 2) ], + [ num(e.ping_ms), fmtNum(e.ping_ms, 1) ], + [ num(e.jitter_ms), fmtNum(e.jitter_ms, 1) ] + ]); + const sorting = this.table.getActiveSortState(); if (sorting) @@ -263,17 +273,8 @@ return view.extend({ this.page = pages - 1; this.renderPager(rows.length, pages); + this.table.update(rows.slice(this.page * pageSize, (this.page + 1) * pageSize)); - this.table.update(rows.slice(this.page * pageSize, (this.page + 1) * pageSize).map(e => [ - [ e.epoch ?? 0, when(e) ], - (e.server && e.server.name) || '–', - e.interface || '–', - protoCell(e), - [ num(e.download_mbps), fmtNum(e.download_mbps, 2) ], - [ num(e.upload_mbps), fmtNum(e.upload_mbps, 2) ], - [ num(e.ping_ms), fmtNum(e.ping_ms, 1) ], - [ num(e.jitter_ms), fmtNum(e.jitter_ms, 1) ] - ])); }, /* Shown only when there is more than one page; the count is always diff --git a/luci-theme-aurora/.claude/skills/aurora-performance/references/aurora-budgets.md b/luci-theme-aurora/.claude/skills/aurora-performance/references/aurora-budgets.md index 45337187c..8d67ce374 100644 --- a/luci-theme-aurora/.claude/skills/aurora-performance/references/aurora-budgets.md +++ b/luci-theme-aurora/.claude/skills/aurora-performance/references/aurora-budgets.md @@ -13,10 +13,10 @@ address, so they stay local). |---|---|---|---| | main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-08 (191,899 B; +~2 KB custom-background mode, +~0.5 KB router progress bar / live region; the bar is now Turbo-shaped — width/opacity only, inserted per navigation) | | login.css (identity/raw) | ≤ 12 KB | size | production build, 2026-07 (10,935 B, token-pruned) | -| menu-aurora.js (identity/raw) | ≤ 21.5 KB | size | production build, 2026-08 (20,851 B; +1.3 KB for the router's `syncRoute`/`closeSurfaces` hooks) | +| menu-aurora.js (identity/raw) | ≤ 22 KB | size | production build, 2026-08 (21,809 B; +0.8 KB palette recents: record on pick, pure-LRU browse order, storage validation; +0.2 KB ">" logout command) | | router-aurora.js (identity/raw, Navigation-API browsers only) | ≤ 15 KB | size | production build, 2026-08 (14,844 B: template shells fetched, per-render listener teardown, timeout-as-failure, expiry gate, readonly folding, node css, wildcard actions, trickling progress bar, same-URL reload rule, visibility gate, contract check) | | Default logo (identity/raw) | ≤ 16 KB | size | production build, 2026-07 (15,057 B) | -| Core admin cold theme assets (identity/raw) | ≤ 267 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-08 (≈266.3 KB; the router is a one-time cost that removes per-click dispatcher work) | +| Core admin cold theme assets (identity/raw) | ≤ 267.5 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-08 (≈267.2 KB; the router is a one-time cost that removes per-click dispatcher work; +1.0 KB palette recents and logout command) | | Login cold theme assets, excluding configured background (identity/raw) | ≤ 55 KB | size | login CSS + default font + logo, 2026-07 (49,572 B) | | Blocking requests before first paint | ≤ 4 | count | current waterfall | | Repeat-visit asset requests | ≈ 0 | count | target state; package-built CSS/JS URLs are versioned, but long-lived cache headers still need live verification | diff --git a/luci-theme-aurora/.dev/src/resource/menu-aurora.js b/luci-theme-aurora/.dev/src/resource/menu-aurora.js index d6f4b915b..187eba6f2 100644 --- a/luci-theme-aurora/.dev/src/resource/menu-aurora.js +++ b/luci-theme-aurora/.dev/src/resource/menu-aurora.js @@ -2,6 +2,8 @@ "require baseclass"; "require ui"; +const PALETTE_RECENTS_KEY = "aurora.paletteRecents"; + return baseclass.extend({ __init__() { ui.menu.load().then((tree) => this.render(tree)); @@ -599,8 +601,12 @@ return baseclass.extend({ if (!toggle || this.paletteIndex) return; this.paletteIndex = []; + let logout = null; items.forEach((item) => { - if (item.isLogout) return; + if (item.isLogout) { + logout = item; + return; + } if (!item.hasChildren) { this.paletteIndex.push({ title: item.title, @@ -645,6 +651,18 @@ return baseclass.extend({ }), ); + // Logout is a command, not a destination: it rides ">" only (last, after + // the modes), stays out of the plain browse list, and is never recorded + // as a recent — but a typed query still reaches it. + if (logout) + this.paletteIndex.push({ + title: logout.title, + name: logout.name, + group: null, + href: logout.href, + isLogout: true, + }); + // Only msgids that already exist in a shipped catalog (luci-base, or the // config app's as above) are used — the theme intentionally ships no // translations of its own. @@ -785,8 +803,15 @@ return baseclass.extend({ // the clicked anchor is replaced by the re-render, which would strand // focus on with the dialog still open. results.addEventListener("click", (e) => { - const mode = e.target?.closest?.(".cmdk-row")?.dataset.mode; - if (!mode) return; + const row = e.target?.closest?.(".cmdk-row"); + if (!row) return; + const mode = row.dataset.mode; + if (!mode) { + // Navigation rows: remember the pick, then let the click navigate + // (same-document router or full load) untouched. + if (row.dataset.name) this.recordPaletteRecent(row.dataset.name); + return; + } e.preventDefault(); setTheme(mode); syncSwitchers(); @@ -876,6 +901,46 @@ return baseclass.extend({ // Empty query matches everything at score 0 (the browse list); title hits // outrank name/path and group hits and are the only ones highlighted. + // localStorage can be unavailable (privacy modes) or hold anything after + // a downgrade — both read as "no history". No size cap: dedupe bounds the + // list by the pages actually visited, i.e. the menu's own scale. + readPaletteRecents() { + try { + const list = JSON.parse(localStorage.getItem(PALETTE_RECENTS_KEY)); + return Array.isArray(list) + ? list.filter((name) => typeof name === "string") + : []; + } catch { + return []; + } + }, + + recordPaletteRecent(name) { + try { + localStorage.setItem( + PALETTE_RECENTS_KEY, + JSON.stringify([ + name, + ...this.readPaletteRecents().filter((kept) => kept !== name), + ]), + ); + } catch { + // History is decorative; a sealed or full store just means none. + } + }, + + // Pure LRU: every visited page floats, so long use converges on a fully + // most-recently-used browse order (Sublime's model). Names that vanished + // from the menu (ACL change, package removed) drop out here; command rows + // never float, whatever the store says. + paletteBrowseRecents() { + return this.readPaletteRecents().filter((name) => + this.paletteIndex.some( + (page) => page.name === name && !page.mode && !page.isLogout, + ), + ); + }, + matchPaletteEntry(q, page) { if (!q) return { score: 0, ranges: null }; @@ -888,23 +953,37 @@ return baseclass.extend({ return rest && { score: rest.score, ranges: null }; }, - renderPaletteResults(value) { + collectPaletteMatches(value) { let q = value.trim().toLowerCase(); // A leading ">" scopes the list to the command rows (the prototype's // command-only mode); ">" covers CJK IMEs emitting the full-width form. const cmdOnly = q[0] === ">" || q[0] === ">"; if (cmdOnly) q = q.slice(1).trim(); - const theme = localStorage.getItem("aurora.theme") || "device"; + // Browsing floats the recently opened pages; recency rides the same + // stable sort as fuzzy rank, so ties keep menu order and typed queries + // (which leave recents empty) stay pure fuzzy. + const recents = q || cmdOnly ? [] : this.paletteBrowseRecents(); const matches = []; for (const page of this.paletteIndex) { - if (cmdOnly && !page.mode) continue; + if (cmdOnly && !page.mode && !page.isLogout) continue; + if (page.isLogout && !cmdOnly && !q) continue; const m = this.matchPaletteEntry(q, page); - if (m) matches.push({ page, score: m.score, ranges: m.ranges }); + if (!m) continue; + const at = recents.indexOf(page.name); + matches.push({ + page, + score: at < 0 ? m.score : recents.length - at, + ranges: m.ranges, + }); } - // sort() is spec-stable: equal scores keep menu order (and the browse - // list stays in menu order by skipping the sort entirely). - if (q) matches.sort((a, b) => b.score - a.score); + if (q || recents.length) matches.sort((a, b) => b.score - a.score); + return matches; + }, + + renderPaletteResults(value) { + const matches = this.collectPaletteMatches(value); + const theme = localStorage.getItem("aurora.theme") || "device"; this.paletteInput.removeAttribute("aria-activedescendant"); this.paletteList.replaceChildren(); @@ -926,6 +1005,7 @@ return baseclass.extend({ href: page.href || "#", }; if (page.mode) attributes["data-mode"] = page.mode; + else if (!page.isLogout) attributes["data-name"] = page.name; if (current) attributes["aria-current"] = "true"; this.paletteList.appendChild( diff --git a/luci-theme-aurora/.dev/tests/build-performance.test.js b/luci-theme-aurora/.dev/tests/build-performance.test.js index 581dbff6d..f73a7299c 100644 --- a/luci-theme-aurora/.dev/tests/build-performance.test.js +++ b/luci-theme-aurora/.dev/tests/build-performance.test.js @@ -23,7 +23,10 @@ test("production assets stay within raw-transfer budgets", () => { // binds. assert.ok(main <= 193_000, "main.css exceeds 193 KB"); assert.ok(login <= 12_000, "login.css exceeds 12 KB"); - assert.ok(menu <= 21_500, "menu-aurora.js exceeds 21.5 KB"); + // 22K: palette recents (record on pick, pure-LRU browse order, storage + // validation) added ~0.8 KB and the ">" logout command ~0.2 KB. The total + // budget below moved by the same amount. + assert.ok(menu <= 22_000, "menu-aurora.js exceeds 22 KB"); // 15K: the expiry gate, readonly folding, menu.d node css, wildcard // actions, progress bar, visibility gate and contract check added ~3 KB // over the first cut; the total-transfer budget below moved by the same @@ -31,8 +34,8 @@ test("production assets stay within raw-transfer budgets", () => { assert.ok(router <= 15_000, "router-aurora.js exceeds 15 KB"); assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB"); assert.ok( - main + menu + router + font + logo <= 267_000, - "admin assets exceed 267 KB", + main + menu + router + font + logo <= 267_500, + "admin assets exceed 267.5 KB", ); assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB"); }); diff --git a/luci-theme-aurora/.dev/tests/command-palette.test.js b/luci-theme-aurora/.dev/tests/command-palette.test.js index 6f89bd350..9feacbafb 100644 --- a/luci-theme-aurora/.dev/tests/command-palette.test.js +++ b/luci-theme-aurora/.dev/tests/command-palette.test.js @@ -13,7 +13,10 @@ const E = (tagName, attributes, children) => ({ children, }); -const loadMenuModule = () => { +const loadMenuModule = ( + localStorage = {}, + { document = {}, navigator = { platform: "" } } = {}, +) => { const baseclass = { extend(module) { return module; @@ -34,8 +37,9 @@ const loadMenuModule = () => { "document", "window", "localStorage", + "navigator", source, - )(baseclass, ui, E, L, (value) => value, {}, {}, {}); + )(baseclass, ui, E, L, (value) => value, document, {}, localStorage, navigator); }; const menu = loadMenuModule(); @@ -174,3 +178,287 @@ test("skips highlighting when case folding would skew the offsets", () => { test("renders the plain title when there are no ranges", () => { assert.deepEqual(menu.highlightPaletteMatch("Status", null), ["Status"]); }); + +// ---- Recents: recording, storage hygiene, and browse-order floating ---- + +const RECENTS_KEY = "aurora.paletteRecents"; + +const fakeStorage = (initial = {}) => { + const map = new Map(Object.entries(initial)); + return { + getItem: (key) => (map.has(key) ? map.get(key) : null), + setItem: (key, value) => map.set(key, String(value)), + map, + }; +}; + +const navPage = (name, title, group = "Group") => ({ + title, + name, + group, + href: `/${name}`, +}); + +const paletteWith = (storage, index) => { + const module = loadMenuModule(storage); + module.paletteIndex = index; + return module; +}; + +const browseIndex = () => [ + navPage("status/overview", "概览", "状态"), + navPage("status/syslog", "系统日志", "状态"), + navPage("network/iface", "接口", "网络"), + navPage("network/firewall", "防火墙", "网络"), + { title: "浅色模式", name: "theme light", group: "主题", mode: "light" }, +]; + +const stored = (recents) => ({ [RECENTS_KEY]: JSON.stringify(recents) }); + +const names = (matches) => matches.map((match) => match.page.name); + +test("a recorded selection lands at the head of the stored recents", () => { + const storage = fakeStorage(); + const palette = paletteWith(storage, browseIndex()); + + palette.recordPaletteRecent("status/syslog"); + palette.recordPaletteRecent("network/iface"); + + assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [ + "network/iface", + "status/syslog", + ]); +}); + +test("re-recording an entry moves it up instead of duplicating it", () => { + const storage = fakeStorage(stored(["network/iface", "status/syslog"])); + const palette = paletteWith(storage, browseIndex()); + + palette.recordPaletteRecent("status/syslog"); + + assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [ + "status/syslog", + "network/iface", + ]); +}); + +test("the store has no artificial cap — dedupe is the only bound", () => { + const storage = fakeStorage(stored(["h", "g", "f", "e", "d", "c", "b", "a"])); + const palette = paletteWith(storage, browseIndex()); + + palette.recordPaletteRecent("i"); + + assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [ + "i", + "h", + "g", + "f", + "e", + "d", + "c", + "b", + "a", + ]); +}); + +test("corrupt or foreign stored values read as no history", () => { + for (const value of ["not json", '"just a string"', "[1,2,3]", "{}"]) { + const palette = paletteWith( + fakeStorage({ [RECENTS_KEY]: value }), + browseIndex(), + ); + assert.deepEqual(palette.readPaletteRecents(), [], value); + } +}); + +test("a throwing storage neither breaks reads nor records", () => { + const hostile = { + getItem: () => { + throw new Error("privacy mode"); + }, + setItem: () => { + throw new Error("quota"); + }, + }; + const palette = paletteWith(hostile, browseIndex()); + + assert.deepEqual(palette.readPaletteRecents(), []); + assert.doesNotThrow(() => palette.recordPaletteRecent("status/overview")); +}); + +test("browsing floats stored recents in recency order, menu order behind", () => { + const palette = paletteWith( + fakeStorage(stored(["network/iface", "status/syslog"])), + browseIndex(), + ); + + assert.deepEqual(names(palette.collectPaletteMatches("")), [ + "network/iface", + "status/syslog", + "status/overview", + "network/firewall", + "theme light", + ]); +}); + +test("every visited page floats in recency order; vanished names drop", () => { + const wide = [ + navPage("p1", "One"), + navPage("p2", "Two"), + navPage("p3", "Three"), + navPage("p4", "Four"), + navPage("p5", "Five"), + navPage("p6", "Six"), + navPage("p7", "Seven"), + ]; + const palette = paletteWith( + fakeStorage(stored(["removed/page", "p7", "p6", "p5", "p4", "p3", "p2"])), + wide, + ); + + // Pure LRU: all six visited pages float, only p1 keeps its menu slot. + assert.deepEqual(names(palette.collectPaletteMatches("")), [ + "p7", + "p6", + "p5", + "p4", + "p3", + "p2", + "p1", + ]); +}); + +test("a stored name never floats a command row", () => { + const palette = paletteWith( + fakeStorage(stored(["theme light"])), + browseIndex(), + ); + + assert.deepEqual(names(palette.collectPaletteMatches("")), [ + "status/overview", + "status/syslog", + "network/iface", + "network/firewall", + "theme light", + ]); +}); + +test("typed queries rank by fuzzy score alone", () => { + const palette = paletteWith( + fakeStorage(stored(["diag"])), + [navPage("dns", "DNS Settings"), navPage("diag", "Diagnostics")], + ); + + // "Diagnostics" is the recent entry, but the tight title hit must win. + assert.deepEqual(names(palette.collectPaletteMatches("dns")), [ + "dns", + "diag", + ]); +}); + +test("'>' browsing lists command rows only, unaffected by recents", () => { + const palette = paletteWith( + fakeStorage(stored(["network/iface"])), + browseIndex(), + ); + + assert.deepEqual(names(palette.collectPaletteMatches(">")), ["theme light"]); +}); + +// ---- Logout as a ">"-only command ---- + +const logoutIndex = () => [ + navPage("p1", "Alpha"), + navPage("p2", "Beta"), + navPage("p3", "Gamma"), + navPage("p4", "Delta"), + navPage("p5", "Epsilon"), + navPage("p6", "Zeta"), + { title: "浅色模式", name: "theme light", group: "主题", mode: "light" }, + { title: "Logout", name: "logout", group: null, href: "/logout", isLogout: true }, +]; + +test("'>' browsing includes the logout command", () => { + const palette = paletteWith(fakeStorage(), logoutIndex()); + + assert.deepEqual(names(palette.collectPaletteMatches(">")), [ + "theme light", + "logout", + ]); +}); + +test("plain browsing hides the logout command", () => { + const palette = paletteWith(fakeStorage(), logoutIndex()); + + assert.deepEqual(names(palette.collectPaletteMatches("")), [ + "p1", + "p2", + "p3", + "p4", + "p5", + "p6", + "theme light", + ]); +}); + +test("typed queries still reach logout", () => { + const palette = paletteWith(fakeStorage(), logoutIndex()); + + assert.deepEqual(names(palette.collectPaletteMatches("logout")), ["logout"]); +}); + +test("a stored logout name never floats", () => { + const palette = paletteWith( + fakeStorage(stored(["logout", "p6", "p5", "p4", "p3", "p2"])), + logoutIndex(), + ); + + assert.deepEqual(names(palette.collectPaletteMatches("")), [ + "p6", + "p5", + "p4", + "p3", + "p2", + "p1", + "theme light", + ]); +}); + +test("initPalette appends logout as the last command row", () => { + const toggle = { + setAttribute() {}, + addEventListener() {}, + }; + const doc = { + querySelector: (sel) => (sel === "#cmdk-trigger" ? toggle : null), + addEventListener() {}, + }; + const palette = loadMenuModule(fakeStorage(), { document: doc }); + + palette.initPalette([ + { + name: "status", + title: "Status", + hasChildren: true, + pages: [{ name: "overview", title: "Overview", href: "/overview" }], + }, + { + name: "logout", + title: "Logout", + href: "/logout", + hasChildren: false, + isLogout: true, + }, + ]); + + const last = palette.paletteIndex.at(-1); + assert.equal(last.isLogout, true); + assert.equal(last.href, "/logout"); + assert.equal(last.title, "Logout"); + // After the theme commands, so ">" lists modes first, logout last. + assert.ok(palette.paletteIndex.findIndex((page) => page.mode) >= 0); + assert.ok( + palette.paletteIndex.findIndex((page) => page.mode) < + palette.paletteIndex.indexOf(last), + ); +}); diff --git a/luci-theme-aurora/Makefile b/luci-theme-aurora/Makefile index adc3b952d..7eb237046 100644 --- a/luci-theme-aurora/Makefile +++ b/luci-theme-aurora/Makefile @@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS) LUCI_DEPENDS:=+luci-base -PKG_VERSION:=1.2.3 -PKG_RELEASE:=69 +PKG_VERSION:=1.2.4 +PKG_RELEASE:=70 PKG_LICENSE:=Apache-2.0 LUCI_MINIFY_CSS:= diff --git a/luci-theme-aurora/htdocs/luci-static/resources/menu-aurora.js b/luci-theme-aurora/htdocs/luci-static/resources/menu-aurora.js index 5f1e5df6d..f9f647d1e 100644 --- a/luci-theme-aurora/htdocs/luci-static/resources/menu-aurora.js +++ b/luci-theme-aurora/htdocs/luci-static/resources/menu-aurora.js @@ -1 +1 @@ -"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(e=>this.render(e)),this.initNavigationControls(),this.initUciIndicator()},initUciIndicator(){const e=ui.changes?.setIndicator;e&&(ui.changes.setIndicator=function(t){e.call(this,t),document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",t||0)})},initNavigationControls(){const e=document.querySelector("#mobile-menu-overlay"),t=document.querySelector("#navigation-toggle");if(!t||!e)return;const a=e.querySelector("#mobile-nav-list"),i=window.matchMedia("(min-width: 768px)"),n="aurora.sidebarCollapsed";let s;const o=()=>i.matches&&"sidebar"===document.body.dataset.navType,r=e=>{const a=o();t.classList.toggle("is-expanded",e&&!i.matches),t.setAttribute("aria-expanded",e?"true":"false"),t.setAttribute("aria-controls",a?"sidebar-panel":"mobile-menu-overlay");const n=e&&!a?_("Close"):_("Navigation");t.setAttribute("title",n),t.setAttribute("aria-label",n)},l=()=>{e.classList.contains("mobile-menu-open")&&(e.classList.remove("mobile-menu-open"),document.body.classList.remove("mobile-navigation-open"),document.body.style.overflow="",this.resetNavigationGroups(a),r(!1))};this.closeMobileNavigation=l;const c=t=>{if(o()){l();const e=!t,a=document.body;return a.classList.remove("sidebar-anim-open","sidebar-anim-close"),a.classList.add(e?"sidebar-anim-close":"sidebar-anim-open"),clearTimeout(s),s=setTimeout(()=>{a.classList.remove("sidebar-anim-open","sidebar-anim-close")},300),a.classList.toggle("sidebar-collapsed",e),localStorage.setItem(n,e),void r(t)}if(i.matches)return l(),void r(!1);e.classList.toggle("mobile-menu-open",t),document.body.classList.toggle("mobile-navigation-open",t),document.body.style.overflow=t?"hidden":"",t?this.expandActiveNavigationGroup(a):this.resetNavigationGroups(a),r(t)},d=()=>{if(l(),o()){const e="true"===localStorage.getItem(n);return document.body.classList.toggle("sidebar-collapsed",e),void r(!e)}r(!1)};t.addEventListener("click",t=>{t.stopPropagation(),c(!(o()?!document.body.classList.contains("sidebar-collapsed"):!i.matches&&e.classList.contains("mobile-menu-open")))}),e.addEventListener("click",t=>{t.target===e&&c(!1)}),document.addEventListener("keydown",t=>{"Escape"===t.key&&e.classList.contains("mobile-menu-open")&&c(!1)}),i.addEventListener("change",d),d(),document.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");a&&e.contains(a)&&c(!1)})},renderMobileMenu(e){const t=document.querySelector("#mobile-nav-list"),a=document.querySelector("#mobile-nav-footer-action");t&&(t.innerHTML=""),a&&(a.innerHTML=""),t&&(e.forEach(e=>{e.isLogout?a&&a.appendChild(E("a",{class:"mobile-nav-logout",href:e.href},[e.title])):t.appendChild(this.renderNavigationItem(e,"mobile"))}),this.bindNavigationAccordion(t))},render(e){this.tree=e,this.renderModeMenu(e),this.renderTabs(e)},renderTabs(e){if(L.env.dispatchpath.length<3)return;let t=e,a="";for(let e=0;e<3&&t;e++){const i=L.env.dispatchpath[e];t=t.children?.[i],a+=(a?"/":"")+i}t&&this.renderTabMenu(t,a)},syncRoute(){if(!this.tree)return;const e=new URL(L.url(...L.env.dispatchpath.slice(0,3)),location.href).pathname;document.querySelectorAll("#topmenu a, .desktop-menu-canvas a, #sidebar-list a, #mobile-nav-list a").forEach(t=>{const a=t.pathname===e&&!t.getAttribute("href").startsWith("#");t.classList.toggle("is-active-page",a),a?t.setAttribute("aria-current","page"):t.removeAttribute("aria-current")}),document.querySelectorAll(".navigation-group").forEach(e=>{const t=!!e.querySelector("a.is-active-page"),a=e.querySelector(".navigation-group-toggle");e.classList.toggle("is-active-group",t),t?a?.setAttribute("aria-current","location"):a?.removeAttribute("aria-current")});for(const e of[document.querySelector("#sidebar-list"),document.querySelector("#mobile-nav-list")])this.resetNavigationGroups(e),this.expandActiveNavigationGroup(e);this.renderCrumb();const t=document.querySelector("#tabmenu");t&&(t.innerHTML="",t.style.display="none"),this.renderTabs(this.tree)},closeSurfaces(){this.hideDesktopNav(),this.paletteList&&this.closePalette(),this.closeMobileNavigation?.()},renderTabMenu(e,t,a=0){const i=document.querySelector("#tabmenu"),n=E("ul",{class:"tabs"}),s=ui.menu.getChildren(e);let o=null;return s.forEach(e=>{const i=L.env.dispatchpath[3+a]===e.name;n.appendChild(E("li",{class:`tabmenu-item-${e.name}${i?" active":""}`},[E("a",{href:L.url(t,e.name)},[_(e.title)])])),i&&(o=e)}),n.children.length?(i.appendChild(n),i.style.display="",o&&this.renderTabMenu(o,`${t}/${o.name}`,a+1),n):E([])},renderMainMenu(e,t,a=0,i=null){const n=a?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu"),s=ui.menu.getChildren(e);if(a>1)return E([]);if(0===a){const e=document.body?.dataset?.navType||"mega-menu";if("sidebar"===e)return this.renderSidebar(i||[]),n||E([]);if(!n||!s.length)return E([]);"mega-menu"===e?this.initMegaMenu(s,t,n):this.initDropdown(s,t,n)}else{if(!s.length)return E([]);s.forEach(a=>{const i=this.isActivePath(e.name,a.name),s={class:i?"is-active-page":"",href:L.url(t,a.name)};i&&(s["aria-current"]="page"),n.appendChild(E("li",{},[E("a",s,[_(a.title)])]))})}return n.style.display="",n},isActivePath:(e,t)=>L.env.dispatchpath[1]===e&&(null==t||L.env.dispatchpath[2]===t),buildNavigationModel(e,t){return e.filter(e=>e?.name).map(e=>{const a=ui.menu.getChildren(e).filter(e=>e?.name).map(a=>({name:a.name,title:_(a.title),href:L.url(t,e.name,a.name),isActivePage:this.isActivePath(e.name,a.name)})),i=a.length>0,n=this.isActivePath(e.name);return{name:e.name,title:_(e.title),href:L.url(t,e.name),hasChildren:i,isLogout:"logout"===e.name,isActiveGroup:i&&n,isActivePage:!i&&n,activePage:a.find(e=>e.isActivePage)||null,pages:a}})},navigationSubmenuId:(e,t)=>`${e}-submenu-${encodeURIComponent(String(t))}`,renderNavigationItem(e,t){const a="mobile"===t,i=a?"mobile-nav-item":"",n=a?"navigation-direct mobile-nav-link":"navigation-direct nav-link";if(!e.hasChildren){const t={class:`${n}${e.isActivePage?" is-active-page":""}`,href:e.href};return e.isActivePage&&(t["aria-current"]="page"),t["data-section"]=e.name,E("li",{class:i},[E("a",t,[E("span",{class:"nav-icon","aria-hidden":"true"}),E("span",{class:"nav-label"},[e.title])])])}const s=this.navigationSubmenuId(t,e.name),o=["navigation-group",a?"mobile-nav-item":"sidebar-group",e.isActiveGroup?"is-active-group":"",e.isActiveGroup?"is-expanded":""].filter(Boolean),r={class:a?"navigation-group-toggle mobile-nav-link mobile-nav-toggle":"navigation-group-toggle nav-category",type:"button","aria-expanded":e.isActiveGroup?"true":"false","aria-controls":s};e.isActiveGroup&&(r["aria-current"]="location"),r["data-section"]=e.name;const l=E("ul",{class:a?"navigation-submenu-list mobile-nav-submenu-list":"navigation-submenu-list sidebar-submenu"});e.pages.forEach(e=>{const t={class:["navigation-sublink",a?"mobile-nav-sublink":"",e.isActivePage?"is-active-page":""].filter(Boolean).join(" "),href:e.href};e.isActivePage&&(t["aria-current"]="page"),l.appendChild(E("li",{class:a?"mobile-nav-subitem":""},[E("a",t,[e.title])]))});const c={class:a?"navigation-group-region mobile-nav-submenu":"navigation-group-region sidebar-section",id:s,"aria-hidden":e.isActiveGroup?"false":"true"};return e.isActiveGroup||(c.inert=""),E("li",{class:o.join(" ")},[E("button",r,[E("span",{class:"nav-icon","aria-hidden":"true"}),E("span",{class:"nav-category-label"},[e.title])]),E("div",c,[l])])},setNavigationGroupExpanded(e,t){const a=e.querySelector(".navigation-group-toggle"),i=e.querySelector(".navigation-group-region");e.classList.toggle("is-expanded",t),a?.setAttribute("aria-expanded",t?"true":"false"),i?.setAttribute("aria-hidden",t?"false":"true"),t?i?.removeAttribute("inert"):i?.setAttribute("inert","")},setExclusiveNavigationGroupExpanded(e,t,a){e&&t&&(a&&e.querySelectorAll(".navigation-group.is-expanded").forEach(e=>{e!==t&&this.setNavigationGroupExpanded(e,!1)}),this.setNavigationGroupExpanded(t,a))},resetNavigationGroups(e){e?.querySelectorAll(".navigation-group.is-expanded").forEach(e=>this.setNavigationGroupExpanded(e,!1))},expandActiveNavigationGroup(e){const t=e?.querySelector(".navigation-group.is-active-group");t&&this.setExclusiveNavigationGroupExpanded(e,t,!0)},bindNavigationAccordion(e){e&&"true"!==e.dataset.accordionBound&&(e.dataset.accordionBound="true",e.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-group-toggle"),i=a?.closest?.(".navigation-group");i&&e.contains(i)&&(t.preventDefault(),t.stopPropagation(),this.setExclusiveNavigationGroupExpanded(e,i,!i.classList.contains("is-expanded")))}))},renderSidebar(e){const t=document.querySelector("#sidebar-list"),a=document.querySelector("#sidebar-footer");t&&(t.innerHTML=""),a&&(a.innerHTML=""),t&&(e.forEach(e=>{e.isLogout?(a||t).appendChild(E("a",{class:"nav-link",href:e.href},[e.title])):t.appendChild(this.renderNavigationItem(e,"sidebar"))}),this.bindNavigationAccordion(t),this.renderCrumb())},renderCrumb(){const e=document.querySelector("#header-crumb"),t=document.querySelector("#sidebar-list");if(!e||!t)return;const a=[],i=t.querySelector(".is-active-group"),n=t.querySelector(".is-active-page");i&&a.push(i.querySelector(".nav-category-label")?.textContent),n&&n.textContent!==a[0]&&a.push(n.textContent),e.innerHTML="",a.forEach((t,i)=>{i&&e.appendChild(E("li",{class:"crumb-sep"},["/"])),e.appendChild(E("li",{class:i===a.length-1?"current":""},[t]))})},initPalette(e){const t=document.querySelector("#cmdk-trigger");if(!t||this.paletteIndex)return;this.paletteIndex=[],e.forEach(e=>{e.isLogout||(e.hasChildren?e.pages.forEach(t=>this.paletteIndex.push({title:t.title,name:`${e.name}/${t.name}`,group:e.title,href:t.href})):this.paletteIndex.push({title:e.title,name:e.name,group:null,href:e.href}))}),[["light",_("Light Mode")],["dark",_("Dark Mode")],["device",_("Automatic")]].forEach(([e,t])=>this.paletteIndex.push({title:t,name:`theme ${e}`,group:_("Design"),mode:e}));const a=/Mac|iP(ad|hone|od)/.test(navigator.platform);this.paletteKey=a?"⌘K":"Ctrl+K",t.setAttribute("aria-keyshortcuts",a?"Meta+K":"Control+K"),t.setAttribute("aria-expanded","false"),t.addEventListener("click",()=>this.togglePalette()),this.paletteTrigger=t,document.addEventListener("keydown",e=>{e.isComposing||229===e.keyCode||((a?!e.metaKey||e.ctrlKey:!e.ctrlKey||e.metaKey)||e.altKey||e.shiftKey||"k"!==(e.key||"").toLowerCase()?"Escape"===e.key&&this.palettePanel&&!this.palettePanel.hidden&&this.closePalette():(e.preventDefault(),this.togglePalette()))}),this.onPaletteAway=e=>{this.palettePanel.contains(e.target)||t.contains(e.target)||this.closePalette()}},buildPalette(){const e=E("input",{class:"cmdk-input",type:"text",enterkeyhint:"go",placeholder:_("Type to filter…"),"aria-label":_("Type to filter…"),autocomplete:"off",spellcheck:"false",role:"combobox","aria-expanded":"true","aria-autocomplete":"list","aria-controls":"cmdk-list"}),t=E("div",{class:"cmdk-list",id:"cmdk-list",role:"listbox"}),a=E("button",{class:"cmdk-clear",type:"button","aria-label":_("Clear")});a.addEventListener("click",()=>{e.value="",this.renderPaletteResults(""),e.focus()});const i=E("button",{class:"cmdk-cancel",type:"button"},[_("Cancel")]);i.addEventListener("click",()=>this.closePalette());const n=E("div",{id:"cmdk-panel",class:"cmdk-panel",role:"dialog","aria-modal":"true","aria-label":_("Navigation"),hidden:""},[E("div",{class:"cmdk-inputrow"},[e,a,i]),t,E("div",{class:"cmdk-footer"},[E("kbd",{},["↑↓"]),E("kbd",{},["↵"]),E("kbd",{},[">"]),E("kbd",{},["esc"]),E("span",{class:"cmdk-hint-close"},[E("kbd",{},[this.paletteKey])])])]);e.addEventListener("input",()=>this.renderPaletteResults(e.value)),e.addEventListener("keydown",e=>{e.isComposing||229===e.keyCode||("ArrowDown"===e.key||"ArrowUp"===e.key?(e.preventDefault(),this.movePaletteSelection("ArrowDown"===e.key?1:-1)):"Enter"===e.key&&t.querySelector(".is-selected")?.click())}),t.addEventListener("mousemove",e=>{const t=e.target?.closest?.(".cmdk-row");t&&!t.classList.contains("is-selected")&&this.setPaletteSelection(t)}),t.addEventListener("click",a=>{const i=a.target?.closest?.(".cmdk-row")?.dataset.mode;i&&(a.preventDefault(),setTheme(i),syncSwitchers(),this.renderPaletteResults(e.value),this.setPaletteSelection(t.querySelector(`[data-mode="${i}"]`)),e.focus())}),n.addEventListener("keydown",n=>{if("Tab"!==n.key)return;const s=[e,a,...t.querySelectorAll("a"),i].filter(e=>e.getClientRects().length),o=n.shiftKey?s[0]:s.at(-1);document.activeElement===o&&(n.preventDefault(),(n.shiftKey?s.at(-1):s[0]).focus())}),document.body.appendChild(n),this.paletteTrigger.setAttribute("aria-controls",n.id),this.palettePanel=n,this.paletteInput=e,this.paletteList=t},fuzzyMatch(e,t){const a=String(t).toLowerCase();let i=null;for(let t=a.indexOf(e[0]);t>=0;t=a.indexOf(e[0],t+1)){let n=t,s=0,o=0;const r=[];for(const t of e){if(" "===t){o=0;continue}const e=a.indexOf(t,n);if(e<0){r.length=0;break}o=e===n&&r.length?o+1:1,s+=1+o+(e===n&&r.length?4:0)+(0===e||" "===a[e-1]||"/"===a[e-1]?3:0);const i=e+t.length,l=r[r.length-1];l&&l[1]===e?l[1]=i:r.push([e,i]),n=i}s-=.02*a.length,r.length&&(!i||s>i.score)&&(i={score:s,ranges:r})}return i},matchPaletteEntry(e,t){if(!e)return{score:0,ranges:null};const a=this.fuzzyMatch(e,t.title);if(a)return{score:a.score+12,ranges:a.ranges};const i=this.fuzzyMatch(e,t.name)||(t.group?this.fuzzyMatch(e,t.group):null);return i&&{score:i.score,ranges:null}},renderPaletteResults(e){let t=e.trim().toLowerCase();const a=">"===t[0]||">"===t[0];a&&(t=t.slice(1).trim());const i=localStorage.getItem("aurora.theme")||"device",n=[];for(const e of this.paletteIndex){if(a&&!e.mode)continue;const i=this.matchPaletteEntry(t,e);i&&n.push({page:e,score:i.score,ranges:i.ranges})}t&&n.sort((e,t)=>t.score-e.score),this.paletteInput.removeAttribute("aria-activedescendant"),this.paletteList.replaceChildren(),n.length?(n.forEach(({page:e,ranges:t},a)=>{const n=e.mode&&e.mode===i,s={class:"cmdk-row",id:`cmdk-option-${a}`,role:"option","aria-selected":"false",href:e.href||"#"};e.mode&&(s["data-mode"]=e.mode),n&&(s["aria-current"]="true"),this.paletteList.appendChild(E("a",s,[E("span",{class:"cmdk-title"},this.highlightPaletteMatch(e.title,t)),n?E("span",{class:"cmdk-group-name","aria-hidden":"true"},[E("mark",{},["✓"])]):e.group?E("span",{class:"cmdk-group-name"},[e.group]):""]))}),this.setPaletteSelection(this.paletteList.firstChild)):this.paletteList.appendChild(E("div",{class:"cmdk-empty"},[_("No entries available")]))},highlightPaletteMatch(e,t){if(!t||e.toLowerCase().length!==e.length)return[e];const a=[];let i=0;return t.forEach(([t,n])=>{a.push(e.slice(i,t),E("mark",{},[e.slice(t,n)])),i=n}),a.push(e.slice(i)),a},setPaletteSelection(e){const t=this.paletteList.querySelector(".is-selected");t?.classList.remove("is-selected"),t?.setAttribute("aria-selected","false"),e.classList.add("is-selected"),e.setAttribute("aria-selected","true"),this.paletteInput.setAttribute("aria-activedescendant",e.id)},movePaletteSelection(e){const t=[...this.paletteList.querySelectorAll(".cmdk-row")];if(!t.length)return;const a=t.findIndex(e=>e.classList.contains("is-selected")),i=t[(a+e+t.length)%t.length];this.setPaletteSelection(i),i.scrollIntoView({block:"nearest"})},togglePalette(){this.palettePanel||this.buildPalette(),this.palettePanel.hidden?this.openPalette():this.closePalette()},openPalette(){this.paletteReturnFocus=document.activeElement,this.paletteTrigger.setAttribute("aria-expanded","true"),this.palettePanel.hidden=!1,this.paletteInput.value="",this.renderPaletteResults(""),this.paletteInput.focus(),document.addEventListener("pointerdown",this.onPaletteAway)},closePalette(){this.paletteTrigger.setAttribute("aria-expanded","false"),this.palettePanel.hidden=!0,document.removeEventListener("pointerdown",this.onPaletteAway),this.paletteReturnFocus?.isConnected&&this.paletteReturnFocus.focus(),this.paletteReturnFocus=null},buildDropdownItem(e,t,a){const i=ui.menu.getChildren(e),n=i.length>0,s=E("li",{},[E("a",{class:"menu",href:n?"#":L.url(t,e.name)},[_(e.title)])]);a.appendChild(s);const o=s.querySelector("a");let r=null;if(n){const a=this.renderMainMenu(e,`${t}/${e.name}`,1),n=[a];if("mega-menu"===document.body?.dataset?.navType){a.style.setProperty("--menu-rows",Math.max(6,Math.ceil(i.length/4))),n.unshift(E("div",{class:"desktop-nav-anchor"},[E("span",{class:"desktop-nav-title","data-section":e.name},[_(e.title)])]));const t=document.querySelector(".desktop-menu-container > .desktop-menu-board");t&&n.push(t.cloneNode(!0))}r=E("div",{class:"desktop-nav"},n),s.appendChild(r),o.addEventListener("click",e=>e.preventDefault())}return{li:s,nav:r,menuLink:o,hasSubmenu:n}},deactivateDesktopNavExcept(e,t){document.querySelectorAll(".desktop-nav").forEach(t=>{t!==e&&t.classList.remove("active")}),document.querySelectorAll("#topmenu a").forEach(e=>{e!==t&&e.classList.remove("menu-active")})},initMegaMenu(e,t,a){const i=document.querySelector(".desktop-menu-container"),n=i?.querySelector(".desktop-menu-canvas"),s=document.querySelector(".desktop-menu-overlay"),o=document.querySelector("header");if(!o||!s)return;let r=null,l=null,c=0,d=300;const u=()=>{c||(o.querySelectorAll(".desktop-nav").forEach(e=>c=Math.max(c,e.offsetHeight)),d=Math.min(480,Math.max(240,Math.round(c/2)))),o.style.setProperty("--mega-menu-height",`${c}px`),o.style.setProperty("--mega-menu-duration",`${d}ms`)};let m=null;window.addEventListener("resize",()=>{c=0,clearTimeout(m),m=setTimeout(u,150)});let h={x:0,y:0},p=0,v={x:0,y:0};o.addEventListener("mousemove",e=>{e.timeStamp-p>60&&(h=v,p=e.timeStamp),v={x:e.clientX,y:e.clientY}}),e.forEach(e=>{const{li:c,nav:d,menuLink:m,hasSubmenu:p}=this.buildDropdownItem(e,t,a);p&&(n&&n.appendChild(d),c.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null);const e=i?.classList.contains("active"),t=e?(()=>{const e=o.querySelector(".desktop-nav.active");if(!e)return!1;const t=v.x-h.x,a=v.y-h.y;if(a<=2)return!1;const i=e.getBoundingClientRect();if(v.y>=i.top)return!0;const n=v.x+t*(i.top-v.y)/a;return n>=i.left-24&&n<=i.right+24})()?260:0:100;r=setTimeout(()=>{const e=d.classList.contains("active");this.deactivateDesktopNavExcept(d,m),e||(m.classList.add("menu-active"),i&&(u(),i.dataset.closeToken="",i.classList.add("active"),i.classList.remove("closing"),s.classList.add("active")),d.classList.add("active"))},t)}),c.addEventListener("mouseleave",()=>{r&&(clearTimeout(r),r=null)}))});const g=window.requestIdleCallback||(e=>setTimeout(e,1));(document.fonts?.ready||Promise.resolve()).then(()=>g(()=>{c||u()}));const b=()=>{r&&(clearTimeout(r),r=null),l=setTimeout(()=>{this.hideDesktopNav()},150)};o.addEventListener("mouseleave",b),s.addEventListener("mouseenter",b),o.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null)}),s.addEventListener("click",()=>this.hideDesktopNav())},initDropdown(e,t,a){e.forEach(e=>{const{li:i,nav:n,menuLink:s,hasSubmenu:o}=this.buildDropdownItem(e,t,a);if(!o)return;let r=null,l=null;i.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null),r=setTimeout(()=>{this.deactivateDesktopNavExcept(n,s),s.classList.add("menu-active"),n.classList.add("active")},100)}),i.addEventListener("mouseleave",()=>{r&&(clearTimeout(r),r=null),l=setTimeout(()=>{n.classList.remove("active"),s.classList.remove("menu-active")},150)})})},hideDesktopNav(){this.deactivateDesktopNavExcept(null,null);const e=document.querySelector(".desktop-menu-container");if(document.querySelector(".desktop-menu-overlay")?.classList.remove("active"),!e)return;if(!e.classList.contains("active")&&!e.classList.contains("closing"))return;e.classList.add("closing"),e.classList.remove("active");const t=e.querySelector(".desktop-menu-sheet"),a=`${Date.now()}-${Math.random()}`;e.dataset.closeToken=a;let i=null;const n=s=>{if(!s?.target||s.target===t){if(e.dataset.closeToken!==a)return null!==i&&(clearTimeout(i),i=null),void t?.removeEventListener("transitionend",n);null!==i&&(clearTimeout(i),i=null),t?.removeEventListener("transitionend",n),e.classList.contains("active")||(e.classList.remove("closing"),delete e.dataset.closeToken)}};if(!t||window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return void n();t.addEventListener("transitionend",n);const s=parseInt(document.querySelector("header")?.style.getPropertyValue("--mega-menu-duration"))||300;i=setTimeout(n,s+50)},renderModeMenu(e){const t=document.querySelector("#modemenu"),a=ui.menu.getChildren(e);let i=null;if(a.forEach((e,a)=>{const n=L.env.requestpath.length?e.name===L.env.requestpath[0]:0===a;t&&t.appendChild(E("li",{class:n?"active":""},[E("a",{href:L.url(e.name)},[_(e.title)])])),n&&(i=e)}),i){const e=this.buildNavigationModel(ui.menu.getChildren(i),i.name);this.renderMainMenu(i,i.name,0,e),this.renderMobileMenu(e),this.initPalette(e)}t?.children.length>1&&(t.style.display="")}}); \ No newline at end of file +"use strict";"require baseclass";"require ui";const PALETTE_RECENTS_KEY="aurora.paletteRecents";return baseclass.extend({__init__(){ui.menu.load().then(e=>this.render(e)),this.initNavigationControls(),this.initUciIndicator()},initUciIndicator(){const e=ui.changes?.setIndicator;e&&(ui.changes.setIndicator=function(t){e.call(this,t),document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",t||0)})},initNavigationControls(){const e=document.querySelector("#mobile-menu-overlay"),t=document.querySelector("#navigation-toggle");if(!t||!e)return;const a=e.querySelector("#mobile-nav-list"),n=window.matchMedia("(min-width: 768px)"),i="aurora.sidebarCollapsed";let s;const o=()=>n.matches&&"sidebar"===document.body.dataset.navType,r=e=>{const a=o();t.classList.toggle("is-expanded",e&&!n.matches),t.setAttribute("aria-expanded",e?"true":"false"),t.setAttribute("aria-controls",a?"sidebar-panel":"mobile-menu-overlay");const i=e&&!a?_("Close"):_("Navigation");t.setAttribute("title",i),t.setAttribute("aria-label",i)},l=()=>{e.classList.contains("mobile-menu-open")&&(e.classList.remove("mobile-menu-open"),document.body.classList.remove("mobile-navigation-open"),document.body.style.overflow="",this.resetNavigationGroups(a),r(!1))};this.closeMobileNavigation=l;const c=t=>{if(o()){l();const e=!t,a=document.body;return a.classList.remove("sidebar-anim-open","sidebar-anim-close"),a.classList.add(e?"sidebar-anim-close":"sidebar-anim-open"),clearTimeout(s),s=setTimeout(()=>{a.classList.remove("sidebar-anim-open","sidebar-anim-close")},300),a.classList.toggle("sidebar-collapsed",e),localStorage.setItem(i,e),void r(t)}if(n.matches)return l(),void r(!1);e.classList.toggle("mobile-menu-open",t),document.body.classList.toggle("mobile-navigation-open",t),document.body.style.overflow=t?"hidden":"",t?this.expandActiveNavigationGroup(a):this.resetNavigationGroups(a),r(t)},d=()=>{if(l(),o()){const e="true"===localStorage.getItem(i);return document.body.classList.toggle("sidebar-collapsed",e),void r(!e)}r(!1)};t.addEventListener("click",t=>{t.stopPropagation(),c(!(o()?!document.body.classList.contains("sidebar-collapsed"):!n.matches&&e.classList.contains("mobile-menu-open")))}),e.addEventListener("click",t=>{t.target===e&&c(!1)}),document.addEventListener("keydown",t=>{"Escape"===t.key&&e.classList.contains("mobile-menu-open")&&c(!1)}),n.addEventListener("change",d),d(),document.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");a&&e.contains(a)&&c(!1)})},renderMobileMenu(e){const t=document.querySelector("#mobile-nav-list"),a=document.querySelector("#mobile-nav-footer-action");t&&(t.innerHTML=""),a&&(a.innerHTML=""),t&&(e.forEach(e=>{e.isLogout?a&&a.appendChild(E("a",{class:"mobile-nav-logout",href:e.href},[e.title])):t.appendChild(this.renderNavigationItem(e,"mobile"))}),this.bindNavigationAccordion(t))},render(e){this.tree=e,this.renderModeMenu(e),this.renderTabs(e)},renderTabs(e){if(L.env.dispatchpath.length<3)return;let t=e,a="";for(let e=0;e<3&&t;e++){const n=L.env.dispatchpath[e];t=t.children?.[n],a+=(a?"/":"")+n}t&&this.renderTabMenu(t,a)},syncRoute(){if(!this.tree)return;const e=new URL(L.url(...L.env.dispatchpath.slice(0,3)),location.href).pathname;document.querySelectorAll("#topmenu a, .desktop-menu-canvas a, #sidebar-list a, #mobile-nav-list a").forEach(t=>{const a=t.pathname===e&&!t.getAttribute("href").startsWith("#");t.classList.toggle("is-active-page",a),a?t.setAttribute("aria-current","page"):t.removeAttribute("aria-current")}),document.querySelectorAll(".navigation-group").forEach(e=>{const t=!!e.querySelector("a.is-active-page"),a=e.querySelector(".navigation-group-toggle");e.classList.toggle("is-active-group",t),t?a?.setAttribute("aria-current","location"):a?.removeAttribute("aria-current")});for(const e of[document.querySelector("#sidebar-list"),document.querySelector("#mobile-nav-list")])this.resetNavigationGroups(e),this.expandActiveNavigationGroup(e);this.renderCrumb();const t=document.querySelector("#tabmenu");t&&(t.innerHTML="",t.style.display="none"),this.renderTabs(this.tree)},closeSurfaces(){this.hideDesktopNav(),this.paletteList&&this.closePalette(),this.closeMobileNavigation?.()},renderTabMenu(e,t,a=0){const n=document.querySelector("#tabmenu"),i=E("ul",{class:"tabs"}),s=ui.menu.getChildren(e);let o=null;return s.forEach(e=>{const n=L.env.dispatchpath[3+a]===e.name;i.appendChild(E("li",{class:`tabmenu-item-${e.name}${n?" active":""}`},[E("a",{href:L.url(t,e.name)},[_(e.title)])])),n&&(o=e)}),i.children.length?(n.appendChild(i),n.style.display="",o&&this.renderTabMenu(o,`${t}/${o.name}`,a+1),i):E([])},renderMainMenu(e,t,a=0,n=null){const i=a?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu"),s=ui.menu.getChildren(e);if(a>1)return E([]);if(0===a){const e=document.body?.dataset?.navType||"mega-menu";if("sidebar"===e)return this.renderSidebar(n||[]),i||E([]);if(!i||!s.length)return E([]);"mega-menu"===e?this.initMegaMenu(s,t,i):this.initDropdown(s,t,i)}else{if(!s.length)return E([]);s.forEach(a=>{const n=this.isActivePath(e.name,a.name),s={class:n?"is-active-page":"",href:L.url(t,a.name)};n&&(s["aria-current"]="page"),i.appendChild(E("li",{},[E("a",s,[_(a.title)])]))})}return i.style.display="",i},isActivePath:(e,t)=>L.env.dispatchpath[1]===e&&(null==t||L.env.dispatchpath[2]===t),buildNavigationModel(e,t){return e.filter(e=>e?.name).map(e=>{const a=ui.menu.getChildren(e).filter(e=>e?.name).map(a=>({name:a.name,title:_(a.title),href:L.url(t,e.name,a.name),isActivePage:this.isActivePath(e.name,a.name)})),n=a.length>0,i=this.isActivePath(e.name);return{name:e.name,title:_(e.title),href:L.url(t,e.name),hasChildren:n,isLogout:"logout"===e.name,isActiveGroup:n&&i,isActivePage:!n&&i,activePage:a.find(e=>e.isActivePage)||null,pages:a}})},navigationSubmenuId:(e,t)=>`${e}-submenu-${encodeURIComponent(String(t))}`,renderNavigationItem(e,t){const a="mobile"===t,n=a?"mobile-nav-item":"",i=a?"navigation-direct mobile-nav-link":"navigation-direct nav-link";if(!e.hasChildren){const t={class:`${i}${e.isActivePage?" is-active-page":""}`,href:e.href};return e.isActivePage&&(t["aria-current"]="page"),t["data-section"]=e.name,E("li",{class:n},[E("a",t,[E("span",{class:"nav-icon","aria-hidden":"true"}),E("span",{class:"nav-label"},[e.title])])])}const s=this.navigationSubmenuId(t,e.name),o=["navigation-group",a?"mobile-nav-item":"sidebar-group",e.isActiveGroup?"is-active-group":"",e.isActiveGroup?"is-expanded":""].filter(Boolean),r={class:a?"navigation-group-toggle mobile-nav-link mobile-nav-toggle":"navigation-group-toggle nav-category",type:"button","aria-expanded":e.isActiveGroup?"true":"false","aria-controls":s};e.isActiveGroup&&(r["aria-current"]="location"),r["data-section"]=e.name;const l=E("ul",{class:a?"navigation-submenu-list mobile-nav-submenu-list":"navigation-submenu-list sidebar-submenu"});e.pages.forEach(e=>{const t={class:["navigation-sublink",a?"mobile-nav-sublink":"",e.isActivePage?"is-active-page":""].filter(Boolean).join(" "),href:e.href};e.isActivePage&&(t["aria-current"]="page"),l.appendChild(E("li",{class:a?"mobile-nav-subitem":""},[E("a",t,[e.title])]))});const c={class:a?"navigation-group-region mobile-nav-submenu":"navigation-group-region sidebar-section",id:s,"aria-hidden":e.isActiveGroup?"false":"true"};return e.isActiveGroup||(c.inert=""),E("li",{class:o.join(" ")},[E("button",r,[E("span",{class:"nav-icon","aria-hidden":"true"}),E("span",{class:"nav-category-label"},[e.title])]),E("div",c,[l])])},setNavigationGroupExpanded(e,t){const a=e.querySelector(".navigation-group-toggle"),n=e.querySelector(".navigation-group-region");e.classList.toggle("is-expanded",t),a?.setAttribute("aria-expanded",t?"true":"false"),n?.setAttribute("aria-hidden",t?"false":"true"),t?n?.removeAttribute("inert"):n?.setAttribute("inert","")},setExclusiveNavigationGroupExpanded(e,t,a){e&&t&&(a&&e.querySelectorAll(".navigation-group.is-expanded").forEach(e=>{e!==t&&this.setNavigationGroupExpanded(e,!1)}),this.setNavigationGroupExpanded(t,a))},resetNavigationGroups(e){e?.querySelectorAll(".navigation-group.is-expanded").forEach(e=>this.setNavigationGroupExpanded(e,!1))},expandActiveNavigationGroup(e){const t=e?.querySelector(".navigation-group.is-active-group");t&&this.setExclusiveNavigationGroupExpanded(e,t,!0)},bindNavigationAccordion(e){e&&"true"!==e.dataset.accordionBound&&(e.dataset.accordionBound="true",e.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-group-toggle"),n=a?.closest?.(".navigation-group");n&&e.contains(n)&&(t.preventDefault(),t.stopPropagation(),this.setExclusiveNavigationGroupExpanded(e,n,!n.classList.contains("is-expanded")))}))},renderSidebar(e){const t=document.querySelector("#sidebar-list"),a=document.querySelector("#sidebar-footer");t&&(t.innerHTML=""),a&&(a.innerHTML=""),t&&(e.forEach(e=>{e.isLogout?(a||t).appendChild(E("a",{class:"nav-link",href:e.href},[e.title])):t.appendChild(this.renderNavigationItem(e,"sidebar"))}),this.bindNavigationAccordion(t),this.renderCrumb())},renderCrumb(){const e=document.querySelector("#header-crumb"),t=document.querySelector("#sidebar-list");if(!e||!t)return;const a=[],n=t.querySelector(".is-active-group"),i=t.querySelector(".is-active-page");n&&a.push(n.querySelector(".nav-category-label")?.textContent),i&&i.textContent!==a[0]&&a.push(i.textContent),e.innerHTML="",a.forEach((t,n)=>{n&&e.appendChild(E("li",{class:"crumb-sep"},["/"])),e.appendChild(E("li",{class:n===a.length-1?"current":""},[t]))})},initPalette(e){const t=document.querySelector("#cmdk-trigger");if(!t||this.paletteIndex)return;this.paletteIndex=[];let a=null;e.forEach(e=>{e.isLogout?a=e:e.hasChildren?e.pages.forEach(t=>this.paletteIndex.push({title:t.title,name:`${e.name}/${t.name}`,group:e.title,href:t.href})):this.paletteIndex.push({title:e.title,name:e.name,group:null,href:e.href})}),[["light",_("Light Mode")],["dark",_("Dark Mode")],["device",_("Automatic")]].forEach(([e,t])=>this.paletteIndex.push({title:t,name:`theme ${e}`,group:_("Design"),mode:e})),a&&this.paletteIndex.push({title:a.title,name:a.name,group:null,href:a.href,isLogout:!0});const n=/Mac|iP(ad|hone|od)/.test(navigator.platform);this.paletteKey=n?"⌘K":"Ctrl+K",t.setAttribute("aria-keyshortcuts",n?"Meta+K":"Control+K"),t.setAttribute("aria-expanded","false"),t.addEventListener("click",()=>this.togglePalette()),this.paletteTrigger=t,document.addEventListener("keydown",e=>{e.isComposing||229===e.keyCode||((n?!e.metaKey||e.ctrlKey:!e.ctrlKey||e.metaKey)||e.altKey||e.shiftKey||"k"!==(e.key||"").toLowerCase()?"Escape"===e.key&&this.palettePanel&&!this.palettePanel.hidden&&this.closePalette():(e.preventDefault(),this.togglePalette()))}),this.onPaletteAway=e=>{this.palettePanel.contains(e.target)||t.contains(e.target)||this.closePalette()}},buildPalette(){const e=E("input",{class:"cmdk-input",type:"text",enterkeyhint:"go",placeholder:_("Type to filter…"),"aria-label":_("Type to filter…"),autocomplete:"off",spellcheck:"false",role:"combobox","aria-expanded":"true","aria-autocomplete":"list","aria-controls":"cmdk-list"}),t=E("div",{class:"cmdk-list",id:"cmdk-list",role:"listbox"}),a=E("button",{class:"cmdk-clear",type:"button","aria-label":_("Clear")});a.addEventListener("click",()=>{e.value="",this.renderPaletteResults(""),e.focus()});const n=E("button",{class:"cmdk-cancel",type:"button"},[_("Cancel")]);n.addEventListener("click",()=>this.closePalette());const i=E("div",{id:"cmdk-panel",class:"cmdk-panel",role:"dialog","aria-modal":"true","aria-label":_("Navigation"),hidden:""},[E("div",{class:"cmdk-inputrow"},[e,a,n]),t,E("div",{class:"cmdk-footer"},[E("kbd",{},["↑↓"]),E("kbd",{},["↵"]),E("kbd",{},[">"]),E("kbd",{},["esc"]),E("span",{class:"cmdk-hint-close"},[E("kbd",{},[this.paletteKey])])])]);e.addEventListener("input",()=>this.renderPaletteResults(e.value)),e.addEventListener("keydown",e=>{e.isComposing||229===e.keyCode||("ArrowDown"===e.key||"ArrowUp"===e.key?(e.preventDefault(),this.movePaletteSelection("ArrowDown"===e.key?1:-1)):"Enter"===e.key&&t.querySelector(".is-selected")?.click())}),t.addEventListener("mousemove",e=>{const t=e.target?.closest?.(".cmdk-row");t&&!t.classList.contains("is-selected")&&this.setPaletteSelection(t)}),t.addEventListener("click",a=>{const n=a.target?.closest?.(".cmdk-row");if(!n)return;const i=n.dataset.mode;i?(a.preventDefault(),setTheme(i),syncSwitchers(),this.renderPaletteResults(e.value),this.setPaletteSelection(t.querySelector(`[data-mode="${i}"]`)),e.focus()):n.dataset.name&&this.recordPaletteRecent(n.dataset.name)}),i.addEventListener("keydown",i=>{if("Tab"!==i.key)return;const s=[e,a,...t.querySelectorAll("a"),n].filter(e=>e.getClientRects().length),o=i.shiftKey?s[0]:s.at(-1);document.activeElement===o&&(i.preventDefault(),(i.shiftKey?s.at(-1):s[0]).focus())}),document.body.appendChild(i),this.paletteTrigger.setAttribute("aria-controls",i.id),this.palettePanel=i,this.paletteInput=e,this.paletteList=t},fuzzyMatch(e,t){const a=String(t).toLowerCase();let n=null;for(let t=a.indexOf(e[0]);t>=0;t=a.indexOf(e[0],t+1)){let i=t,s=0,o=0;const r=[];for(const t of e){if(" "===t){o=0;continue}const e=a.indexOf(t,i);if(e<0){r.length=0;break}o=e===i&&r.length?o+1:1,s+=1+o+(e===i&&r.length?4:0)+(0===e||" "===a[e-1]||"/"===a[e-1]?3:0);const n=e+t.length,l=r[r.length-1];l&&l[1]===e?l[1]=n:r.push([e,n]),i=n}s-=.02*a.length,r.length&&(!n||s>n.score)&&(n={score:s,ranges:r})}return n},readPaletteRecents(){try{const e=JSON.parse(localStorage.getItem(PALETTE_RECENTS_KEY));return Array.isArray(e)?e.filter(e=>"string"==typeof e):[]}catch{return[]}},recordPaletteRecent(e){try{localStorage.setItem(PALETTE_RECENTS_KEY,JSON.stringify([e,...this.readPaletteRecents().filter(t=>t!==e)]))}catch{}},paletteBrowseRecents(){return this.readPaletteRecents().filter(e=>this.paletteIndex.some(t=>t.name===e&&!t.mode&&!t.isLogout))},matchPaletteEntry(e,t){if(!e)return{score:0,ranges:null};const a=this.fuzzyMatch(e,t.title);if(a)return{score:a.score+12,ranges:a.ranges};const n=this.fuzzyMatch(e,t.name)||(t.group?this.fuzzyMatch(e,t.group):null);return n&&{score:n.score,ranges:null}},collectPaletteMatches(e){let t=e.trim().toLowerCase();const a=">"===t[0]||">"===t[0];a&&(t=t.slice(1).trim());const n=t||a?[]:this.paletteBrowseRecents(),i=[];for(const e of this.paletteIndex){if(a&&!e.mode&&!e.isLogout)continue;if(e.isLogout&&!a&&!t)continue;const s=this.matchPaletteEntry(t,e);if(!s)continue;const o=n.indexOf(e.name);i.push({page:e,score:o<0?s.score:n.length-o,ranges:s.ranges})}return(t||n.length)&&i.sort((e,t)=>t.score-e.score),i},renderPaletteResults(e){const t=this.collectPaletteMatches(e),a=localStorage.getItem("aurora.theme")||"device";this.paletteInput.removeAttribute("aria-activedescendant"),this.paletteList.replaceChildren(),t.length?(t.forEach(({page:e,ranges:t},n)=>{const i=e.mode&&e.mode===a,s={class:"cmdk-row",id:`cmdk-option-${n}`,role:"option","aria-selected":"false",href:e.href||"#"};e.mode?s["data-mode"]=e.mode:e.isLogout||(s["data-name"]=e.name),i&&(s["aria-current"]="true"),this.paletteList.appendChild(E("a",s,[E("span",{class:"cmdk-title"},this.highlightPaletteMatch(e.title,t)),i?E("span",{class:"cmdk-group-name","aria-hidden":"true"},[E("mark",{},["✓"])]):e.group?E("span",{class:"cmdk-group-name"},[e.group]):""]))}),this.setPaletteSelection(this.paletteList.firstChild)):this.paletteList.appendChild(E("div",{class:"cmdk-empty"},[_("No entries available")]))},highlightPaletteMatch(e,t){if(!t||e.toLowerCase().length!==e.length)return[e];const a=[];let n=0;return t.forEach(([t,i])=>{a.push(e.slice(n,t),E("mark",{},[e.slice(t,i)])),n=i}),a.push(e.slice(n)),a},setPaletteSelection(e){const t=this.paletteList.querySelector(".is-selected");t?.classList.remove("is-selected"),t?.setAttribute("aria-selected","false"),e.classList.add("is-selected"),e.setAttribute("aria-selected","true"),this.paletteInput.setAttribute("aria-activedescendant",e.id)},movePaletteSelection(e){const t=[...this.paletteList.querySelectorAll(".cmdk-row")];if(!t.length)return;const a=t.findIndex(e=>e.classList.contains("is-selected")),n=t[(a+e+t.length)%t.length];this.setPaletteSelection(n),n.scrollIntoView({block:"nearest"})},togglePalette(){this.palettePanel||this.buildPalette(),this.palettePanel.hidden?this.openPalette():this.closePalette()},openPalette(){this.paletteReturnFocus=document.activeElement,this.paletteTrigger.setAttribute("aria-expanded","true"),this.palettePanel.hidden=!1,this.paletteInput.value="",this.renderPaletteResults(""),this.paletteInput.focus(),document.addEventListener("pointerdown",this.onPaletteAway)},closePalette(){this.paletteTrigger.setAttribute("aria-expanded","false"),this.palettePanel.hidden=!0,document.removeEventListener("pointerdown",this.onPaletteAway),this.paletteReturnFocus?.isConnected&&this.paletteReturnFocus.focus(),this.paletteReturnFocus=null},buildDropdownItem(e,t,a){const n=ui.menu.getChildren(e),i=n.length>0,s=E("li",{},[E("a",{class:"menu",href:i?"#":L.url(t,e.name)},[_(e.title)])]);a.appendChild(s);const o=s.querySelector("a");let r=null;if(i){const a=this.renderMainMenu(e,`${t}/${e.name}`,1),i=[a];if("mega-menu"===document.body?.dataset?.navType){a.style.setProperty("--menu-rows",Math.max(6,Math.ceil(n.length/4))),i.unshift(E("div",{class:"desktop-nav-anchor"},[E("span",{class:"desktop-nav-title","data-section":e.name},[_(e.title)])]));const t=document.querySelector(".desktop-menu-container > .desktop-menu-board");t&&i.push(t.cloneNode(!0))}r=E("div",{class:"desktop-nav"},i),s.appendChild(r),o.addEventListener("click",e=>e.preventDefault())}return{li:s,nav:r,menuLink:o,hasSubmenu:i}},deactivateDesktopNavExcept(e,t){document.querySelectorAll(".desktop-nav").forEach(t=>{t!==e&&t.classList.remove("active")}),document.querySelectorAll("#topmenu a").forEach(e=>{e!==t&&e.classList.remove("menu-active")})},initMegaMenu(e,t,a){const n=document.querySelector(".desktop-menu-container"),i=n?.querySelector(".desktop-menu-canvas"),s=document.querySelector(".desktop-menu-overlay"),o=document.querySelector("header");if(!o||!s)return;let r=null,l=null,c=0,d=300;const u=()=>{c||(o.querySelectorAll(".desktop-nav").forEach(e=>c=Math.max(c,e.offsetHeight)),d=Math.min(480,Math.max(240,Math.round(c/2)))),o.style.setProperty("--mega-menu-height",`${c}px`),o.style.setProperty("--mega-menu-duration",`${d}ms`)};let h=null;window.addEventListener("resize",()=>{c=0,clearTimeout(h),h=setTimeout(u,150)});let m={x:0,y:0},p=0,g={x:0,y:0};o.addEventListener("mousemove",e=>{e.timeStamp-p>60&&(m=g,p=e.timeStamp),g={x:e.clientX,y:e.clientY}}),e.forEach(e=>{const{li:c,nav:d,menuLink:h,hasSubmenu:p}=this.buildDropdownItem(e,t,a);p&&(i&&i.appendChild(d),c.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null);const e=n?.classList.contains("active"),t=e?(()=>{const e=o.querySelector(".desktop-nav.active");if(!e)return!1;const t=g.x-m.x,a=g.y-m.y;if(a<=2)return!1;const n=e.getBoundingClientRect();if(g.y>=n.top)return!0;const i=g.x+t*(n.top-g.y)/a;return i>=n.left-24&&i<=n.right+24})()?260:0:100;r=setTimeout(()=>{const e=d.classList.contains("active");this.deactivateDesktopNavExcept(d,h),e||(h.classList.add("menu-active"),n&&(u(),n.dataset.closeToken="",n.classList.add("active"),n.classList.remove("closing"),s.classList.add("active")),d.classList.add("active"))},t)}),c.addEventListener("mouseleave",()=>{r&&(clearTimeout(r),r=null)}))});const v=window.requestIdleCallback||(e=>setTimeout(e,1));(document.fonts?.ready||Promise.resolve()).then(()=>v(()=>{c||u()}));const b=()=>{r&&(clearTimeout(r),r=null),l=setTimeout(()=>{this.hideDesktopNav()},150)};o.addEventListener("mouseleave",b),s.addEventListener("mouseenter",b),o.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null)}),s.addEventListener("click",()=>this.hideDesktopNav())},initDropdown(e,t,a){e.forEach(e=>{const{li:n,nav:i,menuLink:s,hasSubmenu:o}=this.buildDropdownItem(e,t,a);if(!o)return;let r=null,l=null;n.addEventListener("mouseenter",()=>{l&&(clearTimeout(l),l=null),r=setTimeout(()=>{this.deactivateDesktopNavExcept(i,s),s.classList.add("menu-active"),i.classList.add("active")},100)}),n.addEventListener("mouseleave",()=>{r&&(clearTimeout(r),r=null),l=setTimeout(()=>{i.classList.remove("active"),s.classList.remove("menu-active")},150)})})},hideDesktopNav(){this.deactivateDesktopNavExcept(null,null);const e=document.querySelector(".desktop-menu-container");if(document.querySelector(".desktop-menu-overlay")?.classList.remove("active"),!e)return;if(!e.classList.contains("active")&&!e.classList.contains("closing"))return;e.classList.add("closing"),e.classList.remove("active");const t=e.querySelector(".desktop-menu-sheet"),a=`${Date.now()}-${Math.random()}`;e.dataset.closeToken=a;let n=null;const i=s=>{if(!s?.target||s.target===t){if(e.dataset.closeToken!==a)return null!==n&&(clearTimeout(n),n=null),void t?.removeEventListener("transitionend",i);null!==n&&(clearTimeout(n),n=null),t?.removeEventListener("transitionend",i),e.classList.contains("active")||(e.classList.remove("closing"),delete e.dataset.closeToken)}};if(!t||window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)return void i();t.addEventListener("transitionend",i);const s=parseInt(document.querySelector("header")?.style.getPropertyValue("--mega-menu-duration"))||300;n=setTimeout(i,s+50)},renderModeMenu(e){const t=document.querySelector("#modemenu"),a=ui.menu.getChildren(e);let n=null;if(a.forEach((e,a)=>{const i=L.env.requestpath.length?e.name===L.env.requestpath[0]:0===a;t&&t.appendChild(E("li",{class:i?"active":""},[E("a",{href:L.url(e.name)},[_(e.title)])])),i&&(n=e)}),n){const e=this.buildNavigationModel(ui.menu.getChildren(n),n.name);this.renderMainMenu(n,n.name,0,e),this.renderMobileMenu(e),this.initPalette(e)}t?.children.length>1&&(t.style.display="")}}); \ No newline at end of file diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 4679b9663..990200f93 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=29 +PKG_RELEASE:=30 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index 49daa1b4a..3ea3d96d7 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -86,11 +86,53 @@ function fittersEnabled() { function run() { if (!fittersEnabled()) return; runAll(_fitters, 'fitter'); - /* the page is settled now, so this is the position the next mutation must be measured against — - * unless a correction is already on its way, which would make this reference the drifted one */ + /* the page is settled now: this is the height the NEXT tick may not go below, and the position + * the next mutation must be measured against — the latter unless a correction is already on its + * way, which would make this reference the drifted one */ + holdFloor(); if (!_anchorPending) rememberRest(); } +/* ---- THE DOCUMENT MAY NOT GET SHORTER WHILE A TICK IS IN FLIGHT ---- + * + * The correction below puts a reader back after the engine has moved them. This is the other half, + * and it is the half that means they were never moved: `dom.content()` — what every LuCI poll calls + * to refresh a section — empties the container before it refills it, and a document that is briefly + * shorter than the offset the reader is at is a document the engine clamps the offset into. Nothing + * puts that back afterwards, because the shortening was never real. + * + * So the CONTENT COLUMN keeps a floor: `min-height`, set to the height it had at the last settled + * moment and held until the next one. A section emptying inside it takes nothing off the document, + * there is no shorter document to clamp into, and the tick is invisible. The floor is re-measured + * after every settled batch, so a page that genuinely got shorter is shorter one frame later. + * + * WHY THE FLOOR IS ON `.fs-content` AND NOT ON THE CONTAINER BEING SWAPPED. A version of this + * shipped in fs-overview.js, pinning the container and releasing it in the same statement sequence — + * `dom.content()` performs no layout, so no layout ever saw the pin and it did nothing at all + * (measured: 1882px still clamped away with the pin in place). The fix reported from the field wraps + * `dom.content()` itself and releases two frames later, which does work — at the price of patching + * a luci-base API every app on the router shares, and up to seven read/write pairs per call. One + * element that the theme owns, measured once per settled batch, is the same protection: measured on + * a live router with the corrections switched off, the wrapper and this floor both hold the reader + * at 0px against a 337px drift without either, at 16 measurements against 154 wrapped calls. + * + * NOT WHILE THE READER SCROLLS. Clearing the floor to re-measure is a layout read, and the flick is + * where this file spends its comments avoiding those; the floor simply stays where it was, which is + * still a floor. */ +function holdFloor() { + /* ONLY WHERE THE THEME IS RESPONSIBLE, same rule as the correction and for a measured reason: an + * engine that anchors by itself reads the held height as one more thing that moved. On Chromium + * in the sidebar layout at 1440 the reader ended 15px off with the floor held and 0px without it, + * reproducibly, while the floor bought that engine nothing — it puts the reader back on its own. */ + if (ENGINE_ANCHORS || scrolling()) return; + const el = document.querySelector('.fs-content'); + if (!el) return; /* the login page has no content column */ + /* cleared BEFORE the read, or the floor would measure itself and never come down */ + el.style.minHeight = ''; + const h = el.offsetHeight; + if (h > 0) el.style.minHeight = h + 'px'; +} + /* ---- IS THE PAGE MOVING RIGHT NOW? — ASKED OF THE POSITION, NEVER OF THE EVENTS ---- * * A pass that reads layout must not run while the page moves: on a phone every such read forces a @@ -193,7 +235,9 @@ function sampleMotion() { } if (scrolling()) { requestAnimationFrame(sampleMotion); return; } _sampling = false; - /* the reader has stopped: this is a still moment, and the reference for the next tick */ + /* the reader has stopped: this is a still moment, so the floor and the reference both belong to + * where the page now stands */ + holdFloor(); rememberRest(); /* the page has held still for SCROLL_IDLE: whatever was put off may run now */ if (_deferred) { diff --git a/opera-proxy/Makefile b/opera-proxy/Makefile index b8d15b882..1e83d02aa 100644 --- a/opera-proxy/Makefile +++ b/opera-proxy/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=opera-proxy -PKG_VERSION:=1.28.0 -PKG_RELEASE:=9 +PKG_VERSION:=1.29.0 +PKG_RELEASE:=10 PKG_MAINTAINER:=Konstantine Shevlakov PKG_LICENSE:=MIT