mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-28 23:05:13 +08:00
🎨 Sync 2026-09-17 01:27:32
This commit is contained in:
1 parent
fe7c1ed189
commit
d9eabd07fb
19 files changed
+988
-70
No files matched your search
@@ -11,12 +11,12 @@ address, so they stay local).
|
||||
|
||||
| Metric | Budget | Track | Source |
|
||||
|---|---|---|---|
|
||||
| main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-09-16 (189,650 B; −0.77 KB sidebar push as two transitions instead of run keyframes; +42 B replayed nav skips the fade; +~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; −1.24 KB unblurred mega-menu curtain, delayed visibility, instant nav-pill colours) |
|
||||
| main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-09-17 (190,230 B; +0.6 KB palette tab rows "Parent › Title", unnested short selectors; −0.77 KB sidebar push as two transitions instead of run keyframes; +42 B replayed nav skips the fade; +~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; −1.24 KB unblurred mega-menu curtain, delayed visibility, instant nav-pill colours) |
|
||||
| login.css (identity/raw) | ≤ 12 KB | size | production build, 2026-07 (10,935 B, token-pruned) |
|
||||
| menu-aurora.js (identity/raw) | ≤ 22 KB | size | production build, 2026-09-16 (21,362 B; −0.24 KB sidebar toggle is a class flip; −0.09 KB pre-paint nav cache, its crumb renderer moved into header.ut (the replay itself is per-response HTML, see below); +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) |
|
||||
| menu-aurora.js (identity/raw) | ≤ 23 KB | size | production build, 2026-09-17 (22,665 B; +1.3 KB palette tabs: third-level nodes, redirect parents folded into their tabs, legacy recents mapped, parent-aware scoring, path words matched segment by segment; −0.24 KB sidebar toggle is a class flip; −0.09 KB pre-paint nav cache, its crumb renderer moved into header.ut (the replay itself is per-response HTML, see below); +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) | ≤ 16 KB | size | production build, 2026-09-13 (15,771 B, shipped from @eamonxg/luci-theme-devkit; 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.5 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-09-16 (265,420 B; −1.1 KB sidebar push and nav cache; the router is a one-time cost that removes per-click dispatcher work; +1.0 KB palette recents and logout command; −1.24 KB mega-menu curtain) |
|
||||
| Core admin cold theme assets (identity/raw) | ≤ 268.5 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-09-17 (267,303 B; +1.9 KB palette tabs; −1.1 KB sidebar push and nav cache; the router is a one-time cost that removes per-click dispatcher work; +1.0 KB palette recents and logout command; −1.24 KB mega-menu curtain) |
|
||||
| Login cold theme assets, excluding configured background (identity/raw) | ≤ 55 KB | size | login CSS + default font + logo, 2026-07 (49,572 B) |
|
||||
| Admin HTML inline scripts added per full load (identity, never cached) | — | size | header.ut, 2026-09-16: pre-paint nav replay ≈ +0.5 KB (mega-menu/dropdown) / ≈ +1.9 KB (sidebar, with the shared crumb renderer); login page +0.07 KB. Template shells the router fetches carry it too. |
|
||||
| Blocking requests before first paint | ≤ 4 | count | current waterfall |
|
||||
|
||||
@@ -57,8 +57,8 @@ No `@layer` wrappers in partials; `main.css` import order is cascade order; toke
|
||||
from `@eamonxg/luci-theme-tokens/dist/aurora/tokens.css` (change colours in that repo).
|
||||
Budgets (`tests/build-performance.test.js`, mirrored in
|
||||
`.claude/skills/aurora-performance/references/aurora-budgets.md`): main.css ≤ 193 KB,
|
||||
login.css ≤ 12 KB, menu-aurora.js ≤ 22 KB, router-aurora.js ≤ 15 KB, admin cold set
|
||||
≤ 267.5 KB, login set ≤ 55 KB, SVG data URLs ≤ 17 KB and unique.
|
||||
login.css ≤ 12 KB, menu-aurora.js ≤ 23 KB, router-aurora.js ≤ 16 KB, admin cold set
|
||||
≤ 268.5 KB, login set ≤ 55 KB, SVG data URLs ≤ 17 KB and unique.
|
||||
|
||||
## Patches contract (third-party compatibility)
|
||||
File name = the page's `data-page` prefix; prefix matching on segment boundaries; both
|
||||
|
||||
@@ -130,6 +130,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* A tab row reads "Parent › Title": the parent gives way first, and keeps
|
||||
at least an ellipsis so the "›" never leads the row. Like the section
|
||||
label, it stays subtle on the selected row. Unnested: the classes exist
|
||||
only inside palette rows, and the short selectors keep main.css lean. */
|
||||
.cmdk-title:has(> .cmdk-parent) {
|
||||
@apply flex items-baseline;
|
||||
}
|
||||
|
||||
.cmdk-parent {
|
||||
@apply text-text-subtle min-w-0 truncate font-normal;
|
||||
}
|
||||
|
||||
.cmdk-label {
|
||||
@apply max-w-[calc(100%-1.5rem)] shrink-0 truncate;
|
||||
|
||||
&::before {
|
||||
@apply text-text-subtle px-1.5 font-normal content-['›'];
|
||||
}
|
||||
}
|
||||
|
||||
/* The frost is decorative — both degradation paths go opaque. */
|
||||
@supports not (backdrop-filter: blur(1px)) {
|
||||
.cmdk-panel {
|
||||
|
||||
@@ -584,11 +584,12 @@ return baseclass.extend({
|
||||
// is the navigation model the menus already render from — no extra
|
||||
// requests, no DOM scraping — and the panel DOM is built lazily on first
|
||||
// open, so pages where it is never used pay nothing beyond this flat array.
|
||||
initPalette(items) {
|
||||
initPalette(items, root) {
|
||||
const toggle = document.querySelector("#cmdk-trigger");
|
||||
if (!toggle || this.paletteIndex) return;
|
||||
|
||||
this.paletteIndex = [];
|
||||
this.paletteAliases = {};
|
||||
let logout = null;
|
||||
items.forEach((item) => {
|
||||
if (item.isLogout) {
|
||||
@@ -604,17 +605,44 @@ return baseclass.extend({
|
||||
});
|
||||
return;
|
||||
}
|
||||
item.pages.forEach((page) =>
|
||||
this.paletteIndex.push({
|
||||
title: page.title,
|
||||
// Section-qualified: "status/overview" keeps English dispatch
|
||||
// segments matchable under any UI language, and the "/" is what
|
||||
// arms the scorer's segment-start bonus.
|
||||
name: `${item.name}/${page.name}`,
|
||||
group: item.title,
|
||||
href: page.href,
|
||||
}),
|
||||
);
|
||||
item.pages.forEach((page) => {
|
||||
// Dispatch paths keep English segments matchable under any UI
|
||||
// language.
|
||||
const name = `${item.name}/${page.name}`;
|
||||
// The raw node: getChildren() hands out alias nodes carrying their
|
||||
// target's children, which would hide an alias parent's tabs.
|
||||
const node = root?.children?.[item.name]?.children?.[page.name] ?? {};
|
||||
const tabs = ui.menu.getChildren(node);
|
||||
const type = node.action?.type;
|
||||
const target =
|
||||
type === "alias"
|
||||
? node.action.path.replace(`${root.name}/${name}/`, "")
|
||||
: type === "firstchild" &&
|
||||
tabs.find((tab) => !tab.firstchild_ineligible)?.name;
|
||||
|
||||
// A parent that only redirects to one of its tabs is that tab; its
|
||||
// row gives way to the tabs, and its name (stored by older recents)
|
||||
// resolves to where it redirects.
|
||||
if (tabs.some((tab) => tab.name === target))
|
||||
this.paletteAliases[name] = `${name}/${target}`;
|
||||
else
|
||||
this.paletteIndex.push({
|
||||
title: page.title,
|
||||
name,
|
||||
group: item.title,
|
||||
href: page.href,
|
||||
});
|
||||
|
||||
tabs.forEach((tab) =>
|
||||
this.paletteIndex.push({
|
||||
title: _(tab.title),
|
||||
parent: page.title,
|
||||
name: `${name}/${tab.name}`,
|
||||
group: item.title,
|
||||
href: L.url(root.name, item.name, page.name, tab.name),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// The only non-navigation commands: theme modes. They ride the same
|
||||
@@ -887,17 +915,20 @@ return baseclass.extend({
|
||||
return best;
|
||||
},
|
||||
|
||||
// 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")
|
||||
: [];
|
||||
if (!Array.isArray(list)) return [];
|
||||
return [
|
||||
...new Set(
|
||||
list
|
||||
.filter((name) => typeof name === "string")
|
||||
.map((name) => this.paletteAliases?.[name] ?? name),
|
||||
),
|
||||
];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -929,18 +960,66 @@ return baseclass.extend({
|
||||
);
|
||||
},
|
||||
|
||||
// Empty query matches everything at score 0 (the browse list). Title hits
|
||||
// rank first, then parent hits, which score the parent alone so its tabs
|
||||
// tie and keep menu order; "parent title" pairs split on a space. Then the
|
||||
// path, then the group.
|
||||
matchPaletteEntry(q, page) {
|
||||
if (!q) return { score: 0, ranges: null };
|
||||
|
||||
const title = this.fuzzyMatch(q, page.title);
|
||||
if (title) return { score: title.score + 12, ranges: title.ranges };
|
||||
|
||||
if (page.parent) {
|
||||
const parent = this.fuzzyMatch(q, page.parent);
|
||||
if (parent)
|
||||
return {
|
||||
score: parent.score + 8,
|
||||
ranges: null,
|
||||
parentRanges: parent.ranges,
|
||||
};
|
||||
|
||||
const words = q.split(/\s+/);
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const head = this.fuzzyMatch(words.slice(0, i).join(" "), page.parent);
|
||||
const tail =
|
||||
head && this.fuzzyMatch(words.slice(i).join(" "), page.title);
|
||||
if (tail)
|
||||
return {
|
||||
score: head.score + tail.score + 8,
|
||||
ranges: tail.ranges,
|
||||
parentRanges: head.ranges,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rest =
|
||||
this.fuzzyMatch(q, page.name) ||
|
||||
this.matchPalettePath(q, page.name) ||
|
||||
(page.group ? this.fuzzyMatch(q, page.group) : null);
|
||||
return rest && { score: rest.score, ranges: null };
|
||||
},
|
||||
|
||||
// Each query word (split on spaces or "/") lands inside one segment, in
|
||||
// path order: "network wireless" reaches network/wireless, while a lone
|
||||
// word can't scatter across segments ("ssh" in services/passwall2/other).
|
||||
matchPalettePath(q, name) {
|
||||
const words = q.split(/[\s/]+/).filter(Boolean);
|
||||
const segments = name.split("/");
|
||||
let at = 0;
|
||||
let score = 0;
|
||||
|
||||
for (const word of words) {
|
||||
let hit = null;
|
||||
for (; at < segments.length; at++) {
|
||||
hit = this.fuzzyMatch(word, segments[at]);
|
||||
if (hit) break;
|
||||
}
|
||||
if (!hit) return null;
|
||||
score += hit.score;
|
||||
}
|
||||
return words.length ? { score } : null;
|
||||
},
|
||||
|
||||
collectPaletteMatches(value) {
|
||||
let q = value.trim().toLowerCase();
|
||||
// A leading ">" scopes the list to the command rows (the prototype's
|
||||
@@ -963,6 +1042,7 @@ return baseclass.extend({
|
||||
page,
|
||||
score: at < 0 ? m.score : recents.length - at,
|
||||
ranges: m.ranges,
|
||||
parentRanges: m.parentRanges,
|
||||
});
|
||||
}
|
||||
if (q || recents.length) matches.sort((a, b) => b.score - a.score);
|
||||
@@ -983,7 +1063,7 @@ return baseclass.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
matches.forEach(({ page, ranges }, i) => {
|
||||
matches.forEach(({ page, ranges, parentRanges }, i) => {
|
||||
const current = page.mode && page.mode === theme;
|
||||
const attributes = {
|
||||
class: "cmdk-row",
|
||||
@@ -1001,7 +1081,21 @@ return baseclass.extend({
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-title" },
|
||||
this.highlightPaletteMatch(page.title, ranges),
|
||||
// A tab named after its parent is the page the parent opens.
|
||||
page.parent && page.parent !== page.title
|
||||
? [
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-parent" },
|
||||
this.highlightPaletteMatch(page.parent, parentRanges),
|
||||
),
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-label" },
|
||||
this.highlightPaletteMatch(page.title, ranges),
|
||||
),
|
||||
]
|
||||
: this.highlightPaletteMatch(page.title, ranges),
|
||||
),
|
||||
current
|
||||
? // The ✓ is decorative (aria-current carries the state); mark
|
||||
@@ -1488,7 +1582,7 @@ return baseclass.extend({
|
||||
);
|
||||
this.renderMainMenu(activeChild, activeChild.name, 0, navigationItems);
|
||||
this.renderMobileMenu(navigationItems);
|
||||
this.initPalette(navigationItems);
|
||||
this.initPalette(navigationItems, activeChild);
|
||||
}
|
||||
|
||||
if (ul?.children.length > 1) {
|
||||
|
||||
@@ -26,7 +26,10 @@ test("production assets stay within raw-transfer budgets", () => {
|
||||
// 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");
|
||||
// 23K: palette tabs (third-level nodes, redirect-parent folding, legacy
|
||||
// recents mapping, parent-aware scoring, per-segment path words) added
|
||||
// ~1.3 KB; the total budget below still holds.
|
||||
assert.ok(menu <= 23_000, "menu-aurora.js exceeds 23 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
|
||||
|
||||
@@ -15,14 +15,17 @@ const E = (tagName, attributes, children) => ({
|
||||
|
||||
const loadMenuModule = (
|
||||
localStorage = {},
|
||||
{ document = {}, navigator = { platform: "" } } = {},
|
||||
{
|
||||
document = {},
|
||||
navigator = { platform: "" },
|
||||
ui = { menu: { getChildren: () => [] } },
|
||||
} = {},
|
||||
) => {
|
||||
const baseclass = {
|
||||
extend(module) {
|
||||
return module;
|
||||
},
|
||||
};
|
||||
const ui = { menu: { getChildren: () => [] } };
|
||||
const L = {
|
||||
env: { dispatchpath: [], requestpath: [] },
|
||||
url: (...segments) => `/${segments.join("/")}`,
|
||||
@@ -462,3 +465,328 @@ test("initPalette appends logout as the last command row", () => {
|
||||
palette.paletteIndex.indexOf(last),
|
||||
);
|
||||
});
|
||||
|
||||
// ---- Tabs: third-level menu nodes ----
|
||||
|
||||
// ui.menu.getChildren() as luci-base ships it, down to handing out alias
|
||||
// nodes with their target's children (none, for a tab target).
|
||||
const menuUi = {
|
||||
menu: {
|
||||
getChildren: (node) =>
|
||||
Object.entries(node?.children ?? {})
|
||||
.filter(([, child]) => child.satisfied && "title" in child)
|
||||
.map(([name, child]) =>
|
||||
child.action?.type === "alias"
|
||||
? { ...child, name, children: undefined }
|
||||
: Object.assign(child, { name }),
|
||||
)
|
||||
.sort((a, b) => (a.order ?? 1000) - (b.order ?? 1000)),
|
||||
},
|
||||
};
|
||||
|
||||
const node = (title, order, action, children) => ({
|
||||
satisfied: true,
|
||||
title,
|
||||
order,
|
||||
action,
|
||||
children,
|
||||
});
|
||||
const view = { type: "view" };
|
||||
|
||||
const menuRoot = () => ({
|
||||
name: "admin",
|
||||
children: {
|
||||
status: node("Status", 1, view, {
|
||||
overview: node("Overview", 1, view),
|
||||
logs: node(
|
||||
"System Log",
|
||||
2,
|
||||
{ type: "alias", path: "admin/status/logs/syslog" },
|
||||
{
|
||||
dmesg: node("Kernel Log", 2, view),
|
||||
syslog: node("System Log", 1, view),
|
||||
},
|
||||
),
|
||||
}),
|
||||
system: node("System", 2, view, {
|
||||
admin: node(
|
||||
"Administration",
|
||||
1,
|
||||
{ type: "firstchild" },
|
||||
{
|
||||
dropbear: node("SSH Access", 2, view),
|
||||
hidden: { satisfied: true, order: 0, action: view },
|
||||
password: node("Router Password", 1, view),
|
||||
},
|
||||
),
|
||||
bmx: node("BMX", 2, view, { nodes: node("Nodes", 1, view) }),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const menuItems = () => [
|
||||
{
|
||||
name: "status",
|
||||
title: "Status",
|
||||
hasChildren: true,
|
||||
pages: [
|
||||
{ name: "overview", title: "Overview", href: "/admin/status/overview" },
|
||||
{ name: "logs", title: "System Log", href: "/admin/status/logs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "system",
|
||||
title: "System",
|
||||
hasChildren: true,
|
||||
pages: [
|
||||
{ name: "admin", title: "Administration", href: "/admin/system/admin" },
|
||||
{ name: "bmx", title: "BMX", href: "/admin/system/bmx" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const initTabbedPalette = (storage = fakeStorage()) => {
|
||||
const doc = {
|
||||
querySelector: (sel) =>
|
||||
sel === "#cmdk-trigger"
|
||||
? { setAttribute() {}, addEventListener() {} }
|
||||
: null,
|
||||
addEventListener() {},
|
||||
};
|
||||
const palette = loadMenuModule(storage, { document: doc, ui: menuUi });
|
||||
palette.initPalette(menuItems(), menuRoot());
|
||||
return palette;
|
||||
};
|
||||
|
||||
const navRows = (palette) =>
|
||||
palette.paletteIndex.filter((page) => !page.mode && !page.isLogout);
|
||||
|
||||
test("tabs join the index; a redirecting parent gives way to them", () => {
|
||||
const rows = navRows(initTabbedPalette());
|
||||
|
||||
assert.deepEqual(
|
||||
rows.map((page) => [page.name, page.parent ?? null]),
|
||||
[
|
||||
["status/overview", null],
|
||||
["status/logs/syslog", "System Log"],
|
||||
["status/logs/dmesg", "System Log"],
|
||||
["system/admin/password", "Administration"],
|
||||
["system/admin/dropbear", "Administration"],
|
||||
["system/bmx", null],
|
||||
["system/bmx/nodes", "BMX"],
|
||||
],
|
||||
);
|
||||
assert.equal(rows[2].href, "/admin/status/logs/dmesg");
|
||||
assert.equal(rows[2].group, "Status");
|
||||
});
|
||||
|
||||
test("redirecting parents map to the tab they open", () => {
|
||||
assert.deepEqual(initTabbedPalette().paletteAliases, {
|
||||
"status/logs": "status/logs/syslog",
|
||||
"system/admin": "system/admin/password",
|
||||
});
|
||||
});
|
||||
|
||||
test("a stored parent name reads as its tab, deduplicated", () => {
|
||||
const palette = initTabbedPalette(
|
||||
fakeStorage(
|
||||
stored(["system/admin", "system/admin/password", "status/logs"]),
|
||||
),
|
||||
);
|
||||
|
||||
assert.deepEqual(palette.readPaletteRecents(), [
|
||||
"system/admin/password",
|
||||
"status/logs/syslog",
|
||||
]);
|
||||
assert.deepEqual(names(palette.collectPaletteMatches("")).slice(0, 3), [
|
||||
"system/admin/password",
|
||||
"status/logs/syslog",
|
||||
"status/overview",
|
||||
]);
|
||||
});
|
||||
|
||||
const tab = (name, title, parent = "Firewall") => ({
|
||||
title,
|
||||
parent,
|
||||
name: `network/firewall/${name}`,
|
||||
group: "Network",
|
||||
href: `/${name}`,
|
||||
});
|
||||
|
||||
const firewallIndex = () => [
|
||||
navPage("status/nftables", "Firewall", "Status"),
|
||||
tab("zones", "General Settings"),
|
||||
tab("forwards", "Port Forwards"),
|
||||
tab("rules", "Traffic Rules"),
|
||||
tab("ipsets", "IP Sets"),
|
||||
];
|
||||
|
||||
test("a parent-only query keeps its tabs in menu order", () => {
|
||||
const palette = paletteWith(fakeStorage(), firewallIndex());
|
||||
|
||||
assert.deepEqual(names(palette.collectPaletteMatches("firewall")), [
|
||||
"status/nftables",
|
||||
"network/firewall/zones",
|
||||
"network/firewall/forwards",
|
||||
"network/firewall/rules",
|
||||
"network/firewall/ipsets",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a parent hit highlights the parent, not the title", () => {
|
||||
const match = menu.matchPaletteEntry(
|
||||
"fire",
|
||||
tab("forwards", "Port Forwards"),
|
||||
);
|
||||
|
||||
assert.equal(match.ranges, null);
|
||||
assert.deepEqual(sliced("Firewall", match.parentRanges), ["Fire"]);
|
||||
});
|
||||
|
||||
test("a spaced query pairs parent words with title words", () => {
|
||||
const palette = paletteWith(fakeStorage(), firewallIndex());
|
||||
const [hit, ...rest] = palette.collectPaletteMatches("firewall port");
|
||||
|
||||
assert.equal(hit.page.name, "network/firewall/forwards");
|
||||
assert.deepEqual(rest, []);
|
||||
assert.deepEqual(sliced("Port Forwards", hit.ranges), ["Port"]);
|
||||
assert.deepEqual(sliced("Firewall", hit.parentRanges), ["Firewall"]);
|
||||
});
|
||||
|
||||
test("a query no longer scatters across path segments", () => {
|
||||
const other = {
|
||||
title: "Other Settings",
|
||||
parent: "PassWall 2",
|
||||
name: "services/passwall2/other",
|
||||
group: "Services",
|
||||
};
|
||||
|
||||
assert.ok(!menu.matchPaletteEntry("ssh", other));
|
||||
assert.ok(
|
||||
menu.matchPaletteEntry("sshkeys", {
|
||||
title: "SSH 密钥",
|
||||
parent: "管理权",
|
||||
name: "system/admin/sshkeys",
|
||||
group: "系统",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("path words land in segments in order, never scattered", () => {
|
||||
const wireless = { title: "无线", name: "network/wireless", group: "网络" };
|
||||
const forwards = tab("forwards", "端口转发", "防火墙");
|
||||
|
||||
assert.ok(menu.matchPaletteEntry("network wireless", wireless));
|
||||
assert.ok(
|
||||
menu.matchPaletteEntry(
|
||||
"status/overview",
|
||||
navPage("status/overview", "概览"),
|
||||
),
|
||||
);
|
||||
assert.ok(menu.matchPaletteEntry("network firewall", forwards));
|
||||
assert.ok(menu.matchPaletteEntry("netw forw", forwards));
|
||||
assert.ok(!menu.matchPaletteEntry("wireless network", wireless));
|
||||
assert.ok(!menu.matchPaletteEntry("netwire", wireless));
|
||||
assert.ok(!menu.matchPaletteEntry("/", wireless));
|
||||
});
|
||||
|
||||
test("a parent keeps its row unless it redirects to one of its own tabs", () => {
|
||||
const doc = {
|
||||
querySelector: (sel) =>
|
||||
sel === "#cmdk-trigger"
|
||||
? { setAttribute() {}, addEventListener() {} }
|
||||
: null,
|
||||
addEventListener() {},
|
||||
};
|
||||
const palette = loadMenuModule(fakeStorage(), { document: doc, ui: menuUi });
|
||||
const root = {
|
||||
name: "admin",
|
||||
children: {
|
||||
services: node("Services", 1, view, {
|
||||
// Redirects to an untitled page, so no tab stands in for it.
|
||||
foo: node(
|
||||
"Foo",
|
||||
1,
|
||||
{ type: "alias", path: "admin/services/foo/main" },
|
||||
{
|
||||
main: { satisfied: true, order: 1, action: view },
|
||||
log: node("Log", 2, view),
|
||||
},
|
||||
),
|
||||
bar: node(
|
||||
"Bar",
|
||||
2,
|
||||
{ type: "firstchild" },
|
||||
{ only: { ...node("Only", 1, view), firstchild_ineligible: true } },
|
||||
),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
palette.initPalette(
|
||||
[
|
||||
{
|
||||
name: "services",
|
||||
title: "Services",
|
||||
hasChildren: true,
|
||||
pages: [
|
||||
{ name: "foo", title: "Foo", href: "/admin/services/foo" },
|
||||
{ name: "bar", title: "Bar", href: "/admin/services/bar" },
|
||||
],
|
||||
},
|
||||
],
|
||||
root,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
navRows(palette).map((page) => page.name),
|
||||
["services/foo", "services/foo/log", "services/bar", "services/bar/only"],
|
||||
);
|
||||
assert.deepEqual(palette.paletteAliases, {});
|
||||
});
|
||||
|
||||
test("recording after a legacy read writes the mapped names back", () => {
|
||||
const storage = fakeStorage(stored(["status/logs", "status/overview"]));
|
||||
const palette = initTabbedPalette(storage);
|
||||
|
||||
palette.recordPaletteRecent("system/admin/dropbear");
|
||||
|
||||
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
|
||||
"system/admin/dropbear",
|
||||
"status/logs/syslog",
|
||||
"status/overview",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a tab row renders its parent before the title unless they share a name", () => {
|
||||
const rows = [];
|
||||
const palette = paletteWith(fakeStorage(), [
|
||||
tab("forwards", "Port Forwards"),
|
||||
{ ...tab("firewall", "Firewall"), name: "network/firewall/firewall" },
|
||||
]);
|
||||
palette.paletteInput = { removeAttribute() {}, setAttribute() {} };
|
||||
palette.paletteList = {
|
||||
replaceChildren() {},
|
||||
appendChild: (row) => rows.push(row),
|
||||
};
|
||||
palette.setPaletteSelection = () => {};
|
||||
|
||||
palette.renderPaletteResults("fire");
|
||||
|
||||
const title = (name) =>
|
||||
rows.find((row) => row.attributes["data-name"] === name).children[0]
|
||||
.children;
|
||||
const forwards = title("network/firewall/forwards");
|
||||
const same = title("network/firewall/firewall");
|
||||
assert.deepEqual(
|
||||
forwards.map((part) => part.attributes.class),
|
||||
["cmdk-parent", "cmdk-label"],
|
||||
);
|
||||
const marked = (parts) => parts.some((part) => part.tagName === "mark");
|
||||
// The parent hit is highlighted inside the parent, the title stays plain.
|
||||
assert.ok(marked(forwards[0].children));
|
||||
assert.deepEqual(forwards[1].children, ["Port Forwards"]);
|
||||
// Same name: a plain highlighted title, no prefix.
|
||||
assert.ok(marked(same));
|
||||
assert.ok(!same.some((part) => part.attributes?.class === "cmdk-parent"));
|
||||
});
|
||||
@@ -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.3.7
|
||||
PKG_RELEASE:=81
|
||||
PKG_VERSION:=1.3.8
|
||||
PKG_RELEASE:=82
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (notLine truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (notLine truncated
|
||||
@@ -1 +1 @@
|
||||
"use strict";"require baseclass";"require ui";const PALETTE_RECENTS_KEY="aurora.paletteRecents";return baseclass.extend({__init__(){ui.menu.load().then(e=>this.render(e)).finally(()=>{for(const e of document.querySelectorAll("[data-restored]"))e.inert=!1}),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",s=()=>n.matches&&"sidebar"===document.body.dataset.navType,o=e=>{const a=s();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)},r=()=>{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),o(!1))};this.closeMobileNavigation=r;const l=t=>{if(s()){r();const e=!t;return document.body.classList.toggle("sidebar-collapsed",e),localStorage.setItem(i,e),void o(t)}if(n.matches)return r(),void o(!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),o(t)},c=()=>{if(r(),s()){const e="true"===localStorage.getItem(i);return document.body.classList.toggle("sidebar-collapsed",e),void o(!e)}o(!1)};t.addEventListener("click",t=>{t.stopPropagation(),l(!(s()?!document.body.classList.contains("sidebar-collapsed"):!n.matches&&e.classList.contains("mobile-menu-open")))}),e.addEventListener("click",t=>{t.target===e&&l(!1)}),document.addEventListener("keydown",t=>{"Escape"===t.key&&e.classList.contains("mobile-menu-open")&&l(!1)}),n.addEventListener("change",c),c(),document.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");a&&e.contains(a)&&l(!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),this.cacheNav()},cacheNav(){const e={};for(const t of document.querySelectorAll("#topmenu, #sidebar-list, #sidebar-footer"))t.firstChild&&(e[t.id]=t.innerHTML);try{sessionStorage.setItem("aurora.nav",JSON.stringify([document.body.dataset.navStamp,e]))}catch{}},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);window.auroraCrumb?.();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||"megaLine 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)).finally(()=>{for(const e of document.querySelectorAll("[data-restored]"))e.inert=!1}),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",s=()=>n.matches&&"sidebar"===document.body.dataset.navType,o=e=>{const a=s();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)},r=()=>{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),o(!1))};this.closeMobileNavigation=r;const l=t=>{if(s()){r();const e=!t;return document.body.classList.toggle("sidebar-collapsed",e),localStorage.setItem(i,e),void o(t)}if(n.matches)return r(),void o(!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),o(t)},c=()=>{if(r(),s()){const e="true"===localStorage.getItem(i);return document.body.classList.toggle("sidebar-collapsed",e),void o(!e)}o(!1)};t.addEventListener("click",t=>{t.stopPropagation(),l(!(s()?!document.body.classList.contains("sidebar-collapsed"):!n.matches&&e.classList.contains("mobile-menu-open")))}),e.addEventListener("click",t=>{t.target===e&&l(!1)}),document.addEventListener("keydown",t=>{"Escape"===t.key&&e.classList.contains("mobile-menu-open")&&l(!1)}),n.addEventListener("change",c),c(),document.addEventListener("click",t=>{const a=t.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");a&&e.contains(a)&&l(!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),this.cacheNav()},cacheNav(){const e={};for(const t of document.querySelectorAll("#topmenu, #sidebar-list, #sidebar-footer"))t.firstChild&&(e[t.id]=t.innerHTML);try{sessionStorage.setItem("aurora.nav",JSON.stringify([document.body.dataset.navStamp,e]))}catch{}},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);window.auroraCrumb?.();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||"megaLine truncated
|
||||
@@ -150,6 +150,27 @@ button.cmdk-cancel {
|
||||
}
|
||||
}
|
||||
|
||||
/* A tab row reads "Parent › Title": the parent gives way first (truncate's
|
||||
overflow already lets it shrink below its content), and keeps at least
|
||||
an ellipsis so the "›" never leads the row. Unnested: the classes exist
|
||||
only inside palette rows, and the short selectors keep main.css inside
|
||||
its budget. */
|
||||
.cmdk-title:has(> .cmdk-parent) {
|
||||
@apply flex items-baseline;
|
||||
}
|
||||
|
||||
.cmdk-parent {
|
||||
@apply text-muted-foreground truncate font-normal;
|
||||
}
|
||||
|
||||
.cmdk-label {
|
||||
@apply max-w-[calc(100%-1.5rem)] shrink-0 truncate;
|
||||
|
||||
&::before {
|
||||
@apply text-muted-foreground px-1.5 font-normal content-['›'];
|
||||
}
|
||||
}
|
||||
|
||||
.cmdk-empty {
|
||||
@apply text-muted-foreground px-3 py-7 text-center text-sm;
|
||||
}
|
||||
|
||||
@@ -485,6 +485,7 @@ return baseclass.extend({
|
||||
if (!trigger || this.palIndex) return;
|
||||
|
||||
this.palIndex = [];
|
||||
this.palAliases = {};
|
||||
ui.menu.getChildren(branch).forEach((section) => {
|
||||
const subs = ui.menu.getChildren(section);
|
||||
if (subs.length === 0) {
|
||||
@@ -499,14 +500,46 @@ return baseclass.extend({
|
||||
return;
|
||||
}
|
||||
subs.forEach((page) => {
|
||||
this.palIndex.push({
|
||||
title: _(page.title),
|
||||
group: _(section.title),
|
||||
icon: section.name,
|
||||
path: `${branchUrl}/${section.name}/${page.name}`,
|
||||
href: L.url(branchUrl, section.name, page.name),
|
||||
isLogout: false,
|
||||
});
|
||||
const title = _(page.title);
|
||||
const group = _(section.title);
|
||||
const path = `${branchUrl}/${section.name}/${page.name}`;
|
||||
// The raw node: getChildren() hands out alias nodes carrying their
|
||||
// target's children, which would hide an alias parent's tabs.
|
||||
const node = section.children?.[page.name] ?? {};
|
||||
const tabs = ui.menu.getChildren(node);
|
||||
const type = node.action?.type;
|
||||
const target =
|
||||
type === "alias"
|
||||
? node.action.path.replace(`${path}/`, "")
|
||||
: type === "firstchild" &&
|
||||
tabs.find((tab) => !tab.firstchild_ineligible)?.name;
|
||||
|
||||
// A parent that only redirects to one of its tabs is that tab: its
|
||||
// row gives way to the tabs, and its path (stored by older recents)
|
||||
// resolves to where it redirects.
|
||||
if (tabs.some((tab) => tab.name === target))
|
||||
this.palAliases[path] = `${path}/${target}`;
|
||||
else
|
||||
this.palIndex.push({
|
||||
title,
|
||||
group,
|
||||
icon: section.name,
|
||||
path,
|
||||
href: L.url(branchUrl, section.name, page.name),
|
||||
isLogout: false,
|
||||
});
|
||||
|
||||
tabs.forEach((tab) =>
|
||||
this.palIndex.push({
|
||||
title: _(tab.title),
|
||||
parent: title,
|
||||
group,
|
||||
icon: section.name,
|
||||
path: `${path}/${tab.name}`,
|
||||
href: L.url(branchUrl, section.name, page.name, tab.name),
|
||||
isLogout: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -698,9 +731,14 @@ return baseclass.extend({
|
||||
_palReadRecents() {
|
||||
try {
|
||||
const list = JSON.parse(localStorage.getItem(RECENTS_KEY));
|
||||
return Array.isArray(list)
|
||||
? list.filter((path) => typeof path === "string")
|
||||
: [];
|
||||
if (!Array.isArray(list)) return [];
|
||||
return [
|
||||
...new Set(
|
||||
list
|
||||
.filter((path) => typeof path === "string")
|
||||
.map((path) => this.palAliases?.[path] ?? path),
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
@@ -755,11 +793,22 @@ return baseclass.extend({
|
||||
? this.palIndex.filter((page) => page.isLogout)
|
||||
: this.palIndex;
|
||||
pool.forEach((page) => {
|
||||
const m = this._palScore(q, page.title, page.path, page.group);
|
||||
const m = this._palScore(
|
||||
q,
|
||||
page.title,
|
||||
page.path,
|
||||
page.group,
|
||||
page.parent,
|
||||
);
|
||||
if (m)
|
||||
hits.push({
|
||||
score: m.score,
|
||||
node: this._palPageRow(page, m.ranges, m.groupRanges),
|
||||
node: this._palPageRow(
|
||||
page,
|
||||
m.ranges,
|
||||
m.groupRanges,
|
||||
m.parentRanges,
|
||||
),
|
||||
});
|
||||
});
|
||||
this.palModes.forEach((cmd) => {
|
||||
@@ -814,7 +863,7 @@ return baseclass.extend({
|
||||
else this.palInput.removeAttribute("aria-activedescendant");
|
||||
},
|
||||
|
||||
_palPageRow(page, ranges, groupRanges) {
|
||||
_palPageRow(page, ranges, groupRanges, parentRanges) {
|
||||
// Title leads, the section label is demoted to small type on the right
|
||||
// edge — same row anatomy as luci-theme-aurora's palette, so the two
|
||||
// themes stay legible side by side. The dispatch path is matchable but
|
||||
@@ -833,7 +882,25 @@ return baseclass.extend({
|
||||
},
|
||||
[
|
||||
this._sectionIcon(page.icon, 15),
|
||||
E("span", { class: "cmdk-title" }, this._palMark(page.title, ranges)),
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-title" },
|
||||
// A tab named after its parent is the page the parent opens.
|
||||
page.parent && page.parent !== page.title
|
||||
? [
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-parent" },
|
||||
this._palMark(page.parent, parentRanges),
|
||||
),
|
||||
E(
|
||||
"span",
|
||||
{ class: "cmdk-label" },
|
||||
this._palMark(page.title, ranges),
|
||||
),
|
||||
]
|
||||
: this._palMark(page.title, ranges),
|
||||
),
|
||||
page.group
|
||||
? E(
|
||||
"span",
|
||||
@@ -897,21 +964,66 @@ return baseclass.extend({
|
||||
|
||||
/**
|
||||
* Subsequence scorer: every query char must appear in order; consecutive
|
||||
* runs and word/segment starts weigh extra. Title hits rank first, then
|
||||
* the section label, then the language-neutral path. Title and label hits
|
||||
* runs and word/segment starts weigh extra. Title hits rank first, then a
|
||||
* tab's parent — scored alone so its tabs tie and keep menu order, or
|
||||
* paired with the title when the query splits on a space — then the
|
||||
* section label, then the language-neutral path, where each query word
|
||||
* (split on spaces or "/") lands inside one segment, in path order, so a
|
||||
* lone word can't scatter across segments. Title, parent and label hits
|
||||
* highlight the field they matched; a path hit ranks without highlighting.
|
||||
*/
|
||||
_palScore(q, title, path, group) {
|
||||
_palScore(q, title, path, group, parent) {
|
||||
const t = this._palSub(q, title.toLowerCase());
|
||||
if (t) return { score: t.score + 10, ranges: t.ranges, groupRanges: null };
|
||||
if (parent) {
|
||||
const low = parent.toLowerCase();
|
||||
const pt = this._palSub(q, low);
|
||||
if (pt)
|
||||
return {
|
||||
score: pt.score + 7,
|
||||
ranges: null,
|
||||
groupRanges: null,
|
||||
parentRanges: pt.ranges,
|
||||
};
|
||||
const words = q.split(/\s+/);
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const head = this._palSub(words.slice(0, i).join(" "), low);
|
||||
const tail =
|
||||
head && this._palSub(words.slice(i).join(" "), title.toLowerCase());
|
||||
if (tail)
|
||||
return {
|
||||
score: head.score + tail.score + 7,
|
||||
ranges: tail.ranges,
|
||||
groupRanges: null,
|
||||
parentRanges: head.ranges,
|
||||
};
|
||||
}
|
||||
}
|
||||
// On a localized instance the section label is the only group name the
|
||||
// user ever sees: querying the translated 'Network' must reach its rows.
|
||||
const g = group ? this._palSub(q, group.toLowerCase()) : null;
|
||||
if (g) return { score: g.score + 5, ranges: null, groupRanges: g.ranges };
|
||||
const p = this._palSub(q, String(path || "").toLowerCase());
|
||||
const p = this._palPath(q, String(path || "").toLowerCase());
|
||||
return p ? { score: p.score, ranges: null, groupRanges: null } : null;
|
||||
},
|
||||
|
||||
_palPath(q, low) {
|
||||
const words = q.split(/[\s/]+/).filter(Boolean);
|
||||
const segments = low.split("/");
|
||||
let at = 0;
|
||||
let score = 0;
|
||||
for (const word of words) {
|
||||
let hit = null;
|
||||
for (; at < segments.length; at++) {
|
||||
hit = this._palSub(word, segments[at]);
|
||||
if (hit) break;
|
||||
}
|
||||
if (!hit) return null;
|
||||
score += hit.score;
|
||||
}
|
||||
return words.length ? { score } : null;
|
||||
},
|
||||
|
||||
_palSub(q, low) {
|
||||
let ti = 0;
|
||||
let score = 0;
|
||||
|
||||
@@ -51,12 +51,15 @@ test("production assets stay within raw-transfer budgets", () => {
|
||||
|
||||
assert.ok(main <= 130_000, `main.css ${main} B exceeds 130 KB`);
|
||||
assert.ok(login <= 11_000, `login.css ${login} B exceeds 11 KB`);
|
||||
assert.ok(menu <= 16_000, `menu-shadcn.js ${menu} B exceeds 16 KB`);
|
||||
// 17.5K: palette tabs (third-level nodes, redirect-parent folding, legacy
|
||||
// recents mapping, parent-aware scoring, per-segment path words) added
|
||||
// ~1.2 KB; the admin total below moved by the same amount.
|
||||
assert.ok(menu <= 17_500, `menu-shadcn.js ${menu} B exceeds 17.5 KB`);
|
||||
assert.ok(router <= 16_000, `router-shadcn.js ${router} B exceeds 16 KB`);
|
||||
assert.ok(sidebar <= 6_500, `sidebar-shadcn.js ${sidebar} B exceeds 6.5 KB`);
|
||||
assert.ok(
|
||||
main + menu + router + sidebar + font <= 216_000,
|
||||
"admin assets exceed 216 KB",
|
||||
main + menu + router + sidebar + font <= 217_500,
|
||||
"admin assets exceed 217.5 KB",
|
||||
);
|
||||
assert.ok(login + font + logo <= 60_000, "login assets exceed 60 KB");
|
||||
});
|
||||
@@ -12,17 +12,20 @@ const E = (tagName, attributes, children) => ({
|
||||
tagName,
|
||||
attributes,
|
||||
children,
|
||||
addEventListener() {},
|
||||
});
|
||||
|
||||
// The module's top level only defines constants and returns the extended
|
||||
// object, so empty DOM/storage stubs keep the factory inert.
|
||||
const loadMenuModule = (localStorage = {}) => {
|
||||
const loadMenuModule = (
|
||||
localStorage = {},
|
||||
{ document = {}, ui = { menu: { getChildren: () => [] } } } = {},
|
||||
) => {
|
||||
const baseclass = {
|
||||
extend(module) {
|
||||
return module;
|
||||
},
|
||||
};
|
||||
const ui = { menu: { getChildren: () => [] } };
|
||||
const L = {
|
||||
env: { dispatchpath: [], requestpath: [] },
|
||||
url: (...segments) => `/${segments.join("/")}`,
|
||||
@@ -46,7 +49,7 @@ const loadMenuModule = (localStorage = {}) => {
|
||||
E,
|
||||
L,
|
||||
(value) => value,
|
||||
{},
|
||||
document,
|
||||
{},
|
||||
localStorage,
|
||||
{},
|
||||
@@ -192,3 +195,337 @@ test("the logout leaf stays out of the browse list, stored or not", () => {
|
||||
"admin/network/firewall",
|
||||
]);
|
||||
});
|
||||
|
||||
// ---- Tabs: third-level menu nodes ----
|
||||
|
||||
// ui.menu.getChildren() as luci-base ships it, down to handing out alias
|
||||
// nodes with their target's children (none, for a tab target).
|
||||
const menuUi = {
|
||||
menu: {
|
||||
getChildren: (node) =>
|
||||
Object.entries(node?.children ?? {})
|
||||
.filter(([, child]) => child.satisfied && "title" in child)
|
||||
.map(([name, child]) =>
|
||||
child.action?.type === "alias"
|
||||
? { ...child, name, children: undefined }
|
||||
: Object.assign(child, { name }),
|
||||
)
|
||||
.sort((a, b) => (a.order ?? 1000) - (b.order ?? 1000)),
|
||||
},
|
||||
};
|
||||
|
||||
const node = (title, order, action, children) => ({
|
||||
satisfied: true,
|
||||
title,
|
||||
order,
|
||||
action,
|
||||
children,
|
||||
});
|
||||
const view = { type: "view" };
|
||||
|
||||
const menuBranch = () =>
|
||||
node(
|
||||
"Administration",
|
||||
1,
|
||||
{ type: "firstchild" },
|
||||
{
|
||||
status: node("Status", 1, view, {
|
||||
overview: node("Overview", 1, view),
|
||||
logs: node(
|
||||
"System Log",
|
||||
2,
|
||||
{ type: "alias", path: "admin/status/logs/syslog" },
|
||||
{
|
||||
dmesg: node("Kernel Log", 2, view),
|
||||
syslog: node("System Log", 1, view),
|
||||
},
|
||||
),
|
||||
}),
|
||||
system: node("System", 2, view, {
|
||||
admin: node(
|
||||
"Administration",
|
||||
1,
|
||||
{ type: "firstchild" },
|
||||
{
|
||||
dropbear: node("SSH Access", 2, view),
|
||||
hidden: { satisfied: true, order: 0, action: view },
|
||||
password: node("Router Password", 1, view),
|
||||
},
|
||||
),
|
||||
bmx: node("BMX", 2, view, { nodes: node("Nodes", 1, view) }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const initTabbedPalette = (storage = fakeStorage()) => {
|
||||
const trigger = {
|
||||
querySelector: () => null,
|
||||
setAttribute() {},
|
||||
addEventListener() {},
|
||||
};
|
||||
const document = {
|
||||
getElementById: (id) => (id === "cmdk-trigger" ? trigger : null),
|
||||
addEventListener() {},
|
||||
};
|
||||
const palette = loadMenuModule(storage, { document, ui: menuUi });
|
||||
palette.initPalette(menuBranch(), "admin");
|
||||
return palette;
|
||||
};
|
||||
|
||||
test("tabs join the index; a redirecting parent gives way to them", () => {
|
||||
const index = initTabbedPalette().palIndex;
|
||||
|
||||
assert.deepEqual(
|
||||
index.map((page) => [page.path, page.parent ?? null]),
|
||||
[
|
||||
["admin/status/overview", null],
|
||||
["admin/status/logs/syslog", "System Log"],
|
||||
["admin/status/logs/dmesg", "System Log"],
|
||||
["admin/system/admin/password", "Administration"],
|
||||
["admin/system/admin/dropbear", "Administration"],
|
||||
["admin/system/bmx", null],
|
||||
["admin/system/bmx/nodes", "BMX"],
|
||||
],
|
||||
);
|
||||
assert.equal(index[2].href, "/admin/status/logs/dmesg");
|
||||
assert.equal(index[2].group, "Status");
|
||||
assert.equal(index[2].icon, "status");
|
||||
});
|
||||
|
||||
test("redirecting parents map to the tab they open", () => {
|
||||
assert.deepEqual(initTabbedPalette().palAliases, {
|
||||
"admin/status/logs": "admin/status/logs/syslog",
|
||||
"admin/system/admin": "admin/system/admin/password",
|
||||
});
|
||||
});
|
||||
|
||||
test("a stored parent path reads as its tab, deduplicated", () => {
|
||||
const palette = initTabbedPalette(
|
||||
fakeStorage(
|
||||
stored([
|
||||
"admin/system/admin",
|
||||
"admin/system/admin/password",
|
||||
"admin/status/logs",
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
assert.deepEqual(palette._palReadRecents(), [
|
||||
"admin/system/admin/password",
|
||||
"admin/status/logs/syslog",
|
||||
]);
|
||||
assert.deepEqual(paths(palette._palBrowsePages()).slice(0, 3), [
|
||||
"admin/system/admin/password",
|
||||
"admin/status/logs/syslog",
|
||||
"admin/status/overview",
|
||||
]);
|
||||
});
|
||||
|
||||
const menu = loadMenuModule();
|
||||
const sliced = (text, ranges) =>
|
||||
ranges.map(([from, to]) => text.slice(from, to));
|
||||
|
||||
test("a parent-only query ties the tabs and ranks them under title hits", () => {
|
||||
const title = menu._palScore(
|
||||
"firewall",
|
||||
"Firewall",
|
||||
"admin/status/nftables",
|
||||
"Status",
|
||||
);
|
||||
const zones = menu._palScore(
|
||||
"firewall",
|
||||
"General Settings",
|
||||
"admin/network/firewall/zones",
|
||||
"Network",
|
||||
"Firewall",
|
||||
);
|
||||
const forwards = menu._palScore(
|
||||
"firewall",
|
||||
"Port Forwards",
|
||||
"admin/network/firewall/forwards",
|
||||
"Network",
|
||||
"Firewall",
|
||||
);
|
||||
|
||||
assert.equal(zones.score, forwards.score);
|
||||
assert.ok(title.score > zones.score);
|
||||
assert.equal(zones.ranges, null);
|
||||
assert.deepEqual(sliced("Firewall", zones.parentRanges), ["Firewall"]);
|
||||
});
|
||||
|
||||
test("a parent hit outranks a section-label hit", () => {
|
||||
const parent = menu._palScore(
|
||||
"sys",
|
||||
"Kernel Log",
|
||||
"admin/status/logs/dmesg",
|
||||
"Status",
|
||||
"System Log",
|
||||
);
|
||||
const group = menu._palScore(
|
||||
"sys",
|
||||
"Startup",
|
||||
"admin/system/startup",
|
||||
"System",
|
||||
);
|
||||
|
||||
assert.ok(parent.score > group.score);
|
||||
});
|
||||
|
||||
test("a spaced query pairs parent words with title words", () => {
|
||||
const hit = menu._palScore(
|
||||
"firewall port",
|
||||
"Port Forwards",
|
||||
"admin/network/firewall/forwards",
|
||||
"Network",
|
||||
"Firewall",
|
||||
);
|
||||
|
||||
assert.ok(hit);
|
||||
assert.deepEqual(sliced("Port Forwards", hit.ranges), ["Port"]);
|
||||
assert.deepEqual(sliced("Firewall", hit.parentRanges), ["Firewall"]);
|
||||
assert.equal(
|
||||
menu._palScore(
|
||||
"firewall port",
|
||||
"Traffic Rules",
|
||||
"admin/network/firewall/rules",
|
||||
"Network",
|
||||
"Firewall",
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("a query no longer scatters across path segments", () => {
|
||||
assert.equal(
|
||||
menu._palScore(
|
||||
"ssh",
|
||||
"Other Settings",
|
||||
"admin/services/passwall2/other",
|
||||
"Services",
|
||||
"PassWall 2",
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.ok(
|
||||
menu._palScore(
|
||||
"sshkeys",
|
||||
"SSH 密钥",
|
||||
"admin/system/admin/sshkeys",
|
||||
"系统",
|
||||
"管理权",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("path words land in segments in order, never scattered", () => {
|
||||
const score = (q, path, parent) =>
|
||||
menu._palScore(q, "无线", path, "网络", parent);
|
||||
|
||||
assert.ok(score("network wireless", "admin/network/wireless"));
|
||||
assert.ok(score("status/overview", "admin/status/overview"));
|
||||
assert.ok(
|
||||
menu._palScore(
|
||||
"network firewall",
|
||||
"端口转发",
|
||||
"admin/network/firewall/forwards",
|
||||
"网络",
|
||||
"防火墙",
|
||||
),
|
||||
);
|
||||
assert.equal(score("wireless network", "admin/network/wireless"), null);
|
||||
assert.equal(score("netwire", "admin/network/wireless"), null);
|
||||
assert.equal(score("/", "admin/network/wireless"), null);
|
||||
});
|
||||
|
||||
test("a parent keeps its row unless it redirects to one of its own tabs", () => {
|
||||
const trigger = {
|
||||
querySelector: () => null,
|
||||
setAttribute() {},
|
||||
addEventListener() {},
|
||||
};
|
||||
const document = {
|
||||
getElementById: (id) => (id === "cmdk-trigger" ? trigger : null),
|
||||
addEventListener() {},
|
||||
};
|
||||
const palette = loadMenuModule(fakeStorage(), { document, ui: menuUi });
|
||||
const branch = node(
|
||||
"Administration",
|
||||
1,
|
||||
{ type: "firstchild" },
|
||||
{
|
||||
services: node("Services", 1, view, {
|
||||
// Redirects to an untitled page, so no tab stands in for it.
|
||||
foo: node(
|
||||
"Foo",
|
||||
1,
|
||||
{ type: "alias", path: "admin/services/foo/main" },
|
||||
{
|
||||
main: { satisfied: true, order: 1, action: view },
|
||||
log: node("Log", 2, view),
|
||||
},
|
||||
),
|
||||
bar: node(
|
||||
"Bar",
|
||||
2,
|
||||
{ type: "firstchild" },
|
||||
{ only: { ...node("Only", 1, view), firstchild_ineligible: true } },
|
||||
),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
palette.initPalette(branch, "admin");
|
||||
|
||||
assert.deepEqual(paths(palette.palIndex), [
|
||||
"admin/services/foo",
|
||||
"admin/services/foo/log",
|
||||
"admin/services/bar",
|
||||
"admin/services/bar/only",
|
||||
]);
|
||||
assert.deepEqual(palette.palAliases, {});
|
||||
});
|
||||
|
||||
test("recording after a legacy read writes the mapped paths back", () => {
|
||||
const storage = fakeStorage(
|
||||
stored(["admin/status/logs", "admin/status/overview"]),
|
||||
);
|
||||
const palette = initTabbedPalette(storage);
|
||||
|
||||
palette._palRecordRecent("admin/system/admin/dropbear");
|
||||
|
||||
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
|
||||
"admin/system/admin/dropbear",
|
||||
"admin/status/logs/syslog",
|
||||
"admin/status/overview",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a tab row renders its parent before the title unless they share a name", () => {
|
||||
const palette = loadMenuModule();
|
||||
palette._sectionIcon = () => "";
|
||||
const tabPage = (title) => ({
|
||||
title,
|
||||
parent: "Firewall",
|
||||
group: "Network",
|
||||
icon: "network",
|
||||
path: "admin/network/firewall/x",
|
||||
href: "/x",
|
||||
isLogout: false,
|
||||
});
|
||||
const marked = (parts) => parts.some((part) => part.tagName === "mark");
|
||||
|
||||
const forwards = palette._palPageRow(tabPage("Port Forwards"), null, null, [
|
||||
[0, 4],
|
||||
]).children[1].children;
|
||||
assert.deepEqual(
|
||||
forwards.map((part) => part.attributes.class),
|
||||
["cmdk-parent", "cmdk-label"],
|
||||
);
|
||||
assert.ok(marked(forwards[0].children));
|
||||
assert.deepEqual(forwards[1].children, ["Port Forwards"]);
|
||||
|
||||
const same = palette._palPageRow(tabPage("Firewall"), [[0, 4]]).children[1]
|
||||
.children;
|
||||
assert.ok(marked(same));
|
||||
assert.ok(!same.some((part) => part.attributes?.class === "cmdk-parent"));
|
||||
});
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design language.
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=0.5.10
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=0.5.11
|
||||
PKG_RELEASE:=5
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@@ -1 +1 @@
|
||||
"use strict";"require baseclass";"require ui";const ICON_MAP={status:"activity",system:"settings",network:"network",services:"layers",nas:"hard-drive",control:"sliders-horizontal",vpn:"shield",docker:"container",statistics:"chart-bar",nlbw:"gauge",asterisk:"phone",logout:"log-out",default:"layout-dashboard"},CACHE_KEY="shadcn.sidebar.cache",RECENTS_KEY="shadcn.palette.recents";return baseclass.extend({__init__(){ui.menu.load().then(e=>{this.renderSidebarChrome(),this.render(e),this.initUciIndicator(),this.cacheSidebar(),window.addEventListener("pagehide",()=>this.cacheSidebar()),document.dispatchEvent(new Event("shadcn-sidebar-ready"))})},cacheSidebar(){if(window.shadcnSuppressSidebarCache)return;const e=document.getElementById("sidebar"),t=document.getElementById("sidebar-nav");if(e&&t)try{sessionStorage.setItem(CACHE_KEY,JSON.stringify({v:1,lang:document.documentElement.lang||"",html:e.innerHTML,scroll:t.scrollTop||0}))}catch(e){}},_mediaBase(){const e=document.documentElement.getAttribute("data-shadcn-media"),t=document.getElementById("sidebar"),a=t&&t.getAttribute("data-media");return String(e||a||"/luci-static/shadcn").replace(/\/$/,"")},_sectionIcon(e,t){const a=String(e||"").toLowerCase(),s=ICON_MAP[a]||ICON_MAP.default;return this._iconFile(s,t)},_isLogoutMenuItem(e){const t=String(e&&null!=e.name?e.name:"").toLowerCase();return"logout"===t||t.endsWith("/logout")},_iconFile(e,t){const a=this._mediaBase();return E("img",{src:`${a}/icons/${e}.svg`,width:String(t),height:String(t),alt:"",loading:"lazy",class:"shadcn-icon"})},_resolveMenuBranch(e){const t=L.env.dispatchpath||[],a=L.env.requestpath||[],s=ui.menu.getChildren(e);let n=a.length&&a[0]||t.length&&t[0]||"admin",i=null;for(let e=0;e<s.length;e++)if(s[e].name===n){i=s[e];break}return i||1!==s.length||(i=s[0],n=i.name),i||(i=e),{branch:i,branchUrl:i.name||n}},renderSidebarChrome(){const e=document.getElementById("sidebar");if(!e)return;if("1"===e.getAttribute("data-shadcn-built")){const t=e.querySelector(".sidebar-brand-text"),a=e.getAttribute("data-hostname");return void(t&&a&&t.textContent!==a&&(t.textContent=a))}e.setAttribute("data-shadcn-built","1");const t=this._mediaBase(),a=e.getAttribute("data-hostname")||"Shadcn";for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(E("div",{class:"sidebar-header"},[E("a",{class:"sidebar-brand",href:"/"},[E("img",{class:"sidebar-logo",src:`${t}/images/logo.svg`,alt:"Shadcn",width:"24",height:"24"}),E("span",{class:"sidebar-brand-text"},[a])]),E("button",{id:"sidebar-toggle-btn",class:"sidebar-toggle",type:"button","aria-label":_("Navigation")},[E("span",{class:"icon-collapse"},[this._iconFile("panel-left-close",16)]),E("span",{class:"icon-expand"},[this._iconFile("panel-left-open",16)])])])),e.appendChild(E("nav",{class:"sidebar-nav",id:"sidebar-nav"})),e.appendChild(E("div",{class:"sidebar-footer",id:"sidebar-footer",hidden:""}))},render(e){const{branch:t,branchUrl:a}=this._resolveMenuBranch(e);this.tree=e,this.branch=t,this.branchUrl=a,this.renderSidebarNav(t,a),this.renderBreadcrumb(t,a),this.initPalette(t,a),this.renderTabs(e)},renderTabs(e){const t=L.env.dispatchpath||[],a=document.getElementById("tabmenu");a&&(a.innerHTML="",a.style.display="none");let s=e,n="";if(t.length>=3){for(var i=0;i<3&&s;i++){const e=t[i];if(!s.children||!s.children[e])break;s=s.children[e],n=n+(n?"/":"")+e}s&&this.renderTabMenu(s,n)}},syncRoute(){if(!this.tree)return;const e=document.getElementById("sidebar");if(e){e.querySelectorAll(".active").forEach(e=>e.classList.remove("active"));const t=new URL(L.url(...(L.env.dispatchpath||[]).slice(0,3)),location.href).pathname;let a=null,s=-1;if(e.querySelectorAll("a.sidebar-sub-link, a.sidebar-nav-parent, a.sidebar-logout").forEach(e=>{const n=e.pathname;(t===n||t.startsWith(n+"/"))&&n.length>s&&(a=e,s=n.length)}),a){const t=a.closest(".sidebar-sub-item");if(t){t.classList.add("active");const s=a.closest(".sidebar-accordion-item");s&&(s.classList.add("active"),"true"!==e.getAttribute("data-collapsed")&&e.querySelectorAll(".sidebar-accordion-item").forEach(e=>e.setAttribute("data-open",e===s?"true":"false")))}else a.classList.contains("sidebar-logout")?a.classList.add("active"):a.closest(".sidebar-nav-item")?.classList.add("active")}}this.renderBreadcrumb(this.branch,this.branchUrl),this.renderTabs(this.tree)},closeSurfaces(){this.closePalette(),window.ShadcnSidebar?.closeDrawer?.(),window.ShadcnSidebar?._hideCollapsedPopover?.()},renderSidebarNav(e,t){const a=document.getElementById("sidebar-nav"),s=document.getElementById("sidebar-footer");if(!a)return;const n=ui.menu.getChildren(e),i=L.env.dispatchpath||[],r="1"===document.getElementById("sidebar")?.getAttribute("data-shadcn-restored"),l=r?Array.from(a.querySelectorAll('.sidebar-accordion-item[data-open="true"]'),e=>e.getAttribute("data-section")):null,c=r?a.scrollTop:0;a.innerHTML="",s&&(s.innerHTML="",s.hidden=!0),n.forEach(e=>{if(this._isLogoutMenuItem(e)&&s){const a=i[1]==e.name,n=this._sectionIcon(e.name,18);returLine truncated
|
||||
"use strict";"require baseclass";"require ui";const ICON_MAP={status:"activity",system:"settings",network:"network",services:"layers",nas:"hard-drive",control:"sliders-horizontal",vpn:"shield",docker:"container",statistics:"chart-bar",nlbw:"gauge",asterisk:"phone",logout:"log-out",default:"layout-dashboard"},CACHE_KEY="shadcn.sidebar.cache",RECENTS_KEY="shadcn.palette.recents";return baseclass.extend({__init__(){ui.menu.load().then(e=>{this.renderSidebarChrome(),this.render(e),this.initUciIndicator(),this.cacheSidebar(),window.addEventListener("pagehide",()=>this.cacheSidebar()),document.dispatchEvent(new Event("shadcn-sidebar-ready"))})},cacheSidebar(){if(window.shadcnSuppressSidebarCache)return;const e=document.getElementById("sidebar"),t=document.getElementById("sidebar-nav");if(e&&t)try{sessionStorage.setItem(CACHE_KEY,JSON.stringify({v:1,lang:document.documentElement.lang||"",html:e.innerHTML,scroll:t.scrollTop||0}))}catch(e){}},_mediaBase(){const e=document.documentElement.getAttribute("data-shadcn-media"),t=document.getElementById("sidebar"),a=t&&t.getAttribute("data-media");return String(e||a||"/luci-static/shadcn").replace(/\/$/,"")},_sectionIcon(e,t){const a=String(e||"").toLowerCase(),s=ICON_MAP[a]||ICON_MAP.default;return this._iconFile(s,t)},_isLogoutMenuItem(e){const t=String(e&&null!=e.name?e.name:"").toLowerCase();return"logout"===t||t.endsWith("/logout")},_iconFile(e,t){const a=this._mediaBase();return E("img",{src:`${a}/icons/${e}.svg`,width:String(t),height:String(t),alt:"",loading:"lazy",class:"shadcn-icon"})},_resolveMenuBranch(e){const t=L.env.dispatchpath||[],a=L.env.requestpath||[],s=ui.menu.getChildren(e);let n=a.length&&a[0]||t.length&&t[0]||"admin",i=null;for(let e=0;e<s.length;e++)if(s[e].name===n){i=s[e];break}return i||1!==s.length||(i=s[0],n=i.name),i||(i=e),{branch:i,branchUrl:i.name||n}},renderSidebarChrome(){const e=document.getElementById("sidebar");if(!e)return;if("1"===e.getAttribute("data-shadcn-built")){const t=e.querySelector(".sidebar-brand-text"),a=e.getAttribute("data-hostname");return void(t&&a&&t.textContent!==a&&(t.textContent=a))}e.setAttribute("data-shadcn-built","1");const t=this._mediaBase(),a=e.getAttribute("data-hostname")||"Shadcn";for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(E("div",{class:"sidebar-header"},[E("a",{class:"sidebar-brand",href:"/"},[E("img",{class:"sidebar-logo",src:`${t}/images/logo.svg`,alt:"Shadcn",width:"24",height:"24"}),E("span",{class:"sidebar-brand-text"},[a])]),E("button",{id:"sidebar-toggle-btn",class:"sidebar-toggle",type:"button","aria-label":_("Navigation")},[E("span",{class:"icon-collapse"},[this._iconFile("panel-left-close",16)]),E("span",{class:"icon-expand"},[this._iconFile("panel-left-open",16)])])])),e.appendChild(E("nav",{class:"sidebar-nav",id:"sidebar-nav"})),e.appendChild(E("div",{class:"sidebar-footer",id:"sidebar-footer",hidden:""}))},render(e){const{branch:t,branchUrl:a}=this._resolveMenuBranch(e);this.tree=e,this.branch=t,this.branchUrl=a,this.renderSidebarNav(t,a),this.renderBreadcrumb(t,a),this.initPalette(t,a),this.renderTabs(e)},renderTabs(e){const t=L.env.dispatchpath||[],a=document.getElementById("tabmenu");a&&(a.innerHTML="",a.style.display="none");let s=e,n="";if(t.length>=3){for(var i=0;i<3&&s;i++){const e=t[i];if(!s.children||!s.children[e])break;s=s.children[e],n=n+(n?"/":"")+e}s&&this.renderTabMenu(s,n)}},syncRoute(){if(!this.tree)return;const e=document.getElementById("sidebar");if(e){e.querySelectorAll(".active").forEach(e=>e.classList.remove("active"));const t=new URL(L.url(...(L.env.dispatchpath||[]).slice(0,3)),location.href).pathname;let a=null,s=-1;if(e.querySelectorAll("a.sidebar-sub-link, a.sidebar-nav-parent, a.sidebar-logout").forEach(e=>{const n=e.pathname;(t===n||t.startsWith(n+"/"))&&n.length>s&&(a=e,s=n.length)}),a){const t=a.closest(".sidebar-sub-item");if(t){t.classList.add("active");const s=a.closest(".sidebar-accordion-item");s&&(s.classList.add("active"),"true"!==e.getAttribute("data-collapsed")&&e.querySelectorAll(".sidebar-accordion-item").forEach(e=>e.setAttribute("data-open",e===s?"true":"false")))}else a.classList.contains("sidebar-logout")?a.classList.add("active"):a.closest(".sidebar-nav-item")?.classList.add("active")}}this.renderBreadcrumb(this.branch,this.branchUrl),this.renderTabs(this.tree)},closeSurfaces(){this.closePalette(),window.ShadcnSidebar?.closeDrawer?.(),window.ShadcnSidebar?._hideCollapsedPopover?.()},renderSidebarNav(e,t){const a=document.getElementById("sidebar-nav"),s=document.getElementById("sidebar-footer");if(!a)return;const n=ui.menu.getChildren(e),i=L.env.dispatchpath||[],r="1"===document.getElementById("sidebar")?.getAttribute("data-shadcn-restored"),l=r?Array.from(a.querySelectorAll('.sidebar-accordion-item[data-open="true"]'),e=>e.getAttribute("data-section")):null,o=r?a.scrollTop:0;a.innerHTML="",s&&(s.innerHTML="",s.hidden=!0),n.forEach(e=>{if(this._isLogoutMenuItem(e)&&s){const a=i[1]==e.name,n=this._sectionIcon(e.name,18);returLine truncated
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-Line truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-Line truncated
|
||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.9.9
|
||||
PKG_RELEASE:=15
|
||||
PKG_VERSION:=2026.9.10
|
||||
PKG_RELEASE:=16
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=netdata
|
||||
PKG_VERSION:=2.11.0
|
||||
PKG_RELEASE:=17
|
||||
PKG_VERSION:=2.11.1
|
||||
PKG_RELEASE:=18
|
||||
|
||||
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
+3
-3
@@ -49,12 +49,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.7
|
||||
PKG_RELEASE:=41
|
||||
PKG_VERSION:=1.6.7_p10
|
||||
PKG_RELEASE:=42
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=b5ec86779fb145d23d0fe5085241c8b64f05b930
|
||||
PKG_SOURCE_VERSION:=6d135f52ec5a686b162bd6455917ce25c8080ae0
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
Reference in new issue
Block a user