🎨 Sync 2026-08-20 20:39:01

This commit is contained in:
github-actions[bot] committed 2026-08-20 20:39:01 +08:00
1 parent 9022c43ed2
commit 289ca3521f
14 files changed
+465 -49

No files matched your search

+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
@@ -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
@@ -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 |
@@ -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 <body> 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(
@@ -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");
});
@@ -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),
);
});
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_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:=
@@ -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)}Line truncated
"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?Line truncated
+1 -1
View File
@@ -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
@@ -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) {
+2 -2
View File
@@ -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 <shevlakov@132lan.ru>
PKG_LICENSE:=MIT