🎨 Sync 2026-09-17 01:27:32

This commit is contained in:
github-actions[bot] committed 2026-09-17 01:27:32 +08:00
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"));
});
+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.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"));
});
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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