🎉 Sync 2026-10-13 02:34:41

This commit is contained in:
github-actions[bot] committed 2026-10-13 02:34:41 +08:00
1 parent b537a62e59
commit 72c44a6325
38 files changed
+3510 -1734

No files matched your search

+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage
PKG_VERSION:=1.0.3
PKG_RELEASE:=16
PKG_VERSION:=1.0.4
PKG_RELEASE:=17
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme
@@ -46,7 +46,8 @@ const ICONS = {
moon: [ [ 'path', { d: 'M19.5 14.5A7.5 7.5 0 0 1 9.5 4.5a7.5 7.5 0 1 0 10 10z' } ] ],
auto: [ [ 'circle', { cx: '12', cy: '12', r: '8' } ], [ 'path', { d: 'M12 4a8 8 0 0 1 0 16z', 'class': 'vt-fill' } ] ],
logout: [ [ 'path', { d: 'M14 4h4.5A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5H14' } ], [ 'path', { d: 'M10 8l-4 4 4 4M6 12h10' } ] ],
pin: [ [ 'path', { d: 'M4 5v14M9 12h11M15 7l5 5-5 5' } ] ]
pin: [ [ 'path', { d: 'M4 5v14M9 12h11M15 7l5 5-5 5' } ] ],
info: [ [ 'circle', { cx: '12', cy: '12', r: '8.5' } ], [ 'path', { d: 'M12 11v5.5' } ], [ 'circle', { cx: '12', cy: '7.75', r: '.6' } ] ]
};
function iconFor(name) {
@@ -180,6 +181,11 @@ function technical(text) {
return /\d/.test(rest) && /^[\s0-9a-fA-F.:,\/%()+\-x[\]~=]*$/.test(rest);
}
/* heading text as compared for repeats: case, spacing and a trailing colon don't count */
function headingKey(text) {
return String(text || '').replace(/\s+/g, ' ').replace(/[\s:]+$/, '').trim().toLowerCase();
}
/* ---------------------------------------------------------------------------
* Stock-page enhancers, only where CSS cannot restructure LuCI's markup
* (Status -> Overview: fused memory bar, per-mount storage bars, radio band
@@ -410,11 +416,18 @@ function humpPath(points) {
return 'M' + r(x0) + ',' + r(base) + 'C' + r(x0 + k) + ',' + r(cy) + ' ' + r(x3 - k) + ',' + r(cy) + ' ' + r(x3) + ',' + r(base) + 'Z';
}
/* "#89cdb4" -> "rgb(137, 205, 180)", the form element.style reads back */
function hexRgb(hex) {
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(hex));
return m ? 'rgb(' + [ m[1], m[2], m[3] ].map(h => parseInt(h, 16)).join(', ') + ')' : null;
}
function setupChannelAnalysis() {
const view = document.getElementById('view');
const own = new Set();
const slots = new Map();
let next = 0;
let colourOf = null;
/* slot 1 (the accent) is the AP itself; neighbours take 2..6 in the order
they first appear (the view sorts by channel, so neighbours differ) */
const slotFor = (colour, mine) => {
@@ -455,9 +468,17 @@ function setupChannelAnalysis() {
const dot = tr.querySelector('span[style*="color"]');
const cells = tr.querySelectorAll(':scope > td, :scope > .td');
if (!cells.length) return;
const bssid = cells[cells.length - 1].textContent.trim().toUpperCase();
const printed = cells[cells.length - 1].textContent.trim();
const bssid = printed.toUpperCase();
const mine = own.has(bssid);
if (tr.classList.contains('v-ca-own') !== mine) tr.classList.toggle('v-ca-own', mine);
/* the view prints its own interface's row without the colour
dot, so rebuild that colour the way the view made it: from
the BSSID as printed (the derivation is case-sensitive) */
if (mine && colourOf) {
const derived = hexRgb(colourOf(printed));
if (derived) mineByColour.set(derived, true);
}
if (!dot) return;
const colour = dot.style.color;
if (mine) mineByColour.set(colour, true);
@@ -531,7 +552,15 @@ function setupChannelAnalysis() {
busy = false;
};
L.resolveDefault(callWirelessDevices(), {}).then(devs => {
/* tools.prng is what the view colours networks with (it is loaded
already); derive_color reseeds on every call, so calling it here
leaves the view's colours as they are */
Promise.all([
L.resolveDefault(callWirelessDevices(), {}),
L.resolveDefault(L.require('tools.prng'), null)
]).then(([ devs, prng ]) => {
if (prng && typeof prng.derive_color === 'function')
colourOf = b => prng.derive_color(b);
for (const r of Object.values(devs || {}))
for (const i of (r && Array.isArray(r.interfaces)) ? r.interfaces : [])
[ i.iwinfo && i.iwinfo.bssid, i.config && i.config.macaddr ].forEach(b => {
@@ -558,10 +587,12 @@ return baseclass.extend({
this.closeTimer = null;
this.openTimer = null;
this.entries = [];
this.trailKeys = new Set();
this.setupTheme();
this.setupIndicators();
this.setupNumerals();
this.setupForms();
setupStockEnhancers();
setupCharts();
this.setupKeys();
@@ -605,6 +636,7 @@ return baseclass.extend({
this.renderCrumbs();
this.renderTabs();
this.buildIndex();
this.queueForms();
},
isPinned() {
@@ -861,6 +893,7 @@ return baseclass.extend({
});
this.pageTitle = parts[parts.length - 1].title;
parts.forEach(p => this.trailKeys.add(headingKey(p.title)));
this.updateTitle();
},
@@ -1194,5 +1227,126 @@ return baseclass.extend({
window.requestAnimationFrame(scan);
}).observe(main, { childList: true, subtree: true, characterData: true });
scan();
},
/* ---------------------------------------------------- form chrome */
/* LuCI renders forms into #view and the modal, and renders them again
after a save: watch the body and decorate what is new. This adds (i)
buttons and sets classes and attributes; LuCI's text and the order of
LuCI's nodes stay as they are. */
setupForms() {
this.formsQueued = false;
this.helpSeq = 0;
this.helpSizes = window.ResizeObserver ? new ResizeObserver(entries => entries.forEach(e => this.fitHelp(e.target))) : null;
if (window.MutationObserver)
new MutationObserver(() => this.queueForms()).observe(document.body, { childList: true, subtree: true });
this.decorateForms();
},
queueForms() {
if (this.formsQueued) return;
this.formsQueued = true;
window.requestAnimationFrame(() => {
this.formsQueued = false;
this.decorateForms();
});
},
decorateForms() {
document.querySelectorAll('.cbi-value > .cbi-value-title + .cbi-value-field > .cbi-value-description:not(.vt-help-text)')
.forEach(desc => this.addFieldHelp(desc));
document.querySelectorAll(':is(.cbi-map-descr, .cbi-section-descr):not(:has(> .vt-help))')
.forEach(desc => this.addIntroHelp(desc));
this.markRepeats();
},
/* Help text shows its first line; (i) unfolds the rest. LuCI also puts
warnings here, so none is hidden whole: its start stays visible.
Field help: the button sits in the label column, after the field,
because LuCI's label click handler finds the widget through
nextElementSibling. */
addFieldHelp(desc) {
const row = desc.parentNode.parentNode;
const title = row.querySelector(':scope > .cbi-value-title');
row.appendChild(this.helpToggle(desc, row, _('Show full help: %s').format(title.textContent.trim()), _('Show full help')));
},
/* Page and section intros: the button leads the first line, inside the
description, since nothing may stand between LuCI's siblings (ui.tabs
finds a tab menu as its pane group's previous sibling). The fold needs
the button, so a view that rewrote the text would show it whole until
the next pass adds the button again. The cut-off line unfolds on a
click too, a bigger target on touch than the icon. */
addIntroHelp(desc) {
if (desc.closest('.vt-app') || !desc.textContent.trim()) return;
desc.insertBefore(this.helpToggle(desc, desc, _('Show full description'), _('Show full description')), desc.firstChild);
if (desc._vtIntroClick) return;
desc._vtIntroClick = true;
desc.addEventListener('click', ev => {
const btn = desc.querySelector(':scope > .vt-help');
if (!btn || btn.hidden || desc.getAttribute('data-v-help') !== 'closed') return;
if (ev.target.closest('a, button') || String(window.getSelection())) return;
btn.click();
});
},
/* The (i) button for desc. Its state lives on holder (the field row, or
the intro itself) as data-v-help, which the CSS folds by. */
helpToggle(desc, holder, label, title) {
const state = holder.getAttribute('data-v-help') || 'closed';
holder.setAttribute('data-v-help', state);
desc.classList.add('vt-help-text');
if (!desc.id) desc.id = 'vt-help-' + (++this.helpSeq);
const btn = E('button', {
'class': 'vt-help', 'type': 'button', 'hidden': '',
'aria-expanded': String(state === 'open'), 'aria-controls': desc.id,
'aria-label': label, 'title': title,
'click': () => {
const open = holder.getAttribute('data-v-help') !== 'open';
holder.setAttribute('data-v-help', open ? 'open' : 'closed');
btn.setAttribute('aria-expanded', String(open));
this.fitHelp(desc);
}
}, [ svg('info', 16) ]);
desc._vtHelp = { btn: btn, holder: holder };
if (this.helpSizes) this.helpSizes.observe(desc);
else btn.hidden = false;
return btn;
},
/* (i) shows only while the one-line clamp cuts the text. Text that is
not displayed (another tab, an unmet dependency) measures 0 and waits
for the next resize notification. */
fitHelp(desc) {
if (!desc.isConnected) {
if (this.helpSizes) this.helpSizes.unobserve(desc);
return;
}
const h = desc._vtHelp;
if (!h || !h.btn.isConnected || !desc.clientHeight) return;
const cut = desc.scrollHeight > desc.clientHeight + 1;
h.btn.hidden = !cut && h.holder.getAttribute('data-v-help') !== 'open';
},
/* A page title or card heading that only repeats the breadcrumb, the
map title or its own tab's name is hidden visually; screen readers
still get it as a heading. Headings that hold a control (Overview's
Hide/Show label) always stay. */
markRepeats() {
/* template pages (Status -> Overview) print their title before #view */
document.querySelectorAll('#maincontent > h2, #view h2, #view .cbi-section > h3:first-child').forEach(h => {
if (h.closest('.vt-app') || h.querySelector('a, button, input, select, [data-clickable], [tabindex]')) return;
const key = headingKey(h.textContent);
let repeat = this.trailKeys.has(key);
if (!repeat && h.tagName === 'H3') {
const map = h.closest('.cbi-map');
const mapTitle = map && map.querySelector(':scope > h2');
const pane = h.closest('[data-tab-title]');
repeat = (mapTitle && headingKey(mapTitle.textContent) === key) ||
(pane && headingKey(pane.getAttribute('data-tab-title')) === key);
}
h.classList.toggle('vt-repeat', !!(key && repeat));
});
}
});
@@ -39,7 +39,9 @@
--rail-w: 56px;
--rail-w-pinned: 204px;
--top-h: 52px;
--ctl-h: 32px;
--ctl-h: 36px; /* every control: inputs, selects, buttons, tabs, chips */
--label-w: 180px; /* form label column */
--field-max: 360px; /* widest single-line field */
--content-max: 1320px;
/* light: cool paper grey, olive accent text */
@@ -72,6 +74,9 @@
--scrim: rgba(20, 23, 26, .38);
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
--zone-mix: .22;
--seg-bg: var(--s3); /* segmented control: track, selected segment */
--seg-on: var(--s2);
--seg-on-ring: 0 0 0 1px var(--line), var(--v-shadow);
}
html[data-darkmode="true"] {
@@ -107,6 +112,14 @@ html[data-darkmode="true"] {
--scrim: rgba(0, 0, 0, .58);
--shadow-pop: 0 16px 40px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .02);
--zone-mix: .2;
--seg-bg: var(--bg);
--seg-on: var(--s3);
--seg-on-ring: 0 0 0 1px var(--line-2);
}
/* touch: controls are sized by the pointer, not the screen width */
@media (pointer: coarse) {
:root { --ctl-h: 44px; }
}
/* Shared Vantage tokens (--v-*): the stable contract other Vantage views
@@ -136,9 +149,17 @@ html[data-darkmode="true"] {
--v-info-soft: var(--info-soft);
--v-font: var(--font);
--v-mono: var(--mono);
--v-fs-micro: var(--fs-micro); /* the type scale: 10.5 12 13 14 16 20 24 px */
--v-fs-sm: var(--fs-sm);
--v-fs-base: var(--fs-base);
--v-fs-md: var(--fs-md);
--v-fs-lg: var(--fs-lg);
--v-fs-xl: var(--fs-xl);
--v-fs-2xl: var(--fs-2xl);
--v-r-sm: var(--r-sm);
--v-r: var(--r);
--v-r-lg: var(--r-lg);
--v-ctl-h: var(--ctl-h); /* every control: 36px, 44px on touch */
--v-shadow: 0 1px 2px rgba(20, 23, 26, .06), 0 1px 0 rgba(20, 23, 26, .03);
/* where sticky content may start: below the top bar and page tabs */
--v-stick-top: var(--stick-top);
@@ -508,7 +529,7 @@ a.vt-crumb:hover { color: var(--text); }
font-size: var(--fs-sm);
white-space: nowrap;
}
.vt-host b { color: var(--text); font-weight: 600; font-family: var(--mono); font-size: 11.5px; }
.vt-host b { color: var(--text); font-weight: 600; font-family: var(--mono); font-size: var(--fs-micro); }
.vt-host span { overflow: hidden; text-overflow: ellipsis; }
.vt-host-sep { color: var(--text-3); }
@@ -555,7 +576,7 @@ a.vt-crumb:hover { color: var(--text); }
min-width: 20px; height: 20px; padding: 0 6px;
border-radius: 999px;
background: var(--warn); color: #1a1200;
font: 700 11.5px/1 var(--mono);
font: 700 var(--fs-micro)/1 var(--mono);
}
#indicators > span[data-indicator="media_error"] { color: var(--err); border-color: var(--err); background: var(--err-soft); }
@@ -575,7 +596,7 @@ a.vt-crumb:hover { color: var(--text); }
.vt-tab {
position: relative;
display: inline-flex; align-items: center;
height: 40px; padding: 0 var(--sp-3);
height: var(--ctl-h); padding: 0 var(--sp-3);
color: var(--text-2);
font-weight: 500;
white-space: nowrap;
@@ -691,7 +712,7 @@ a.vt-crumb:hover { color: var(--text); }
/* page = title block + a stack of cards. Cards carry a full-bleed header
row (h3) and full-bleed tables; nested sections lose their chrome. */
:root { --card-px: 20px; --card-py: 16px; --stick-top: var(--top-h); }
html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + var(--ctl-h) + 1px); }
.cbi-map { margin-bottom: var(--sp-6); }
.cbi-map > h2, .vt-main > h2, #view > h2, #view > div > h2:first-child, .cbi-map > h2:first-child {
@@ -704,18 +725,18 @@ html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
max-width: 76ch;
margin: 0 0 var(--sp-6);
color: var(--text-2);
font-size: 13.5px; line-height: 1.55;
font-size: var(--fs-base); line-height: 1.55;
}
.cbi-section-descr { max-width: 80ch; margin: 0 0 var(--sp-4); color: var(--text-2); }
.cbi-map-descr:empty, .cbi-section-descr:empty { display: none; }
.cbi-map > h2 + .cbi-tabmenu, .cbi-map > h2 + .cbi-map-descr:empty + .cbi-tabmenu { margin-top: var(--sp-4); }
.cbi-map > h2:not(.vt-repeat) + .cbi-tabmenu, .cbi-map > h2:not(.vt-repeat) + .cbi-map-descr:empty + .cbi-tabmenu { margin-top: var(--sp-4); }
.cbi-section, .cbi-map > fieldset {
margin: 0 0 var(--sp-4);
padding: var(--card-py) var(--card-px);
background: var(--s1);
border: 1px solid var(--line);
border-radius: 10px;
border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
min-width: 0;
background-clip: padding-box;
@@ -754,22 +775,27 @@ html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
}
.cbi-section .cbi-section > h3:first-child { margin: 0 0 var(--sp-3); padding: 0; min-height: 0; border: 0; }
.cbi-section .cbi-section + .cbi-section { margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--line); }
/* a nested table section scrolls its own table, so it takes the card bleed
itself (out by --card-px, padded back in): the table's bleed then stays
inside the scroll box instead of overflowing it by --card-px */
.cbi-section .cbi-section.cbi-tblsection {
margin-left: calc(-1 * var(--card-px)); margin-right: calc(-1 * var(--card-px));
padding-left: var(--card-px); padding-right: var(--card-px);
}
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: var(--sp-3); }
/* option rows: two aligned columns on desktop */
/* option rows: label column + field, no rules between rows */
.cbi-value {
display: grid;
grid-template-columns: minmax(160px, 260px) minmax(0, 1fr);
column-gap: var(--sp-8);
grid-template-columns: var(--label-w) minmax(0, 1fr);
column-gap: var(--sp-5);
align-items: start;
padding: 14px 0;
border-bottom: 1px solid var(--line);
padding: 6px 0;
}
.cbi-value:last-child, .cbi-value.cbi-value-last { border-bottom: 0; }
.cbi-value > .cbi-value-title {
padding-top: 7px;
padding-top: calc((var(--ctl-h) - 1.5em) / 2); /* 1.5em: one line at line-height 1.5 */
color: var(--text);
font-weight: 500;
overflow-wrap: anywhere;
@@ -790,6 +816,64 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v
line-height: 1.5;
}
.cbi-value-helpicon img { width: 14px; height: 14px; opacity: .6; }
/* folding help (menu-vantage.js): one line until (i) unfolds it; the
button shares the label cell, so both are placed explicitly */
.cbi-value[data-v-help="closed"] > .cbi-value-field > .vt-help-text {
display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; line-clamp: 1;
max-height: 1.5em;
overflow: hidden;
}
.cbi-value[data-v-help] > .cbi-value-title { grid-row: 1; grid-column: 1; }
.cbi-value[data-v-help] > .cbi-value-field { grid-row: 1; }
.cbi-value:has(> .vt-help:not([hidden])) > .cbi-value-title { padding-right: 32px; }
.cbi-value > .vt-help {
grid-row: 1; grid-column: 1;
justify-self: end;
display: grid; place-items: center;
width: 28px; height: var(--ctl-h);
padding: 0;
background: none; border: 0; border-radius: var(--r);
color: var(--text-3);
cursor: pointer;
}
/* a title that opens with <br> (Wireless "Operating frequency", lined up
with captioned selects) has its text on the second line: so has (i) */
.cbi-value:has(> .cbi-value-title > br:first-child) > .vt-help { margin-top: 1.5em; }
/* page and section intros fold the same way, (i) leading the first line;
the fold needs the button, so without it the text shows whole */
:is(.cbi-map-descr, .cbi-section-descr)[data-v-help="closed"]:has(> .vt-help) {
display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; line-clamp: 1;
overflow: hidden;
}
.cbi-map-descr[data-v-help="closed"]:has(> .vt-help) { max-height: 1.55em; } /* its line-height */
.cbi-section-descr[data-v-help="closed"]:has(> .vt-help) { max-height: 1.5em; }
:is(.cbi-map-descr, .cbi-section-descr)[data-v-help="closed"]:has(> .vt-help:not([hidden])) { cursor: pointer; }
:is(.cbi-map-descr, .cbi-section-descr) > .vt-help {
display: inline-grid; place-items: center;
width: 1.5em; height: 1.5em;
margin: 0 6px 0 -3px; padding: 0;
vertical-align: top;
background: none; border: 0; border-radius: var(--r-sm);
color: var(--text-3);
cursor: pointer;
}
/* both buttons, after their placement rules */
.vt-help svg { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.vt-help:hover { color: var(--text); background: var(--s3); }
.vt-help[aria-expanded="true"] { color: var(--accent-text); }
/* a heading that repeats the breadcrumb or its tab (menu-vantage.js):
gone from view, still a heading for screen readers */
.vt-repeat {
position: absolute !important;
width: 1px; height: 1px; min-height: 0 !important;
margin: -1px !important; padding: 0 !important; border: 0 !important;
overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.cbi-value.cbi-value-error .cbi-value-title,
.cbi-value-field .cbi-input-invalid + .cbi-value-description { color: var(--err); }
@@ -812,7 +896,7 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v
backdrop-filter: saturate(1.3) blur(14px);
-webkit-backdrop-filter: saturate(1.3) blur(14px);
border: 1px solid var(--line-2);
border-radius: 12px;
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
}
.cbi-page-actions > * { margin: 0 !important; }
@@ -833,7 +917,7 @@ input, select, textarea, button { font: inherit; color: inherit; }
.vt-main input[type="date"], .vt-main input[type="time"], .vt-main input[type="datetime-local"],
.vt-main input:not([type]), .modal input[type="text"], .modal input[type="password"], .modal input:not([type]), .modal select, .modal textarea {
width: 100%;
max-width: 440px;
max-width: var(--field-max);
min-height: var(--ctl-h);
padding: 5px 10px;
background: var(--s2);
@@ -844,7 +928,7 @@ input, select, textarea, button { font: inherit; color: inherit; }
line-height: 20px;
transition: border-color .12s, box-shadow .12s;
}
.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: none; min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: calc(2 * var(--field-max)); min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
.vt-main select, .cbi-input-select, .modal select {
appearance: none; -webkit-appearance: none;
padding-right: 30px;
@@ -853,6 +937,13 @@ input, select, textarea, button { font: inherit; color: inherit; }
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
/* a native select in a form row cuts a long choice off without an
ellipsis: let it grow past --field-max to fit (field-sizing: the chosen
value; elsewhere the widest option), never past its column */
.cbi-value > .cbi-value-field > select:not([multiple]), .cbi-value > .cbi-value-field > div > select:not([multiple]) {
field-sizing: content;
width: auto; min-width: min(100%, var(--field-max)); max-width: 100%;
}
.vt-main select[multiple] { background-image: none; padding-right: 10px; }
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 1; }
input:hover, select:hover, textarea:hover { border-color: var(--text-3); }
@@ -876,7 +967,7 @@ input[readonly] { cursor: default; }
/* password + reveal and other control groups */
.control-group {
display: flex; align-items: stretch; flex-wrap: nowrap;
max-width: 440px;
max-width: var(--field-max);
}
.control-group > * { margin: 0 !important; }
.control-group > :not(:first-child) { margin-left: -1px !important; }
@@ -913,7 +1004,7 @@ input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outlin
input[type="checkbox"][disabled]:checked { opacity: .5; }
/* range slider */
.cbi-range-slider { display: flex; align-items: center; gap: var(--sp-3); max-width: 440px; min-height: var(--ctl-h); flex-wrap: wrap; }
.cbi-range-slider { display: flex; align-items: center; gap: var(--sp-3); max-width: var(--field-max); min-height: var(--ctl-h); flex-wrap: wrap; }
.cbi-range-slider input[type="range"] { flex: 1 1 160px; accent-color: var(--accent-text); }
.cbi-range-slider-value, .cbi-range-slider-calc { font-family: var(--mono); color: var(--text-2); font-size: var(--fs-sm); }
@@ -942,7 +1033,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
opacity: .45; cursor: not-allowed; pointer-events: none;
}
.cbi-button-up, .cbi-button-down { min-width: 28px; padding: 0 6px; }
.cbi-button-up, .cbi-button-down { min-width: var(--ctl-h); padding: 0 6px; }
.button-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-4); }
.right > .btn, .right > .cbi-button, .right > button { margin-left: var(--sp-2); }
@@ -951,7 +1042,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dropdown {
position: relative;
display: inline-flex; align-items: stretch;
width: 100%; max-width: 440px;
width: 100%; max-width: var(--field-max);
min-height: var(--ctl-h);
padding: 0;
background: var(--s2);
@@ -1044,8 +1135,8 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
}
.cbi-dropdown[open] > ul.dropdown[style*="bottom"] { top: auto; }
.cbi-dropdown[open] > ul.dropdown > li {
display: flex;
min-height: 30px;
display: flex; align-items: center;
min-height: calc(var(--ctl-h) - 4px);
padding: 4px 8px;
border-radius: var(--r-sm);
white-space: normal;
@@ -1062,6 +1153,18 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dropdown[multiple][open] > ul.dropdown > li > form { display: inline-flex; }
.cbi-dropdown[open] > ul.dropdown > li input.create-item-input { max-width: none; min-height: 28px; }
.cbi-dropdown[open] > .open { color: var(--accent-text); }
/* one arrow for every field: the select's triangle, drawn where LuCI puts
its "▾" glyph (ComboButtons keep theirs) */
.cbi-dropdown:not(.btn, .cbi-button) > .open {
--arrow: var(--text-3);
width: 30px; padding: 0;
font-size: 0;
background-image: linear-gradient(45deg, transparent 50%, var(--arrow) 50%), linear-gradient(135deg, var(--arrow) 50%, transparent 50%);
background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.cbi-dropdown:not(.btn, .cbi-button)[open] > .open { --arrow: var(--accent-text); }
/* ComboButton: dropdown dressed as a split button */
.cbi-dropdown.btn, .cbi-dropdown.cbi-button {
@@ -1139,12 +1242,12 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
:is(.btn, .cbi-button).cbi-button-reset:hover { color: var(--text); }
/* dynamic list */
.cbi-dynlist { display: flex; flex-direction: column; gap: 6px; max-width: 440px; }
.cbi-dynlist { display: flex; flex-direction: column; gap: 6px; max-width: var(--field-max); }
.cbi-dynlist > .item {
position: relative;
display: flex; align-items: center;
min-height: 30px;
padding: 4px 34px 4px 10px;
min-height: var(--ctl-h);
padding: 4px calc(var(--ctl-h) + 4px) 4px 10px;
background: var(--s2);
border: 1px solid var(--line);
border-radius: var(--r);
@@ -1155,11 +1258,11 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dynlist > .item::after {
content: "\00d7";
position: absolute; top: 0; right: 0; bottom: 0;
width: 30px;
width: var(--ctl-h);
display: flex; align-items: center; justify-content: center;
border-left: 1px solid var(--line);
color: var(--text-3);
font: 400 16px/1 var(--font);
font: 400 var(--fs-lg)/1 var(--font);
cursor: pointer;
}
.cbi-dynlist > .item:hover::after { color: var(--err); background: var(--err-soft); border-radius: 0 var(--r) var(--r) 0; }
@@ -1168,7 +1271,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dynlist[disabled] > .item::after { display: none; }
.cbi-dynlist > .add-item { max-width: none; }
.cbi-dynlist > .add-item > .cbi-dropdown { max-width: none; }
.cbi-dynlist > .add-item > .cbi-button-add { min-width: 34px; font-family: var(--mono); font-size: var(--fs-md); }
.cbi-dynlist > .add-item > .cbi-button-add { min-width: var(--ctl-h); font-family: var(--mono); font-size: var(--fs-md); }
/* add rows built without .control-group (e.g. System → SSH-Keys): input and button side by side */
.cbi-dynlist > .add-item:not(.control-group) { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); max-width: 720px; }
.cbi-dynlist > .add-item:not(.control-group) > input { flex: 1 1 320px; min-width: 0; margin: 0; }
@@ -1242,13 +1345,13 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
.td:not(.cbi-value-field)[data-title*="Target" i], .td:not(.cbi-value-field)[data-title*="Source" i], .td:not(.cbi-value-field)[data-title*="DUID" i], .td:not(.cbi-value-field)[data-title="PID"],
.td:not(.cbi-value-field)[data-title*="Metric" i], .td:not(.cbi-value-field)[data-title*="Priority" i], .td:not(.cbi-value-field)[data-title*="Remaining" i], .td:not(.cbi-value-field)[data-title*="Usage" i] {
white-space: nowrap;
font-family: var(--mono); font-size: 12.5px;
font-family: var(--mono); font-size: var(--fs-sm);
font-variant-numeric: tabular-nums slashed-zero;
}
.td > span:has(> span + br + span) { display: flex; flex-direction: column; gap: 1px; }
.td > span:has(> span + br + span) > br { display: none; }
.td > span:has(> span + br + span) > span { white-space: nowrap; font: 12px/1.5 var(--mono); }
.td > span:has(> span + br + span) > span:not(:first-child) { color: var(--text-3); font-size: 11.5px; }
.td > span:has(> span + br + span) > span { white-space: nowrap; font: var(--fs-sm)/1.5 var(--mono); }
.td > span:has(> span + br + span) > span:not(:first-child) { color: var(--text-3); font-size: var(--fs-micro); }
/* badges inside cells are content, not pills: icon + text */
.td .ifacebadge, .td .ifacebadge.large {
@@ -1257,7 +1360,7 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
font-size: inherit; white-space: nowrap;
}
.td .ifacebadge img, .td .ifacebadge img[style] { width: 18px !important; height: 18px !important; flex: none; }
.td .ifacebadge small { color: var(--text-3); font-family: var(--mono); font-size: 11.5px; }
.td .ifacebadge small { color: var(--text-3); font-family: var(--mono); font-size: var(--fs-micro); }
.td > .ifacebadge + .ifacebadge, .td > span + .ifacebadge { margin-left: 10px; }
.cbi-section-table-descr > .th { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); }
.cbi-section-table-cell, .cbi-section-table-row > .td { vertical-align: middle; }
@@ -1273,7 +1376,7 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
.td.cbi-value-field.inactive { visibility: hidden; }
.td.cbi-section-actions, .th.cbi-section-actions { text-align: right; white-space: nowrap; width: 1%; }
.td.cbi-section-actions > div { display: inline-flex; gap: 4px; flex-wrap: nowrap; align-items: center; }
.td.cbi-section-actions .btn, .td.cbi-section-actions .cbi-button { min-height: 28px; padding: 0 10px; font-size: var(--fs-sm); }
.td.cbi-section-actions .btn, .td.cbi-section-actions .cbi-button { padding: 0 10px; }
.drag-handle { cursor: grab; color: var(--text-3); padding: 0 6px; font-family: var(--mono); }
.drag-over-above > .td, .drag-over-above > td { box-shadow: inset 0 2px 0 var(--accent-text); }
.drag-over-below > .td, .drag-over-below > td { box-shadow: inset 0 -2px 0 var(--accent-text); }
@@ -1294,38 +1397,120 @@ div.right, div.left { display: block; }
div.right { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }
div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0; }
/* in-map section tabs (ui.tabs) */
/* Tabs inside a page (ui.tabs), one look per level:
- tabs standing at page level (not inside a form section or another
tab) are the page's sub-pages when the menu shows no page tabs: they
take the page-tab underline, like .vt-tabs (Interfaces / Devices /
..., DHCP's Leases / dnsmasq, Channel Analysis's radios). Under
.vt-tabs they are the first level inside the page instead;
- the first level inside a page, form or modal: a segmented control;
- tabs inside a tab: a side list where the pane is wide enough
(container query on the pane, so modals and pages alike), chips
where it is not and at every deeper level.
A level is a pane of a section's option tabs (.cbi-section-node-tabbed);
map-level panes are not counted, so a page's sub-pages add no level. */
.cbi-tabmenu {
display: flex; flex-wrap: nowrap; gap: var(--sp-1);
margin: 0 0 var(--sp-4); padding: 0;
display: flex; flex-wrap: nowrap; align-items: center; gap: 2px;
width: fit-content; max-width: 100%; min-height: var(--ctl-h);
margin: 0 0 var(--sp-5); padding: 2px;
list-style: none;
border-bottom: 1px solid var(--line);
background: var(--seg-bg);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow-x: auto;
scrollbar-width: none;
}
.cbi-tabmenu::-webkit-scrollbar { display: none; }
.cbi-tabmenu > li { position: relative; flex: none; }
.cbi-tabmenu > li > a {
display: inline-flex; align-items: center; gap: 6px;
height: 36px; padding: 0 var(--sp-3);
display: flex; align-items: center; gap: 6px;
height: calc(var(--ctl-h) - 6px); padding: 0 var(--sp-3);
border-radius: var(--r);
color: var(--text-2);
font-weight: 500;
white-space: nowrap;
text-decoration: none !important;
}
.cbi-tabmenu > li > a:hover { color: var(--text); }
.cbi-tabmenu > li.cbi-tab > a { color: var(--text); font-weight: 600; }
.cbi-tabmenu > li.cbi-tab::after {
content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: -1px;
.cbi-tabmenu > li.cbi-tab > a { background: var(--seg-on); color: var(--text); font-weight: 600; box-shadow: var(--seg-on-ring); }
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) {
gap: var(--sp-1);
width: auto; padding: 0;
background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
}
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { height: var(--ctl-h); border-radius: 0; }
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab > a { background: none; box-shadow: none; }
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab::after {
content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: 0;
height: 2px; background: var(--accent-text); border-radius: 2px 2px 0 0;
}
.cbi-section-node-tabbed > [data-tab-title] { container-type: inline-size; }
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
flex-wrap: wrap; gap: var(--sp-2);
width: auto; margin-bottom: var(--sp-4); padding: 0;
background: none; border: 0; border-radius: 0;
overflow: visible;
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
height: var(--ctl-h); padding: 0 14px;
border: 1px solid var(--line-2); border-radius: 999px;
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a {
background: var(--accent-soft); border-color: var(--accent-line);
color: var(--accent-text); box-shadow: none;
}
@container (min-width: 640px) {
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) {
display: grid;
grid-template-columns: 168px minmax(0, 1fr);
column-gap: var(--sp-6);
align-items: start;
}
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > * { grid-column: 1 / -1; }
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * { grid-column: 2; }
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
grid-column: 1;
flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 2px;
margin: 0;
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
justify-content: space-between;
padding: 0 var(--sp-3);
border: 0; border-radius: var(--r);
white-space: normal; line-height: 1.25;
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: var(--s3); }
.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent-line); }
/* beside the list there is no card edge to bleed to: table sections and
tables fit the column */
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .cbi-tblsection { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table { width: 100% !important; margin-left: 0; margin-right: 0; }
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):first-child { padding-left: 0; }
.cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):last-child { padding-right: 0; }
/* deeper levels: back to the chips above */
.cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) { display: block; }
.cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
margin: 0 0 var(--sp-4);
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
justify-content: normal;
padding: 0 14px;
border: 1px solid var(--line-2); border-radius: 999px;
white-space: nowrap; line-height: inherit;
}
.cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: none; }
.cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { border-color: var(--accent-line); box-shadow: none; }
}
.cbi-tabmenu > li[data-errors] > a::after {
content: attr(data-errors);
display: inline-flex; align-items: center; justify-content: center;
min-width: 18px; height: 18px; padding: 0 5px;
border-radius: 999px;
background: var(--err); color: #fff;
font: 700 10.5px/1 var(--mono);
font: 700 var(--fs-micro)/1 var(--mono);
}
.cbi-tabmenu > li.cbi-tab-disabled[data-errors] > a { color: var(--err); }
[data-tab-title]:not([data-tab-active="true"]) { display: none; }
@@ -1386,13 +1571,13 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
min-width: 0;
white-space: normal; overflow-wrap: anywhere;
color: var(--text);
font: 400 12px/1.55 var(--mono);
font: 400 var(--fs-sm)/1.55 var(--mono);
font-variant-numeric: tabular-nums slashed-zero;
}
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap:not(:has(> strong)) { display: block; }
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap > strong {
color: var(--text-3);
font: 600 10.5px/1.55 var(--font);
font: 600 var(--fs-micro)/1.55 var(--font);
letter-spacing: .06em; text-transform: uppercase;
white-space: nowrap;
}
@@ -1452,7 +1637,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
padding: 2px 8px;
border: 1px solid var(--line-2); border-radius: 999px;
color: var(--text-2);
font: 600 10.5px/1.5 var(--mono); letter-spacing: .02em;
font: 600 var(--fs-micro)/1.5 var(--mono); letter-spacing: .02em;
}
.network-status-table .ifacebox-body { display: flex; flex-direction: column; gap: 14px; padding: 8px 16px 16px; }
.network-status-table .ifacebox-body > div:not(:empty) {
@@ -1474,7 +1659,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
white-space: normal;
}
.network-status-table .ifacebox .ifacebadge > img { width: 18px; height: 18px; margin-top: 1px; }
.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child { display: block; margin-bottom: 4px; font: 600 13px/1.4 var(--font); }
.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child { display: block; margin-bottom: 4px; font: 600 var(--fs-base)/1.4 var(--font); }
.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child > strong { display: none; }
/* zone colours arrive inline (--zone-color-rgb + background-color): tint instead */
@@ -1522,7 +1707,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
display: flex; align-items: center;
padding: 0 8px;
color: var(--text);
font: 500 11.5px/1 var(--mono);
font: 500 var(--fs-micro)/1 var(--mono);
font-variant-numeric: tabular-nums;
white-space: nowrap;
z-index: 1;
@@ -1551,7 +1736,6 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
.alert-message.success { border-left-color: var(--ok); background: color-mix(in srgb, var(--ok-soft) 60%, var(--s1)); }
.alert-message.warning { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn-soft) 70%, var(--s1)); }
.alert-message.error, .alert-message.danger { border-left-color: var(--err); background: color-mix(in srgb, var(--err-soft) 70%, var(--s1)); }
.alert-message .btn { min-height: 28px; }
/* spinners */
.spinning { position: relative; }
@@ -1743,7 +1927,7 @@ body.modal-overlay-active #modal_overlay { display: block; }
margin: 0 0 var(--sp-4);
background: var(--s1);
border: 1px solid var(--line);
border-radius: 10px;
border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
overflow: clip;
}
@@ -1755,7 +1939,8 @@ body.modal-overlay-active #modal_overlay { display: block; }
.includes { margin-top: var(--sp-4); }
#view > .cbi-section:first-child { margin-top: 0; }
.cbi-section > div[style*="display: none"] + div { display: block; }
.wifi-qr-code-content, .qr-code { background: #fff; padding: var(--sp-2); border-radius: var(--r); }
/* the QR image gets a white tile; the "Generate QR" button (also .qr-code) stays a button */
.wifi-qr-code-content, div.qr-code { background: #fff; padding: var(--sp-2); border-radius: var(--r); }
iframe { border: 1px solid var(--line); border-radius: var(--r); }
/* ------------------------------------------------------ 7b. stock charts
@@ -1798,7 +1983,7 @@ iframe { border: 1px solid var(--line); border-radius: var(--r); }
#view div[style*="height:300px"][style*="background"] svg text, #view div[id="channel_graph"] svg text {
fill: var(--v-chart-axis) !important;
text-shadow: none !important;
font: 500 10.5px/1 var(--v-mono) !important;
font: 500 var(--fs-micro)/1 var(--v-mono) !important;
font-variant-numeric: tabular-nums;
letter-spacing: .02em;
paint-order: stroke; /* halo: labels stay legible over areas */
@@ -1821,7 +2006,7 @@ body[data-page^="admin-status-realtime"] #view svg polyline:is(#load05, #tx, #tc
/* "(4 minute window, 3 second interval)" */
body[data-page^="admin-status-realtime"] #view .cbi-section > br { display: none; }
body[data-page^="admin-status-realtime"] #view .cbi-section > div.right:has(> small[id^="scale"]) { margin: 6px 0 0; }
body[data-page^="admin-status-realtime"] #view small[id^="scale"] { color: var(--v-chart-axis); font: 10.5px/1.4 var(--v-mono); letter-spacing: .02em; }
body[data-page^="admin-status-realtime"] #view small[id^="scale"] { color: var(--v-chart-axis); font: var(--fs-micro)/1.4 var(--v-mono); letter-spacing: .02em; }
body[data-page^="admin-status-realtime"] #view .cbi-section > .table ~ div[style*="height:300px"] { margin-top: var(--sp-6); }
body[data-page^="admin-status-realtime"] #view .cbi-section-create:empty { display: none; }
@@ -1868,11 +2053,11 @@ body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-
}
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6)) {
font-family: var(--v-mono); font-variant-numeric: tabular-nums;
color: var(--v-text-3); font-size: 11.5px;
color: var(--v-text-3); font-size: var(--fs-micro);
white-space: nowrap;
}
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6))::first-line { color: var(--v-text-2); font-size: 12.5px; }
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:nth-child(2)::first-line { color: var(--v-text); font-weight: 650; font-size: 14px; }
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6))::first-line { color: var(--v-text-2); font-size: var(--fs-sm); }
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:nth-child(2)::first-line { color: var(--v-text); font-weight: 650; font-size: var(--fs-md); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load01_cur, #rx_bw_cur, #rssi_bw_cur, #lb_udp_cur)) { --key: var(--v-chart-1); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load05_cur, #tx_bw_cur, #rate_bw_cur, #lb_tcp_cur)) { --key: var(--v-chart-2); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load15_cur, #noise_bw_cur, #lb_otr_cur)) { --key: var(--v-chart-3); }
@@ -1887,22 +2072,24 @@ body[data-page^="admin-status-realtime"] #view #settings-row input { max-width:
#connections .tr > .th:first-child { padding-left: var(--card-px); }
#connections .tr:not(.table-titles) > .td:is(:nth-child(1), :nth-child(2)) {
color: var(--v-text-2);
font: 600 10.5px/1.5 var(--v-mono); letter-spacing: .05em;
font: 600 var(--fs-micro)/1.5 var(--v-mono); letter-spacing: .05em;
}
#connections .tr:not(.table-titles) > .td:is(:nth-child(3), :nth-child(4)) { font-family: var(--v-mono); font-size: 12px; overflow-wrap: break-word; }
#connections .tr:not(.table-titles) > .td:is(:nth-child(3), :nth-child(4)) { font-family: var(--v-mono); font-size: var(--fs-sm); overflow-wrap: break-word; }
#connections .tr > .th:is(:nth-child(1), :nth-child(2)) { width: 9%; }
#connections .tr > .th:is(:nth-child(3), :nth-child(4)) { width: 31%; }
#connections .tr > .th:nth-child(5) { width: 20%; }
#connections .tr:not(.table-titles) > .td:last-child { font: 12px var(--v-mono); font-variant-numeric: tabular-nums; color: var(--v-text-2); }
#connections .tr:not(.table-titles) > .td:last-child { font: var(--fs-sm) var(--v-mono); font-variant-numeric: tabular-nums; color: var(--v-text-2); }
/* channel analysis: each tab is one card (graph over table) */
body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section > h2 { margin: 0; }
/* the buttons keep the right edge when the title repeats the breadcrumb and is hidden */
body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section > .cbi-title-buttons { margin-left: auto; }
body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not(.cbi-section) {
padding: var(--card-py) var(--card-px) 0;
background: var(--s1);
border: 1px solid var(--line);
border-radius: 10px;
border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
overflow: clip;
}
@@ -1938,7 +2125,7 @@ body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not([dat
.v-ca-shape[data-v-own] { stroke-width: 2.25px; }
#view div[id="channel_graph"] svg text.v-ca-label {
fill: var(--v-text-2) !important;
font: 500 11px/1 var(--v-font) !important;
font: 500 var(--fs-micro)/1 var(--v-font) !important;
letter-spacing: 0;
paint-order: stroke;
stroke: var(--s1); stroke-width: 3px; stroke-linejoin: round;
@@ -2068,7 +2255,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
padding: 10px 12px;
}
.cbi-section-table .ifacebox-body > br { display: none; }
.cbi-section-table .ifacebox-body > small { flex-basis: 100%; color: var(--text-2); font: 12px/1.3 var(--mono); }
.cbi-section-table .ifacebox-body > small { flex-basis: 100%; color: var(--text-2); font: var(--fs-sm)/1.3 var(--mono); }
/* "( dev dev dev )": drop the parentheses, group the member ports */
.cbi-section-table .ifacebox-body > span:not(.cbi-tooltip-container) {
display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px;
@@ -2087,11 +2274,11 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
[id$="-ifc-description"] > .nowrap {
display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 0 1 auto;
white-space: normal; overflow-wrap: anywhere;
font: 12.5px/1.45 var(--mono); font-variant-numeric: tabular-nums slashed-zero;
font: var(--fs-sm)/1.45 var(--mono); font-variant-numeric: tabular-nums slashed-zero;
}
[id$="-ifc-description"] > .nowrap > strong, .td[data-name="_stat"] .nowrap > strong {
color: var(--text-3);
font: 600 11.5px/1.5 var(--font); letter-spacing: .06em; text-transform: uppercase;
font: 600 var(--fs-micro)/1.5 var(--font); letter-spacing: .06em; text-transform: uppercase;
}
/* Network -> Wireless: radios as group headers, networks as rows */
@@ -2121,7 +2308,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.cbi-progressbar::before {
inset: auto 0 calc(100% + 5px) 0; padding: 0;
color: var(--text-2);
font: 500 11.5px/1.2 var(--mono);
font: 500 var(--fs-micro)/1.2 var(--mono);
}
/* fused memory / storage meters (built by menu-vantage.js) */
@@ -2130,9 +2317,9 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.v-meter + .v-meter { margin-top: var(--sp-3); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.v-meter-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.v-meter-title { color: var(--text); font-weight: 600; }
.v-meter-big { font: 650 22px/1 var(--mono); letter-spacing: -.02em; color: var(--text); }
.v-meter-big { font: 650 var(--fs-xl)/1 var(--mono); letter-spacing: -.02em; color: var(--text); }
.v-meter-big small { margin-left: 6px; font: 500 var(--fs-sm) var(--font); letter-spacing: 0; color: var(--text-2); }
.v-meter-sub { margin-left: auto; color: var(--text-2); font: 12px/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-sub { margin-left: auto; color: var(--text-2); font: var(--fs-sm)/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-bar {
position: relative;
display: flex;
@@ -2155,16 +2342,16 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.v-seg.is-crit { background: var(--err); }
.v-meter-legend { display: flex; flex-wrap: wrap; gap: 6px 20px; margin: 0; padding: 0; list-style: none; font-size: var(--fs-sm); color: var(--text-2); }
.v-meter-legend li { display: inline-flex; align-items: center; gap: 7px; }
.v-meter-legend b { color: var(--text); font: 600 12px var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-legend b { color: var(--text); font: 600 var(--fs-sm) var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-legend em { color: var(--text-3); font-style: normal; font-size: var(--fs-micro); }
.v-key { width: 10px; height: 10px; border-radius: 2px; flex: none; }
.v-key.v-seg-free { background: var(--s3); box-shadow: inset 0 0 0 1px var(--line-2); }
.v-meter-rows { display: grid; grid-template-columns: minmax(120px, max-content) minmax(0, 1fr) max-content; gap: 14px 20px; padding: 4px 0 6px; }
.v-meter-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; grid-template-areas: "label bar val"; align-items: center; }
.v-meter-row > .v-meter-label { grid-area: label; color: var(--text-2); overflow-wrap: anywhere; }
.v-meter-row > .v-meter-label small { display: block; color: var(--text-3); font: 11px var(--mono); }
.v-meter-row > .v-meter-label small { display: block; color: var(--text-3); font: var(--fs-micro) var(--mono); }
.v-meter-row > .v-meter-bar { grid-area: bar; }
.v-meter-row > .v-meter-val { grid-area: val; color: var(--text-2); font: 12px var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.v-meter-row > .v-meter-val { grid-area: val; color: var(--text-2); font: var(--fs-sm) var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.v-meter-row > .v-meter-val b { color: var(--text); font-weight: 600; }
.v-sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -2230,7 +2417,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label {
display: block !important;
max-width: 100%;
font-size: 10.5px; font-weight: 500;
font-size: var(--fs-micro); font-weight: 500;
text-align: center;
}
.vt-rail-item.is-active > .vt-rail-link::before { left: 14px; right: 14px; top: -6px; bottom: auto; width: auto; height: 2px; border-radius: 0 0 2px 2px; }
@@ -2286,7 +2473,31 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.cbi-value { grid-template-columns: minmax(0, 1fr); row-gap: 6px; }
.cbi-value > .cbi-value-title { padding-top: 0; }
.cbi-value > .cbi-value-field { grid-column: 1; }
.vt-main input, .vt-main select, .vt-main textarea, .cbi-dropdown, .modal input, .modal select { font-size: 16px !important; }
.cbi-value[data-v-help] > .cbi-value-field { grid-row: 2; }
.cbi-value:has(> .vt-help:not([hidden])) > .cbi-value-title { align-self: center; }
/* stacked, there is nothing beside the title to line up with */
.cbi-value > .cbi-value-title > br:first-child { display: none; }
.cbi-value:has(> .cbi-value-title > br:first-child) > .vt-help { margin-top: 0; }
/* checkbox rows: box, label and (i) on one line, help below. The field
box dissolves so its children join the row grid; LuCI's nodes stay put. */
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) {
grid-template-columns: auto minmax(0, 1fr) auto;
column-gap: var(--sp-3);
align-items: center;
}
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field { display: contents; }
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-checkbox { grid-row: 1; grid-column: 1; }
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-title { grid-row: 1; grid-column: 2; padding: 0; }
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .vt-help { grid-row: 1; grid-column: 3; }
.cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > :not(.cbi-checkbox) { grid-column: 1 / -1; }
/* a segmented control wraps its segments instead of hiding them */
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) { flex-wrap: wrap; width: auto; }
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li { flex: 1 1 auto; }
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li > a { justify-content: center; }
/* ... while an underline strip scrolls, like the page tabs */
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) { flex-wrap: nowrap; }
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li { flex: none; }
html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { justify-content: normal; }
.cbi-dropdown, .vt-main input, .vt-main select, .control-group, .cbi-dynlist { max-width: none; }
/* tables become cards */
@@ -2345,6 +2556,16 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.vt-pal-foot > span:not(.vt-pal-count) { display: none; }
}
/* touch, at any width: 16px text in fields (iOS zooms below that),
bigger check marks, a little more air between rows */
@media (pointer: coarse), (max-width: 720px) {
.vt-main input, .vt-main select, .vt-main textarea, .cbi-dropdown:not(.btn, .cbi-button), .modal input, .modal select { font-size: 16px !important; }
}
@media (pointer: coarse) {
input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
.cbi-value { padding: 8px 0; }
}
/* ----------------------------------------------------------- motion */
/* phones: content in cards wraps instead of pushing the card wider */
@@ -1,13 +1,13 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:918
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:951
msgid "Actions"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
msgid "Appearance"
msgstr ""
@@ -15,7 +15,7 @@ msgstr ""
msgid "Authorization Required"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Auto"
msgstr ""
@@ -23,52 +23,52 @@ msgstr ""
msgid "Breadcrumb"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Buffered"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
msgid "Cache"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Cached"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:344
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:350
msgid "Channel"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719
msgid "Collapse navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1093
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1126
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81
msgid "Colour theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:934
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:967
msgid "Command palette"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Dark"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54
msgid "Expand navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
msgid "Follow system theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:295
msgid "Free"
msgstr ""
@@ -76,9 +76,9 @@ msgstr ""
msgid "Home"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:283
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293
msgid "In use"
msgstr ""
@@ -86,11 +86,11 @@ msgstr ""
msgid "Invalid username and/or password! Please try again."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963
msgid "Jump to a page or action…"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Light"
msgstr ""
@@ -99,7 +99,7 @@ msgstr ""
msgid "Log in"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
msgid "Log out"
msgstr ""
@@ -108,12 +108,12 @@ msgstr ""
msgid "Main navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:273
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:264
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
msgid "Memory"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1005
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1038
msgid "No matching pages"
msgstr ""
@@ -125,57 +125,69 @@ msgstr ""
msgid "Password"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825
msgid "Press"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:932
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:965
msgid "Results"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74
msgid "Search pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
msgid "Session"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1283
msgid "Show full description"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272
msgid "Show full help"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272
msgid "Show full help: %s"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:44
msgid "Skip to content"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:310
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:316
msgid "Storage"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:296
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:304
msgid "Swap"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Swap free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
msgid "Total"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268
msgid "Total Available"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
msgid "Use dark theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
msgid "Use light theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268
msgid "Used"
msgstr ""
@@ -183,54 +195,54 @@ msgstr ""
msgid "Username"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:337
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:343
msgid "Wireless"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:285
msgid "available"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:940
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:973
msgid "close"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
msgid "free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1007
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1040
msgid "items"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:938
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:971
msgid "navigate"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:939
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:972
msgid "open"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805
msgid "page"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805
msgid "pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
msgid "reclaimable"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825
msgid "to search all pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
msgid "used"
msgstr ""