mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 07:03:56 +08:00
🎉 Sync 2026-10-13 02:34:41
This commit is contained in:
1 parent
b537a62e59
commit
72c44a6325
38 files changed
+3510
-1734
No files matched your search
@@ -1 +1 @@
|
|||||||
snapshot
|
both
|
||||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.10.4
|
PKG_VERSION:=26.10.4
|
||||||
PKG_RELEASE:=331
|
PKG_RELEASE:=332
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ sources.validate = function(self, value, t)
|
|||||||
flag = true
|
flag = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if flag == false and datatypes.macaddr(v) then
|
if flag == false and api.datatypes.macaddr(v) then
|
||||||
flag = true
|
flag = true
|
||||||
end
|
end
|
||||||
if flag == false and datatypes.ip4addr(v) then
|
if flag == false and api.datatypes.ip4addr(v) then
|
||||||
flag = true
|
flag = true
|
||||||
end
|
end
|
||||||
if flag == false and api.iprange(v) then
|
if flag == false and api.iprange(v) then
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ datatypes = api.datatypes
|
|||||||
local fs = api.fs
|
local fs = api.fs
|
||||||
has_singbox = api.finded_com("sing-box")
|
has_singbox = api.finded_com("sing-box")
|
||||||
has_xray = api.finded_com("xray")
|
has_xray = api.finded_com("xray")
|
||||||
|
local has_smartdns = api.is_finded("smartdns")
|
||||||
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
|
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
|
||||||
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
|
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
|
||||||
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
|
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
|
||||||
@@ -180,7 +181,7 @@ o.write = function(self, section, value)
|
|||||||
end
|
end
|
||||||
return ListValue.write(self, section, value)
|
return ListValue.write(self, section, value)
|
||||||
end
|
end
|
||||||
if api.is_finded("smartdns") then
|
if has_smartdns then
|
||||||
o:value("smartdns", "SmartDNS")
|
o:value("smartdns", "SmartDNS")
|
||||||
|
|
||||||
o = s:taboption("DNS", Value, "group_domestic", translate("Domestic group name"))
|
o = s:taboption("DNS", Value, "group_domestic", translate("Domestic group name"))
|
||||||
@@ -284,7 +285,7 @@ o.remove = function(self, section)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---- SmartDNS Forward Mode
|
---- SmartDNS Forward Mode
|
||||||
if api.is_finded("smartdns") then
|
if has_smartdns then
|
||||||
o = s:taboption("DNS", ListValue, "smartdns_dns_mode", translate("Filter Mode"))
|
o = s:taboption("DNS", ListValue, "smartdns_dns_mode", translate("Filter Mode"))
|
||||||
o:value("socks", "Socks")
|
o:value("socks", "Socks")
|
||||||
if has_singbox then
|
if has_singbox then
|
||||||
@@ -487,14 +488,14 @@ o:value("remote", translate("Remote DNS"))
|
|||||||
o:value("direct", translate("Direct DNS"))
|
o:value("direct", translate("Direct DNS"))
|
||||||
o.description = desc .. "</ul>"
|
o.description = desc .. "</ul>"
|
||||||
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||||
if api.is_finded("smartdns") then
|
if has_smartdns then
|
||||||
o:depends({dns_shunt = "smartdns", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
o:depends({dns_shunt = "smartdns", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||||
end
|
end
|
||||||
|
|
||||||
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
||||||
o.default = "0"
|
o.default = "0"
|
||||||
o:depends({dns_shunt = "chinadns-ng"})
|
o:depends({dns_shunt = "chinadns-ng"})
|
||||||
if api.is_finded("smartdns") then
|
if has_smartdns then
|
||||||
o:depends({dns_shunt = "smartdns"})
|
o:depends({dns_shunt = "smartdns"})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -871,19 +871,26 @@ function clone(org)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function get_bin_version_cache(file, cmd)
|
function get_bin_version_cache(file, cmd)
|
||||||
sys.call("mkdir -p " .. CACHE_PATH)
|
if file and file ~= "" and fs.access(file) then
|
||||||
if fs.access(file) then
|
|
||||||
chmod_755(file)
|
chmod_755(file)
|
||||||
local md5 = md5_file(file)
|
local stat = fs.stat(file)
|
||||||
if fs.access(CACHE_PATH .. "/" .. md5) then
|
local cache_file
|
||||||
return sys.exec("echo -n $(cat %s)" % { CACHE_PATH .. "/" .. md5 })
|
if stat and stat.dev and stat.ino and stat.size and stat.mtime and stat.ctime then
|
||||||
else
|
-- Use metadata to avoid hashing the entire binary on every lookup.
|
||||||
local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd))
|
cache_file = CACHE_PATH .. "/binver_" .. table.concat({stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime}, "_")
|
||||||
if version and version ~= "" then
|
local cached = fs.readfile(cache_file)
|
||||||
sys.call("echo '%s' > %s" % { version, CACHE_PATH .. "/" .. md5})
|
if cached and trim(cached) ~= "" then
|
||||||
return version
|
return trim(cached)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
local version = sys.exec(string.format("echo -n $(%s %s)", util.shellquote(file), cmd))
|
||||||
|
if version and version ~= "" then
|
||||||
|
if cache_file then
|
||||||
|
fs.mkdirr(CACHE_PATH)
|
||||||
|
fs.writefile(cache_file, version)
|
||||||
|
end
|
||||||
|
return version
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@@ -904,7 +911,7 @@ end
|
|||||||
|
|
||||||
local function is_file(path)
|
local function is_file(path)
|
||||||
if path and #path > 1 then
|
if path and #path > 1 then
|
||||||
if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then
|
if fs.stat(path, "type") == "reg" then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -913,7 +920,7 @@ end
|
|||||||
|
|
||||||
local function is_dir(path)
|
local function is_dir(path)
|
||||||
if path and #path > 1 then
|
if path and #path > 1 then
|
||||||
if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then
|
if fs.stat(path, "type") == "dir" then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1554,7 +1561,7 @@ function to_install_self(id, force)
|
|||||||
remove(dir)
|
remove(dir)
|
||||||
return {code = 1}
|
return {code = 1}
|
||||||
end
|
end
|
||||||
fs.chmod(script, 755)
|
chmod_755(script)
|
||||||
fs.writefile(status, '{"installing":true}')
|
fs.writefile(status, '{"installing":true}')
|
||||||
sys.call("nohup " .. script .. " " .. dir .. " " .. status .. " " .. manager .. " " ..
|
sys.call("nohup " .. script .. " " .. dir .. " " .. status .. " " .. manager .. " " ..
|
||||||
(force == "1" and "1" or "0") .. " >/dev/null 2>&1 </dev/null &")
|
(force == "1" and "1" or "0") .. " >/dev/null 2>&1 </dev/null &")
|
||||||
@@ -1562,7 +1569,7 @@ function to_install_self(id, force)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function is_js_luci()
|
function is_js_luci()
|
||||||
return sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
|
return fs.stat("/www/luci-static/resources/uci.js", "type") == "reg"
|
||||||
end
|
end
|
||||||
|
|
||||||
function set_default_cbi()
|
function set_default_cbi()
|
||||||
@@ -1614,6 +1621,16 @@ function set_default_cbi()
|
|||||||
return obj
|
return obj
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if true then
|
||||||
|
--Table
|
||||||
|
local Table = cbi.Table
|
||||||
|
local default_init = Table.__init__
|
||||||
|
function Table.__init__(self, form, ...)
|
||||||
|
default_init(self, form, ...)
|
||||||
|
-- Table 的 map 是独立数据源,需要从所属表单传递 API
|
||||||
|
self.map.api = form.api
|
||||||
|
end
|
||||||
|
end
|
||||||
if true then
|
if true then
|
||||||
--TextValue
|
--TextValue
|
||||||
local TextValue = cbi.TextValue
|
local TextValue = cbi.TextValue
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
-- Description: Passwall(2) UI template
|
-- Description: Passwall(2) UI template
|
||||||
|
|
||||||
local json = require "luci.jsonc"
|
local json = require "luci.jsonc"
|
||||||
|
local api = self.map.api
|
||||||
|
|
||||||
-- 读取值(keylist/vallist/group)
|
-- 读取值(keylist/vallist/group)
|
||||||
local values = {}
|
local values = {}
|
||||||
@@ -48,6 +49,30 @@ for _, item in ipairs(values) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 仅共享组名、节点 key/label 及顺序完全相同的分组,保留各控件的候选限制
|
||||||
|
local group_cache = self.map._lv_group_cache
|
||||||
|
if not group_cache then
|
||||||
|
group_cache = {}
|
||||||
|
self.map._lv_group_cache = group_cache
|
||||||
|
end
|
||||||
|
local group_refs = {}
|
||||||
|
local new_groups = {}
|
||||||
|
for i, g in ipairs(group_order) do
|
||||||
|
-- 使用有序数组生成完整签名,不依赖 JSON 对象的字段顺序或哈希值
|
||||||
|
local signature = {g}
|
||||||
|
for _, item in ipairs(groups[g]) do
|
||||||
|
signature[#signature + 1] = {item.key, item.label}
|
||||||
|
end
|
||||||
|
local cache_key = json.stringify(signature)
|
||||||
|
local ref = group_cache[cache_key]
|
||||||
|
if not ref then
|
||||||
|
ref = cbid .. ".group." .. i
|
||||||
|
group_cache[cache_key] = ref
|
||||||
|
new_groups[ref] = groups[g]
|
||||||
|
end
|
||||||
|
group_refs[i] = ref
|
||||||
|
end
|
||||||
|
|
||||||
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
||||||
if not current_key then
|
if not current_key then
|
||||||
if #ungrouped > 0 then
|
if #ungrouped > 0 then
|
||||||
@@ -73,19 +98,28 @@ local function find_label_by_key(k)
|
|||||||
end
|
end
|
||||||
local current_label = find_label_by_key(current_key) or ""
|
local current_label = find_label_by_key(current_key) or ""
|
||||||
|
|
||||||
-- 打包数据给 JS
|
-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
|
||||||
local dropdown_data = {
|
local dropdown_data = {
|
||||||
|
appname = api.appname,
|
||||||
current_key = current_key,
|
current_key = current_key,
|
||||||
current_label = current_label,
|
current_label = current_label,
|
||||||
ungrouped = ungrouped,
|
ungrouped = ungrouped,
|
||||||
groups = groups,
|
group_refs = group_refs,
|
||||||
group_order = group_order,
|
group_order = group_order,
|
||||||
cbid = cbid,
|
cbid = cbid,
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<script>
|
<script>
|
||||||
window.lv_dropdown_data = window.lv_dropdown_data || {};
|
(function() {
|
||||||
window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
const appname = <%=json.stringify(api.appname)%>;
|
||||||
|
window.lv_groups_pool = window.lv_groups_pool || Object.create(null);
|
||||||
|
const pool = window.lv_groups_pool[appname] || (window.lv_groups_pool[appname] = Object.create(null));
|
||||||
|
<% if next(new_groups) then %>
|
||||||
|
Object.assign(pool, <%=json.stringify(new_groups)%>);
|
||||||
|
<% end %>
|
||||||
|
window.lv_dropdown_data = window.lv_dropdown_data || {};
|
||||||
|
window.lv_dropdown_data[<%=json.stringify(cbid)%>] = <%=json.stringify(dropdown_data)%>;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="<%=cbid%>.main" class="lv-dropdown-container">
|
<div id="<%=cbid%>.main" class="lv-dropdown-container">
|
||||||
|
|||||||
@@ -577,8 +577,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分组项
|
// 分组项
|
||||||
data.group_order.forEach(gname => {
|
const groupPool = window.lv_groups_pool && window.lv_groups_pool[data.appname];
|
||||||
const items = data.groups[gname];
|
data.group_order.forEach((gname, index) => {
|
||||||
|
const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]];
|
||||||
|
if (!Array.isArray(items)) return;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="lv-group" data-group="${lv_idSafe(gname)}" style="margin-bottom:8px;">
|
<div class="lv-group" data-group="${lv_idSafe(gname)}" style="margin-bottom:8px;">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
-- Description: Passwall(2) UI template
|
-- Description: Passwall(2) UI template
|
||||||
|
|
||||||
local json = require "luci.jsonc"
|
local json = require "luci.jsonc"
|
||||||
|
local api = self.map.api
|
||||||
|
|
||||||
-- 读取值(keylist/vallist/group)
|
-- 读取值(keylist/vallist/group)
|
||||||
local values = {{key = "", label = translate("-- Please choose --")}}
|
local values = {{key = "", label = translate("-- Please choose --")}}
|
||||||
@@ -56,6 +57,30 @@ if not current_key_added and current_key and current_key ~= "" then
|
|||||||
table.insert(ungrouped, {key = current_key, label = current_key})
|
table.insert(ungrouped, {key = current_key, label = current_key})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 仅共享候选分组,各控件的自定义地址仍保留在自己的 ungrouped 中
|
||||||
|
local group_cache = self.map._v_group_cache
|
||||||
|
if not group_cache then
|
||||||
|
group_cache = {}
|
||||||
|
self.map._v_group_cache = group_cache
|
||||||
|
end
|
||||||
|
local group_refs = {}
|
||||||
|
local new_groups = {}
|
||||||
|
for i, g in ipairs(group_order) do
|
||||||
|
-- 组名、节点 key/label 及顺序完全相同才复用,不与 ListValue 缓存混用
|
||||||
|
local signature = {g}
|
||||||
|
for _, item in ipairs(groups[g]) do
|
||||||
|
signature[#signature + 1] = {item.key, item.label}
|
||||||
|
end
|
||||||
|
local cache_key = json.stringify(signature)
|
||||||
|
local ref = group_cache[cache_key]
|
||||||
|
if not ref then
|
||||||
|
ref = cbid .. ".v-group." .. i
|
||||||
|
group_cache[cache_key] = ref
|
||||||
|
new_groups[ref] = groups[g]
|
||||||
|
end
|
||||||
|
group_refs[i] = ref
|
||||||
|
end
|
||||||
|
|
||||||
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
||||||
if not current_key then
|
if not current_key then
|
||||||
if #ungrouped > 0 then
|
if #ungrouped > 0 then
|
||||||
@@ -81,19 +106,28 @@ local function find_label_by_key(k)
|
|||||||
end
|
end
|
||||||
local current_label = find_label_by_key(current_key) or ""
|
local current_label = find_label_by_key(current_key) or ""
|
||||||
|
|
||||||
-- 打包数据给 JS
|
-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
|
||||||
local dropdown_data = {
|
local dropdown_data = {
|
||||||
|
appname = api.appname,
|
||||||
current_key = current_key,
|
current_key = current_key,
|
||||||
current_label = current_label,
|
current_label = current_label,
|
||||||
ungrouped = ungrouped,
|
ungrouped = ungrouped,
|
||||||
groups = groups,
|
group_refs = group_refs,
|
||||||
group_order = group_order,
|
group_order = group_order,
|
||||||
cbid = cbid,
|
cbid = cbid,
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<script>
|
<script>
|
||||||
window.v_dropdown_data = window.v_dropdown_data || {};
|
(function() {
|
||||||
window.v_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
const appname = <%=json.stringify(api.appname)%>;
|
||||||
|
window.v_groups_pool = window.v_groups_pool || Object.create(null);
|
||||||
|
const pool = window.v_groups_pool[appname] || (window.v_groups_pool[appname] = Object.create(null));
|
||||||
|
<% if next(new_groups) then %>
|
||||||
|
Object.assign(pool, <%=json.stringify(new_groups)%>);
|
||||||
|
<% end %>
|
||||||
|
window.v_dropdown_data = window.v_dropdown_data || {};
|
||||||
|
window.v_dropdown_data[<%=json.stringify(cbid)%>] = <%=json.stringify(dropdown_data)%>;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="<%=cbid%>.v-main" class="v-dropdown-container">
|
<div id="<%=cbid%>.v-main" class="v-dropdown-container">
|
||||||
|
|||||||
@@ -603,8 +603,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分组项
|
// 分组项
|
||||||
data.group_order.forEach(gname => {
|
const groupPool = window.v_groups_pool && window.v_groups_pool[data.appname];
|
||||||
const items = data.groups[gname];
|
data.group_order.forEach((gname, index) => {
|
||||||
|
const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]];
|
||||||
|
if (!Array.isArray(items)) return;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="v-group" data-group="${v_idSafe(gname)}" style="margin-bottom:8px;">
|
<div class="v-group" data-group="${v_idSafe(gname)}" style="margin-bottom:8px;">
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4 check" onclick="check_connect('aliyun', 'https://www.aliyun.com')" title="<%:HTTP request latency, for reference only%>">
|
<div class="pure-u-1-4 check" onclick="check_connect('aliyun', 'https://www.aliyun.com/favicon.ico')" title="<%:HTTP request latency, for reference only%>">
|
||||||
<div class="block pure-g">
|
<div class="block pure-g">
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<div class="img-con">
|
<div class="img-con">
|
||||||
@@ -163,7 +163,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com')" title="<%:HTTP request latency, for reference only%>">
|
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com/favicon.ico')" title="<%:HTTP request latency, for reference only%>">
|
||||||
<div class="block pure-g">
|
<div class="block pure-g">
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<div class="img-con">
|
<div class="img-con">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.10.1
|
PKG_VERSION:=26.10.1
|
||||||
PKG_RELEASE:=140
|
PKG_RELEASE:=141
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ local doh_validate = function(self, value, t)
|
|||||||
for i = 1, #val do
|
for i = 1, #val do
|
||||||
local v = val[i]
|
local v = val[i]
|
||||||
if v then
|
if v then
|
||||||
if not datatypes.ipmask4(v) then
|
if not api.datatypes.ipmask4(v) then
|
||||||
flag = 1
|
flag = 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -113,11 +113,11 @@ sources.validate = function(self, value, t)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if flag == false and datatypes.macaddr(v) then
|
if flag == false and api.datatypes.macaddr(v) then
|
||||||
flag = true
|
flag = true
|
||||||
end
|
end
|
||||||
|
|
||||||
if flag == false and datatypes.ip4addr(v) then
|
if flag == false and api.datatypes.ip4addr(v) then
|
||||||
flag = true
|
flag = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-trafficctl
|
PKG_NAME:=luci-app-trafficctl
|
||||||
PKG_VERSION:=1.21.5
|
PKG_VERSION:=1.21.6
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
|
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
|
||||||
|
|||||||
@@ -4212,6 +4212,24 @@ return view.extend({
|
|||||||
_('Offloads routing to the hardware engine (PPE/NPU). Requires software offload. ' +
|
_('Offloads routing to the hardware engine (PPE/NPU). Requires software offload. ' +
|
||||||
'⚠ On many platforms (e.g. Mediatek Filogic) the driver does not report byte counters back to the kernel — real-time speed monitoring will show zero.'));
|
'⚠ On many platforms (e.g. Mediatek Filogic) the driver does not report byte counters back to the kernel — real-time speed monitoring will show zero.'));
|
||||||
|
|
||||||
|
// Qualcomm NSS acceleration (#81). This is NOT one of the
|
||||||
|
// flow-offload modes above and is not controlled by the two
|
||||||
|
// toggles below it: ECM pushes flow rules into the NSS
|
||||||
|
// firmware cores, so an accelerated flow never reaches the
|
||||||
|
// Linux stack at all and neither the nftables policer nor
|
||||||
|
// Linux HTB can act on it. The apply result says so on every
|
||||||
|
// attempt; this is here because the Flow Offload panel is
|
||||||
|
// where somebody asking "why is my limit doing nothing" looks.
|
||||||
|
if (cfg.nss === 'active') {
|
||||||
|
container.appendChild(E('div', {
|
||||||
|
'style': 'margin-bottom:10px;padding:8px;border-left:3px solid var(--tc-warn);font-size:12px'
|
||||||
|
}, [
|
||||||
|
E('b', {'style':'color:var(--tc-warn)'}, '⚠ ' + _('Qualcomm NSS acceleration is active')),
|
||||||
|
E('div', {'style':'margin-top:4px;color:var(--tc-muted)'},
|
||||||
|
_('Rate limits and traffic shaping cannot be enforced while it is: accelerated flows bypass the Linux network stack, so rules are installed but have no effect. The toggles below do not control this — stop the ECM front end (front_end_ipv4_stop / front_end_ipv6_stop under /sys/kernel/debug/ecm) or set acceleration_engine in /etc/config/ecm, then re-apply the limit.'))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
container.appendChild(modeBadge);
|
container.appendChild(modeBadge);
|
||||||
container.appendChild(E('div', {'class':'tc-offload-row'}, [swToggleEl, saveStatus]));
|
container.appendChild(E('div', {'class':'tc-offload-row'}, [swToggleEl, saveStatus]));
|
||||||
container.appendChild(swDesc);
|
container.appendChild(swDesc);
|
||||||
|
|||||||
@@ -302,10 +302,11 @@ call)
|
|||||||
hw=$(uci -q get firewall.@defaults[0].flow_offloading_hw)
|
hw=$(uci -q get firewall.@defaults[0].flow_offloading_hw)
|
||||||
poll=$(uci -q get trafficctl.main.poll_interval)
|
poll=$(uci -q get trafficctl.main.poll_interval)
|
||||||
window=$(uci -q get trafficctl.main.avg_window)
|
window=$(uci -q get trafficctl.main.avg_window)
|
||||||
printf '{"enabled":%s,"default_mode":"%s","offload_mode":"%s","sw":%s,"hw":%s,"poll_interval":%d,"avg_window":%d}' \
|
printf '{"enabled":%s,"default_mode":"%s","offload_mode":"%s","nss":"%s","sw":%s,"hw":%s,"poll_interval":%d,"avg_window":%d}' \
|
||||||
"$([ "$enabled" = "1" ] && echo true || echo false)" \
|
"$([ "$enabled" = "1" ] && echo true || echo false)" \
|
||||||
"${mode:-limiter}" \
|
"${mode:-limiter}" \
|
||||||
"$offload" \
|
"$offload" \
|
||||||
|
"$(tctl_nss_status)" \
|
||||||
"$([ "$sw" = "1" ] && echo true || echo false)" \
|
"$([ "$sw" = "1" ] && echo true || echo false)" \
|
||||||
"$([ "$hw" = "1" ] && echo true || echo false)" \
|
"$([ "$hw" = "1" ] && echo true || echo false)" \
|
||||||
"${poll:-2}" \
|
"${poll:-2}" \
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ if tctl_block_add "$IP" "$COMMENT"; then
|
|||||||
# flow offload those are never re-evaluated against the new rule — the
|
# flow offload those are never re-evaluated against the new rule — the
|
||||||
# device would stay online over v6 indefinitely while the UI reads blocked.
|
# device would stay online over v6 indefinitely while the UI reads blocked.
|
||||||
MAC=$(tctl_target_mac "$IP" 2>/dev/null) && tctl_conntrack_flush_v6 "$MAC"
|
MAC=$(tctl_target_mac "$IP" 2>/dev/null) && tctl_conntrack_flush_v6 "$MAC"
|
||||||
|
# Same problem one layer further out: a flow already accelerated by the
|
||||||
|
# NSS firmware is not re-evaluated against this rule either.
|
||||||
|
tctl_nss_defunct
|
||||||
|
|
||||||
tctl_persist_enabled && tctl_persist_save "block" "$IP" "$LABEL"
|
tctl_persist_enabled && tctl_persist_save "block" "$IP" "$LABEL"
|
||||||
tctl_log "block" "$IP" "$LABEL" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
tctl_log "block" "$IP" "$LABEL" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||||
@@ -58,10 +61,12 @@ if tctl_block_add "$IP" "$COMMENT"; then
|
|||||||
else
|
else
|
||||||
NOTE=" — IPv4 only: no MAC known for $IP (no DHCP lease, no neighbour entry), so IPv6 is not covered"
|
NOTE=" — IPv4 only: no MAC known for $IP (no DHCP lease, no neighbour entry), so IPv6 is not covered"
|
||||||
fi
|
fi
|
||||||
|
NOTE="$NOTE$(tctl_nss_note)"
|
||||||
|
NSS="$(tctl_nss_status)"
|
||||||
if [ "$SELF_BLOCK" = "true" ]; then
|
if [ "$SELF_BLOCK" = "true" ]; then
|
||||||
echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
|
echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
|
||||||
else
|
else
|
||||||
echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"msg\":\"internet blocked for $IP$NOTE\"}"
|
echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP$NOTE\"}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "{\"ok\":false,\"msg\":\"failed to block $IP\"}"
|
echo "{\"ok\":false,\"msg\":\"failed to block $IP\"}"
|
||||||
|
|||||||
@@ -1149,3 +1149,81 @@ tctl_get_offload_mode() {
|
|||||||
echo "none"
|
echo "none"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Qualcomm NSS / ECM Detection ───────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# On Qualcomm NSS builds (ipq807x/ipq60xx with qca-nss-ecm, qca-nss-drv and
|
||||||
|
# qca-nss-clients) the Enhanced Connection Manager installs flow rules into
|
||||||
|
# the NSS firmware cores, and an accelerated flow NEVER TRAVERSES THE LINUX
|
||||||
|
# NETWORK STACK. Both of this package's enforcement paths are therefore inert
|
||||||
|
# on such a router: the nftables netdev policer never sees the packets, and
|
||||||
|
# Linux HTB/IFB never gets to queue them. Reported in issue #81, where a user
|
||||||
|
# applied WiFi client limits and nothing was limited.
|
||||||
|
#
|
||||||
|
# Nothing here attempts to fix that — an NSS backend would mean nsshtb/nsstbl
|
||||||
|
# and act_nssmirred in place of htb and ifb, which needs hardware to develop
|
||||||
|
# against. What these helpers do is let the callers STOP REPORTING SUCCESS for
|
||||||
|
# a rule that cannot be enforced, which is the part that can be made correct
|
||||||
|
# without hardware and is the actual complaint.
|
||||||
|
#
|
||||||
|
# Named by absolute path, as it must be on the router; the tests sed this line
|
||||||
|
# into their scratch directory (same approach as TCTL_LEASES_FILE above).
|
||||||
|
TCTL_ECM_DEBUGFS="/sys/kernel/debug/ecm"
|
||||||
|
|
||||||
|
# absent — no ECM on this system; every rule this package installs applies
|
||||||
|
# normally and nothing needs qualifying.
|
||||||
|
# stopped — ECM is present but its front end has been stopped, so flows are
|
||||||
|
# back on the Linux path and our rules DO apply.
|
||||||
|
# active — ECM is accelerating. Our rules are installed but not enforced for
|
||||||
|
# any accelerated flow.
|
||||||
|
#
|
||||||
|
# The distinction between "stopped" and "active" is deliberate and is read
|
||||||
|
# from the kernel rather than inferred from the packages installed: somebody
|
||||||
|
# who has turned acceleration off to get working QoS must not be told their
|
||||||
|
# limits do not work.
|
||||||
|
tctl_nss_status() {
|
||||||
|
[ -d "$TCTL_ECM_DEBUGFS" ] || { printf 'absent\n'; return 0; }
|
||||||
|
local v4 v6
|
||||||
|
v4=$(cat "$TCTL_ECM_DEBUGFS/front_end_ipv4_stop" 2>/dev/null)
|
||||||
|
v6=$(cat "$TCTL_ECM_DEBUGFS/front_end_ipv6_stop" 2>/dev/null)
|
||||||
|
# Both stopped is the only state in which the Linux datapath is whole. A
|
||||||
|
# missing knob is NOT read as stopped: an ECM build whose debugfs layout we
|
||||||
|
# do not recognise is still accelerating, and guessing "stopped" there
|
||||||
|
# would restore exactly the false success this exists to remove.
|
||||||
|
if [ "$v4" = "1" ] && [ "$v6" = "1" ]; then
|
||||||
|
printf 'stopped\n'
|
||||||
|
else
|
||||||
|
printf 'active\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tctl_nss_active() { [ "$(tctl_nss_status)" = "active" ]; }
|
||||||
|
|
||||||
|
# The suffix every caller appends to its own message. One wording in one place,
|
||||||
|
# so the limiter, the shaper and the block path cannot drift into describing
|
||||||
|
# the same condition three different ways.
|
||||||
|
tctl_nss_note() {
|
||||||
|
tctl_nss_active || return 0
|
||||||
|
printf ' [NOT ENFORCED: Qualcomm NSS acceleration is active — accelerated flows bypass the Linux network stack entirely, so this rule is installed but has no effect. Stop acceleration (echo 1 > %s/front_end_ipv4_stop, same for ipv6) or set acceleration_engine in /etc/config/ecm, then re-apply]' \
|
||||||
|
"$TCTL_ECM_DEBUGFS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Defunct the flows ECM has already accelerated.
|
||||||
|
#
|
||||||
|
# This is the NSS counterpart of the `conntrack -D` every rule change already
|
||||||
|
# does, and it is needed for the same reason: a flow that is ALREADY offloaded
|
||||||
|
# is not re-evaluated against a rule that appeared afterwards, so without this
|
||||||
|
# a new block or limit would only affect connections started later — which
|
||||||
|
# presents as "it works sometimes".
|
||||||
|
#
|
||||||
|
# Best effort by design: absent on every non-NSS router, and a write failure
|
||||||
|
# must never fail the operation the caller is performing. UNVERIFIED ON
|
||||||
|
# HARDWARE — there is no IPQ device here to test it against, so it is wired in
|
||||||
|
# as an addition to the existing flush rather than as something any result
|
||||||
|
# depends on.
|
||||||
|
tctl_nss_defunct() {
|
||||||
|
local f="$TCTL_ECM_DEBUGFS/ecm_db/defunct_all"
|
||||||
|
[ -w "$f" ] || return 0
|
||||||
|
echo 1 > "$f" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -96,11 +96,21 @@ else
|
|||||||
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE" "$MODE" \
|
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE" "$MODE" \
|
||||||
"$([ "$RATE_UP" != "$RATE" ] && echo "$RATE_UP")"
|
"$([ "$RATE_UP" != "$RATE" ] && echo "$RATE_UP")"
|
||||||
tctl_log "ratelimit_set" "$IP" "$(rate_text)" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
tctl_log "ratelimit_set" "$IP" "$(rate_text)" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||||
|
# An already-accelerated flow is not re-evaluated against a rule that
|
||||||
|
# appeared afterwards, exactly as with flow offload — so the NSS flow
|
||||||
|
# table gets the same treatment the conntrack table does.
|
||||||
|
tctl_nss_defunct
|
||||||
|
# On a Qualcomm NSS router the rules above are installed and inert: the
|
||||||
|
# packets never reach the Linux stack. Reporting an unqualified success
|
||||||
|
# there is the defect in #81, so every message below carries the verdict
|
||||||
|
# and the result carries the status as a field.
|
||||||
|
NSSNOTE="$(tctl_nss_note)"
|
||||||
|
NSS="$(tctl_nss_status)"
|
||||||
if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then
|
if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then
|
||||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE\"}"
|
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}"
|
||||||
elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then
|
elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then
|
||||||
echo "{\"ok\":true,\"ipv6_upload\":false,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]\"}"
|
echo "{\"ok\":true,\"ipv6_upload\":false,\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}"
|
||||||
else
|
else
|
||||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE\"}"
|
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -303,14 +303,21 @@ do_add() {
|
|||||||
save_shape "$IP" "$RATE" "$classid" "$RATE_UP"
|
save_shape "$IP" "$RATE" "$classid" "$RATE_UP"
|
||||||
local rates="${RATE} kbit/s"
|
local rates="${RATE} kbit/s"
|
||||||
[ "$RATE_UP" != "$RATE" ] && rates="${RATE} kbit/s down, ${RATE_UP} kbit/s up"
|
[ "$RATE_UP" != "$RATE" ] && rates="${RATE} kbit/s down, ${RATE_UP} kbit/s up"
|
||||||
|
# Linux HTB never gets to queue a flow the NSS firmware is forwarding, so
|
||||||
|
# a shape on such a router is installed and inert (#81). NSS builds ship
|
||||||
|
# their own qdiscs (nsshtb/nsstbl) which this does not yet use; until it
|
||||||
|
# does, say so instead of reporting success.
|
||||||
|
tctl_nss_defunct
|
||||||
|
NSSNOTE="$(tctl_nss_note)"
|
||||||
|
NSS="$(tctl_nss_status)"
|
||||||
if [ "$ul_ok" = "1" ]; then
|
if [ "$ul_ok" = "1" ]; then
|
||||||
echo "{\"ok\":true,\"msg\":\"shape $rates applied to $IP both directions (class $classid)\"}"
|
echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape $rates applied to $IP both directions (class $classid)$NSSNOTE\"}"
|
||||||
elif [ "$RATE_UP" != "$RATE" ]; then
|
elif [ "$RATE_UP" != "$RATE" ]; then
|
||||||
# Saying "applied" here would be a lie about the half the operator
|
# Saying "applied" here would be a lie about the half the operator
|
||||||
# explicitly asked to differ.
|
# explicitly asked to differ.
|
||||||
echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP for download only — the ${RATE_UP} kbit/s upload ceiling was NOT applied, it needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched\"}"
|
echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape ${RATE} kbit/s applied to $IP for download only — the ${RATE_UP} kbit/s upload ceiling was NOT applied, it needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched$NSSNOTE\"}"
|
||||||
else
|
else
|
||||||
echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP (download only — upload needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched)\"}"
|
echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape ${RATE} kbit/s applied to $IP (download only — upload needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched)$NSSNOTE\"}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ return baseclass.extend({
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps')));
|
cbi_update_table(table, rows, E('em', _('No active IPv4/IPv6 port maps')));
|
||||||
|
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ return view.extend({
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('There are no active IPv4/IPv6 port maps')));
|
cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('No active IPv4/IPv6 port maps')));
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
@@ -137,7 +137,7 @@ return view.extend({
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps')));
|
cbi_update_table(table, rows, E('em', _('No active IPv4/IPv6 port maps')));
|
||||||
|
|
||||||
return E('div', { 'class': 'cbi-section cbi-tblsection' }, [
|
return E('div', { 'class': 'cbi-section cbi-tblsection' }, [
|
||||||
E('h3', _('Active Port Maps')), table
|
E('h3', _('Active Port Maps')), table
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ const methods = {
|
|||||||
if (leasefile) {
|
if (leasefile) {
|
||||||
for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) {
|
for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) {
|
||||||
const record = split(line, ':', 7);
|
const record = split(line, ':', 7);
|
||||||
if (length(record) == 6 || length(record) == 7) {
|
if (length(record) == 7) {
|
||||||
// Daemon version >2.3.11 changes lease file format
|
let descr = trim(record[6]);
|
||||||
let recshift = 0;
|
|
||||||
if (length(record) == 7) recshift = 1;
|
|
||||||
let descr = trim(record[5 + recshift]);
|
|
||||||
let m = match(descr, /^PCP [A-Z]+ ([0-9a-f]{24})$/);
|
let m = match(descr, /^PCP [A-Z]+ ([0-9a-f]{24})$/);
|
||||||
if (m) descr = 'PCP (nonce ' + m[1] + ')';
|
if (m) descr = 'PCP (nonce ' + m[1] + ')';
|
||||||
else if (match(descr, /^NAT-PMP \d+ \w+$/)) descr = 'NAT-PMP';
|
else if (match(descr, /^NAT-PMP \d+ \w+$/)) descr = 'NAT-PMP';
|
||||||
@@ -33,9 +30,9 @@ const methods = {
|
|||||||
push(leases, {
|
push(leases, {
|
||||||
proto: uc(record[0]),
|
proto: uc(record[0]),
|
||||||
extport: +record[1],
|
extport: +record[1],
|
||||||
intaddr: arrtoip(iptoarr(record[2 + recshift])),
|
intaddr: arrtoip(iptoarr(record[3])),
|
||||||
intport: +record[3 + recshift],
|
intport: +record[4],
|
||||||
expires: record[4 + recshift] - timelocal(localtime()),
|
expires: record[5] - timelocal(localtime()),
|
||||||
descr: descr
|
descr: descr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -84,14 +81,14 @@ const methods = {
|
|||||||
const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null');
|
const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null');
|
||||||
if (nft) {
|
if (nft) {
|
||||||
for (let line = nft.read('line'), num = 1; length(line); line = nft.read('line')) {
|
for (let line = nft.read('line'), num = 1; length(line); line = nft.read('line')) {
|
||||||
let m = match(line, /^\t\tiif ".+" @nh,72,8 (0x6|0x11) th dport ([0-9]+).* dnat ip to ([0-9.]+):([0-9]+)/);
|
let m = match(line, /^\t\tiif ".+" ip protocol ([ut].p) .* dport ([0-9]+).* dnat ip to ([0-9.]+):([0-9]+)/);
|
||||||
if (m) {
|
if (m) {
|
||||||
push(rules, {
|
push(rules, {
|
||||||
num: `${num}`,
|
num: `${num}`,
|
||||||
intaddr: arrtoip(iptoarr(m[3])),
|
intaddr: arrtoip(iptoarr(m[3])),
|
||||||
intport: +m[4],
|
intport: +m[4],
|
||||||
extport: +m[2],
|
extport: +m[2],
|
||||||
proto: (m[1] == '0x6') ? 'TCP' : 'UDP'
|
proto: uc(m[1])
|
||||||
});
|
});
|
||||||
num++;
|
num++;
|
||||||
}
|
}
|
||||||
@@ -101,17 +98,14 @@ const methods = {
|
|||||||
const nftipv6 = popen('nft --handle list chain inet fw4 upnp_forward 2>/dev/null');
|
const nftipv6 = popen('nft --handle list chain inet fw4 upnp_forward 2>/dev/null');
|
||||||
if (nftipv6) {
|
if (nftipv6) {
|
||||||
for (let line = nftipv6.read('line'), num = 1; length(line); line = nftipv6.read('line')) {
|
for (let line = nftipv6.read('line'), num = 1; length(line); line = nftipv6.read('line')) {
|
||||||
let m = match(line, /^\t\tiif ".+" th dport ([0-9]+) @nh,192,128 0x([0-9a-f]+) @nh,48,8 (0x6|0x11) accept/);
|
let m = match(line, /^\t\tiif ".+" th dport ([0-9]+) ip6 daddr ([0-9a-f:]+) ip6 nexthdr ([ut].p) accept/);
|
||||||
if (m) {
|
if (m) {
|
||||||
push(rules, {
|
push(rules, {
|
||||||
num: '0',
|
num: '0',
|
||||||
intaddr: arrtoip(iptoarr(join(':', [substr(m[2], 0, 4),
|
intaddr: m[2],
|
||||||
substr(m[2], 4, 4), substr(m[2], 8, 4), substr(m[2], 12, 4), substr(m[2], 16, 4),
|
|
||||||
substr(m[2], 20, 4), substr(m[2], 24, 4), substr(m[2], 28, 4)
|
|
||||||
]))),
|
|
||||||
intport: +m[1],
|
intport: +m[1],
|
||||||
extport: +m[1],
|
extport: +m[1],
|
||||||
proto: (m[3] == '0x6') ? 'TCP' : 'UDP'
|
proto: uc(m[3])
|
||||||
});
|
});
|
||||||
num++;
|
num++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-vantage
|
PKG_NAME:=luci-app-vantage
|
||||||
PKG_VERSION:=1.0.3
|
PKG_VERSION:=1.0.4
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
LUCI_TITLE:=Vantage dashboard
|
LUCI_TITLE:=Vantage dashboard
|
||||||
|
|||||||
@@ -37,9 +37,18 @@
|
|||||||
--vt-info-soft: var(--v-info-soft, rgba(31, 99, 198, .1));
|
--vt-info-soft: var(--v-info-soft, rgba(31, 99, 198, .1));
|
||||||
--vt-font: var(--v-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
|
--vt-font: var(--v-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
|
||||||
--vt-mono: var(--v-mono, ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace);
|
--vt-mono: var(--v-mono, ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace);
|
||||||
|
--vt-fs-micro: var(--v-fs-micro, 10.5px); /* the theme's type scale */
|
||||||
|
--vt-fs-sm: var(--v-fs-sm, 12px);
|
||||||
|
--vt-fs-base: var(--v-fs-base, 13px);
|
||||||
|
--vt-fs-md: var(--v-fs-md, 14px);
|
||||||
|
--vt-fs-lg: var(--v-fs-lg, 16px);
|
||||||
|
--vt-fs-xl: var(--v-fs-xl, 20px);
|
||||||
|
--vt-fs-2xl: var(--v-fs-2xl, 24px);
|
||||||
--vt-r-sm: var(--v-r-sm, 6px);
|
--vt-r-sm: var(--v-r-sm, 6px);
|
||||||
--vt-r: var(--v-r, 10px);
|
--vt-r: var(--v-r, 10px);
|
||||||
--vt-r-lg: var(--v-r-lg, 14px);
|
--vt-r-lg: var(--v-r-lg, 14px);
|
||||||
|
--vt-ctl: var(--v-ctl-h, 36px); /* every control: buttons, fields, chips */
|
||||||
|
--vt-r-ctl: var(--v-r, 6px); /* their corners, as the theme's controls */
|
||||||
--vt-shadow: var(--v-shadow, 0 1px 2px rgba(16, 20, 24, .05), 0 1px 1px rgba(16, 20, 24, .03));
|
--vt-shadow: var(--v-shadow, 0 1px 2px rgba(16, 20, 24, .05), 0 1px 1px rgba(16, 20, 24, .03));
|
||||||
|
|
||||||
/* derived: readable accent for text/strokes on surfaces (a lime or
|
/* derived: readable accent for text/strokes on surfaces (a lime or
|
||||||
@@ -76,6 +85,12 @@
|
|||||||
--vt-sheet-shadow: -18px 0 48px rgba(12, 14, 17, .16), -2px 0 8px rgba(12, 14, 17, .06);
|
--vt-sheet-shadow: -18px 0 48px rgba(12, 14, 17, .16), -2px 0 8px rgba(12, 14, 17, .06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* touch: controls follow the pointer, not the width (the theme sets
|
||||||
|
--v-ctl-h the same way; this is the fallback under other themes) */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.vt-app { --vt-ctl: var(--v-ctl-h, 44px); }
|
||||||
|
}
|
||||||
|
|
||||||
html[data-darkmode="true"] .vt-app {
|
html[data-darkmode="true"] .vt-app {
|
||||||
--vt-bg: var(--v-bg, #0f1113);
|
--vt-bg: var(--v-bg, #0f1113);
|
||||||
--vt-surface: var(--v-surface, #171a1d);
|
--vt-surface: var(--v-surface, #171a1d);
|
||||||
@@ -121,7 +136,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
.vt-app {
|
.vt-app {
|
||||||
color: var(--vt-text);
|
color: var(--vt-text);
|
||||||
font-family: var(--vt-font);
|
font-family: var(--vt-font);
|
||||||
font-size: 13.5px;
|
font-size: var(--vt-fs-base);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -154,11 +169,11 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
.vt-app .vt-mono { font-family: var(--vt-mono); font-size: .92em; letter-spacing: -.01em; }
|
.vt-app .vt-mono { font-family: var(--vt-mono); font-size: .92em; letter-spacing: -.01em; }
|
||||||
.vt-app .vt-tn { font-variant-numeric: tabular-nums; }
|
.vt-app .vt-tn { font-variant-numeric: tabular-nums; }
|
||||||
.vt-app .vt-muted { color: var(--vt-text-3); }
|
.vt-app .vt-muted { color: var(--vt-text-3); }
|
||||||
.vt-app .vt-hint { color: var(--vt-text-3); font-size: 12px; }
|
.vt-app .vt-hint { color: var(--vt-text-3); font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-t-warn { color: var(--vt-warn); }
|
.vt-app .vt-t-warn { color: var(--vt-warn); }
|
||||||
.vt-app .vt-t-err { color: var(--vt-err); }
|
.vt-app .vt-t-err { color: var(--vt-err); }
|
||||||
.vt-app .vt-label { display: block; color: var(--vt-text-3); font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
|
.vt-app .vt-label { display: block; color: var(--vt-text-3); font-size: var(--vt-fs-micro); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
|
||||||
.vt-app .vt-big { font-size: 30px; font-weight: 680; letter-spacing: -.02em; line-height: 1.05; }
|
.vt-app .vt-big { font-size: var(--vt-fs-2xl); font-weight: 680; letter-spacing: -.02em; line-height: 1.05; }
|
||||||
|
|
||||||
.vt-app .vt-ico {
|
.vt-app .vt-ico {
|
||||||
width: 18px; height: 18px; flex: none;
|
width: 18px; height: 18px; flex: none;
|
||||||
@@ -174,10 +189,10 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
/* buttons of the app (the theme's global button look is reset here) */
|
/* buttons of the app (the theme's global button look is reset here) */
|
||||||
.vt-app .vt-btn {
|
.vt-app .vt-btn {
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
height: 32px; padding: 0 12px;
|
height: var(--vt-ctl); padding: 0 12px;
|
||||||
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm);
|
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl);
|
||||||
background: var(--vt-surface); color: var(--vt-text);
|
background: var(--vt-surface); color: var(--vt-text);
|
||||||
font-size: 13px; font-weight: 560; line-height: 1;
|
font-size: var(--vt-fs-base); font-weight: 560; line-height: 1;
|
||||||
cursor: pointer; box-shadow: none; text-transform: none; min-width: 0;
|
cursor: pointer; box-shadow: none; text-transform: none; min-width: 0;
|
||||||
transition: background .15s, border-color .15s;
|
transition: background .15s, border-color .15s;
|
||||||
}
|
}
|
||||||
@@ -190,7 +205,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
|
|
||||||
.vt-app .vt-link {
|
.vt-app .vt-link {
|
||||||
display: inline-flex; align-items: center; gap: 2px;
|
display: inline-flex; align-items: center; gap: 2px;
|
||||||
color: var(--vt-accent-text); font-size: 12.5px; font-weight: 600; text-decoration: none; white-space: nowrap;
|
color: var(--vt-accent-text); font-size: var(--vt-fs-sm); font-weight: 600; text-decoration: none; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.vt-app .vt-link .vt-ico { width: 14px; height: 14px; transition: transform .15s; }
|
.vt-app .vt-link .vt-ico { width: 14px; height: 14px; transition: transform .15s; }
|
||||||
.vt-app .vt-link:hover { text-decoration: none; color: var(--vt-text); }
|
.vt-app .vt-link:hover { text-decoration: none; color: var(--vt-text); }
|
||||||
@@ -198,7 +213,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
|
|
||||||
.vt-app .vt-pill {
|
.vt-app .vt-pill {
|
||||||
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
|
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
|
||||||
border-radius: 999px; font-size: 10.5px; font-weight: 650; letter-spacing: .02em; white-space: nowrap;
|
border-radius: 999px; font-size: var(--vt-fs-micro); font-weight: 650; letter-spacing: .02em; white-space: nowrap;
|
||||||
background: var(--vt-surface-3); color: var(--vt-text-2);
|
background: var(--vt-surface-3); color: var(--vt-text-2);
|
||||||
}
|
}
|
||||||
.vt-app .vt-pill-accent { background: var(--vt-accent); color: var(--vt-accent-ink); }
|
.vt-app .vt-pill-accent { background: var(--vt-accent); color: var(--vt-accent-ink); }
|
||||||
@@ -206,7 +221,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
|
|
||||||
.vt-app .vt-band {
|
.vt-app .vt-band {
|
||||||
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
|
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
|
||||||
border-radius: 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .01em; white-space: nowrap;
|
border-radius: 5px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .01em; white-space: nowrap;
|
||||||
--vt-bc: var(--vt-b60);
|
--vt-bc: var(--vt-b60);
|
||||||
background: color-mix(in oklab, var(--vt-bc) 14%, transparent);
|
background: color-mix(in oklab, var(--vt-bc) 14%, transparent);
|
||||||
color: color-mix(in oklab, var(--vt-bc) 78%, var(--vt-text));
|
color: color-mix(in oklab, var(--vt-bc) 78%, var(--vt-text));
|
||||||
@@ -215,11 +230,11 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
.vt-app .vt-band-5g { --vt-bc: var(--vt-b5); }
|
.vt-app .vt-band-5g { --vt-bc: var(--vt-b5); }
|
||||||
.vt-app .vt-band-6g { --vt-bc: var(--vt-b6); }
|
.vt-app .vt-band-6g { --vt-bc: var(--vt-b6); }
|
||||||
|
|
||||||
.vt-app .vt-gen { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
|
.vt-app .vt-gen { font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
|
||||||
|
|
||||||
.vt-app .vt-src {
|
.vt-app .vt-src {
|
||||||
display: inline-flex; align-items: center; height: 16px; padding: 0 5px;
|
display: inline-flex; align-items: center; height: 16px; padding: 0 5px;
|
||||||
border-radius: 4px; font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
|
border-radius: 4px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
|
||||||
background: var(--vt-surface-3); color: var(--vt-text-3); white-space: nowrap;
|
background: var(--vt-surface-3); color: var(--vt-text-3); white-space: nowrap;
|
||||||
}
|
}
|
||||||
.vt-app .vt-src-alias { background: var(--vt-accent-soft); color: var(--vt-accent-text); }
|
.vt-app .vt-src-alias { background: var(--vt-accent-soft); color: var(--vt-accent-text); }
|
||||||
@@ -263,20 +278,20 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
}
|
}
|
||||||
.vt-app .vt-sk-max {
|
.vt-app .vt-sk-max {
|
||||||
position: absolute; right: 0; top: -2px; transform: translateY(-100%);
|
position: absolute; right: 0; top: -2px; transform: translateY(-100%);
|
||||||
font-size: 10px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
|
font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-sk-thresh {
|
.vt-app .vt-sk-thresh {
|
||||||
position: absolute; transform: translate(-100%, -120%);
|
position: absolute; transform: translate(-100%, -120%);
|
||||||
font-size: 10px; line-height: 1; color: var(--vt-warn); white-space: nowrap; pointer-events: none;
|
font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-warn); white-space: nowrap; pointer-events: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-sk-note { position: absolute; right: 0; bottom: 4px; font-size: 10.5px; color: var(--vt-chart-axis); }
|
.vt-app .vt-sk-note { position: absolute; right: 0; bottom: 4px; font-size: var(--vt-fs-micro); color: var(--vt-chart-axis); }
|
||||||
.vt-app .vt-spark-cpu { --vt-sc: var(--vt-chart-5); }
|
.vt-app .vt-spark-cpu { --vt-sc: var(--vt-chart-5); }
|
||||||
.vt-app .vt-spark-2g { --vt-sc: var(--vt-b2); }
|
.vt-app .vt-spark-2g { --vt-sc: var(--vt-b2); }
|
||||||
.vt-app .vt-spark-5g { --vt-sc: var(--vt-b5); }
|
.vt-app .vt-spark-5g { --vt-sc: var(--vt-b5); }
|
||||||
.vt-app .vt-spark-6g { --vt-sc: var(--vt-b6); }
|
.vt-app .vt-spark-6g { --vt-sc: var(--vt-b6); }
|
||||||
.vt-app .vt-spark-x { --vt-sc: var(--vt-text-3); }
|
.vt-app .vt-spark-x { --vt-sc: var(--vt-text-3); }
|
||||||
.vt-app .vt-spark-up { --vt-sc: var(--vt-series-up); }
|
.vt-app .vt-spark-up { --vt-sc: var(--vt-series-up); }
|
||||||
.vt-app .vt-cap { display: block; margin-top: 3px; font-size: 10px; line-height: 1.1; color: var(--vt-chart-axis); letter-spacing: .02em; white-space: nowrap; }
|
.vt-app .vt-cap { display: block; margin-top: 3px; font-size: var(--vt-fs-micro); line-height: 1.1; color: var(--vt-chart-axis); letter-spacing: .02em; white-space: nowrap; }
|
||||||
.vt-app .vt-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
.vt-app .vt-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ 3. layout */
|
/* ------------------------------------------------------------ 3. layout */
|
||||||
@@ -299,7 +314,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
|
|
||||||
/* ----------------------------------------------- 3b. arranging the tiles */
|
/* ----------------------------------------------- 3b. arranging the tiles */
|
||||||
|
|
||||||
.vt-app .vt-layout-btn { flex: none; height: 30px; gap: 6px; font-size: 12px; }
|
.vt-app .vt-layout-btn { flex: none; gap: 6px; font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-layout-btn .vt-ico { width: 16px; height: 16px; }
|
.vt-app .vt-layout-btn .vt-ico { width: 16px; height: 16px; }
|
||||||
.vt-app .vt-layout-btn[aria-pressed="true"] { background: var(--vt-accent-soft); color: var(--vt-text); }
|
.vt-app .vt-layout-btn[aria-pressed="true"] { background: var(--vt-accent-soft); color: var(--vt-text); }
|
||||||
@container vt (max-width: 699px) { .vt-app .vt-layout-btn span { display: none; } }
|
@container vt (max-width: 699px) { .vt-app .vt-layout-btn span { display: none; } }
|
||||||
@@ -312,7 +327,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
}
|
}
|
||||||
.vt-app .vt-editbar[hidden] { display: none; }
|
.vt-app .vt-editbar[hidden] { display: none; }
|
||||||
.vt-app .vt-editbar-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 260px; min-width: 0; }
|
.vt-app .vt-editbar-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 260px; min-width: 0; }
|
||||||
.vt-app .vt-editbar-text strong { font-size: 13px; font-weight: 650; }
|
.vt-app .vt-editbar-text strong { font-size: var(--vt-fs-base); font-weight: 650; }
|
||||||
.vt-app .vt-edit-hidden { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
.vt-app .vt-edit-hidden { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||||
.vt-app .vt-edit-hidden:empty { display: none; }
|
.vt-app .vt-edit-hidden:empty { display: none; }
|
||||||
.vt-app .vt-edit-hidden .vt-fchip .vt-ico { width: 13px; height: 13px; }
|
.vt-app .vt-edit-hidden .vt-fchip .vt-ico { width: 13px; height: 13px; }
|
||||||
@@ -334,11 +349,11 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
margin: 8px; padding: 6px; border-radius: var(--vt-r-sm);
|
margin: 8px; padding: 6px; border-radius: var(--vt-r-sm);
|
||||||
background: var(--vt-surface); border: 1px solid var(--vt-line-2); box-shadow: var(--vt-shadow);
|
background: var(--vt-surface); border: 1px solid var(--vt-line-2); box-shadow: var(--vt-shadow);
|
||||||
}
|
}
|
||||||
.vt-app .vt-ov-name { flex: 1 1 auto; min-width: 0; font-weight: 650; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-ov-name { flex: 1 1 auto; min-width: 0; font-weight: 650; font-size: var(--vt-fs-base); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-ov-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
|
.vt-app .vt-ov-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
|
||||||
.vt-app .vt-ov-grip, .vt-app .vt-ov-btn {
|
.vt-app .vt-ov-grip, .vt-app .vt-ov-btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
||||||
width: 30px; height: 30px; min-width: 0; margin: 0; padding: 0; border: 0; border-radius: 8px;
|
width: var(--vt-ctl); height: var(--vt-ctl); min-width: 0; margin: 0; padding: 0; border: 0; border-radius: var(--vt-r-ctl);
|
||||||
background: transparent; color: var(--vt-text-2); box-shadow: none; cursor: pointer;
|
background: transparent; color: var(--vt-text-2); box-shadow: none; cursor: pointer;
|
||||||
}
|
}
|
||||||
.vt-app .vt-ov-grip { cursor: grab; touch-action: none; background: var(--vt-surface-3); color: var(--vt-text); }
|
.vt-app .vt-ov-grip { cursor: grab; touch-action: none; background: var(--vt-surface-3); color: var(--vt-text); }
|
||||||
@@ -348,7 +363,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
.vt-app .vt-ov-grip .vt-ico { stroke-width: 3.2; }
|
.vt-app .vt-ov-grip .vt-ico { stroke-width: 3.2; }
|
||||||
.vt-app .vt-ov-flip .vt-ico { transform: scaleX(-1); }
|
.vt-app .vt-ov-flip .vt-ico { transform: scaleX(-1); }
|
||||||
.vt-app .vt-ov-hide { margin-left: 4px; }
|
.vt-app .vt-ov-hide { margin-left: 4px; }
|
||||||
.vt-app .vt-ov-size { min-width: 26px; text-align: center; font-size: 11.5px; font-weight: 650; color: var(--vt-text-2); }
|
.vt-app .vt-ov-size { min-width: 26px; text-align: center; font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-2); }
|
||||||
.vt-app.vt-edit-1col .vt-ov-col { display: none; }
|
.vt-app.vt-edit-1col .vt-ov-col { display: none; }
|
||||||
|
|
||||||
.vt-app.vt-editing .vt-cell { transition: top .2s ease, left .2s ease; }
|
.vt-app.vt-editing .vt-cell { transition: top .2s ease, left .2s ease; }
|
||||||
@@ -371,7 +386,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
}
|
}
|
||||||
.vt-app .vt-tile-title {
|
.vt-app .vt-tile-title {
|
||||||
display: inline-flex; align-items: center; gap: 8px; min-width: 0;
|
display: inline-flex; align-items: center; gap: 8px; min-width: 0;
|
||||||
font-size: 13px; font-weight: 650; letter-spacing: .005em; color: var(--vt-text);
|
font-size: var(--vt-fs-base); font-weight: 650; letter-spacing: .005em; color: var(--vt-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.vt-app .vt-tile-title .vt-ico { color: var(--vt-text-3); width: 17px; height: 17px; }
|
.vt-app .vt-tile-title .vt-ico { color: var(--vt-text-3); width: 17px; height: 17px; }
|
||||||
@@ -382,7 +397,7 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
@container vt (max-width: 699px) {
|
@container vt (max-width: 699px) {
|
||||||
.vt-app .vt-fold-btn {
|
.vt-app .vt-fold-btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
||||||
width: 32px; height: 32px; margin: -4px -8px -4px 0; padding: 0; border: 0; border-radius: 8px;
|
width: var(--vt-ctl); height: var(--vt-ctl); margin: -4px -8px -4px 0; padding: 0; border: 0; border-radius: var(--vt-r-ctl);
|
||||||
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
||||||
}
|
}
|
||||||
.vt-app .vt-fold-btn:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
.vt-app .vt-fold-btn:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
||||||
@@ -412,9 +427,9 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
}
|
}
|
||||||
.vt-app .vt-rsum:hover { background: var(--vt-surface-2); }
|
.vt-app .vt-rsum:hover { background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-rsum .vt-status { margin: 0; }
|
.vt-app .vt-rsum .vt-status { margin: 0; }
|
||||||
.vt-app .vt-rsum-main { font-weight: 650; font-size: 13px; white-space: nowrap; }
|
.vt-app .vt-rsum-main { font-weight: 650; font-size: var(--vt-fs-base); white-space: nowrap; }
|
||||||
.vt-app .vt-rsum-sub { font-size: 12px; color: var(--vt-text-3); white-space: nowrap; }
|
.vt-app .vt-rsum-sub { font-size: var(--vt-fs-sm); color: var(--vt-text-3); white-space: nowrap; }
|
||||||
.vt-app .vt-rsum-rate { margin-left: auto; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; }
|
.vt-app .vt-rsum-rate { margin-left: auto; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; }
|
||||||
.vt-app .vt-rsum > .vt-ico { color: var(--vt-text-3); }
|
.vt-app .vt-rsum > .vt-ico { color: var(--vt-text-3); }
|
||||||
|
|
||||||
.vt-app .vt-empty {
|
.vt-app .vt-empty {
|
||||||
@@ -422,9 +437,9 @@ html[data-darkmode="true"] .vt-app {
|
|||||||
padding: 32px 16px; text-align: center; color: var(--vt-text-3);
|
padding: 32px 16px; text-align: center; color: var(--vt-text-3);
|
||||||
}
|
}
|
||||||
.vt-app .vt-empty > .vt-ico { width: 28px; height: 28px; stroke-width: 1.4; margin-bottom: 4px; }
|
.vt-app .vt-empty > .vt-ico { width: 28px; height: 28px; stroke-width: 1.4; margin-bottom: 4px; }
|
||||||
.vt-app .vt-empty-title { color: var(--vt-text); font-weight: 620; font-size: 14px; }
|
.vt-app .vt-empty-title { color: var(--vt-text); font-weight: 620; font-size: var(--vt-fs-md); }
|
||||||
.vt-app .vt-empty .vt-btn { margin-top: 8px; }
|
.vt-app .vt-empty .vt-btn { margin-top: 8px; }
|
||||||
.vt-app .vt-empty-inline { color: var(--vt-text-3); font-size: 12.5px; padding: 4px 0; }
|
.vt-app .vt-empty-inline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); padding: 4px 0; }
|
||||||
|
|
||||||
/* ----------------------------------------------------- 4. head, health */
|
/* ----------------------------------------------------- 4. head, health */
|
||||||
|
|
||||||
@@ -458,16 +473,16 @@ h2, h3{
|
|||||||
|
|
||||||
.vt-app .vt-head-text { min-width: 0; flex: 1; }
|
.vt-app .vt-head-text { min-width: 0; flex: 1; }
|
||||||
.vt-app .vt-headline {
|
.vt-app .vt-headline {
|
||||||
font-size: 21px; font-weight: 680; letter-spacing: -.015em; line-height: 1.2; color: var(--vt-text);
|
font-size: var(--vt-fs-xl); font-weight: 680; letter-spacing: -.015em; line-height: 1.2; color: var(--vt-text);
|
||||||
margin: 0; padding: 0; border: 0; text-transform: none;
|
margin: 0; padding: 0; border: 0; text-transform: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-subline { color: var(--vt-text-3); font-size: 12.5px; margin-top: 2px; }
|
.vt-app .vt-subline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 2px; }
|
||||||
|
|
||||||
.vt-app .vt-livepill {
|
.vt-app .vt-livepill {
|
||||||
flex: none; display: inline-flex; align-items: center; gap: 8px;
|
flex: none; display: inline-flex; align-items: center; gap: 8px;
|
||||||
height: 30px; padding: 0 12px 0 10px; border-radius: 999px;
|
height: 30px; padding: 0 12px 0 10px; border-radius: 999px;
|
||||||
border: 1px solid var(--vt-line); background: var(--vt-surface);
|
border: 1px solid var(--vt-line); background: var(--vt-surface);
|
||||||
font-size: 12px; font-weight: 560; color: var(--vt-text-2); white-space: nowrap;
|
font-size: var(--vt-fs-sm); font-weight: 560; color: var(--vt-text-2); white-space: nowrap;
|
||||||
}
|
}
|
||||||
.vt-app .vt-live-beat { width: 8px; height: 8px; border-radius: 50%; background: var(--vt-ok); box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 50%, transparent); animation: vt-beat 2s infinite; }
|
.vt-app .vt-live-beat { width: 8px; height: 8px; border-radius: 50%; background: var(--vt-ok); box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 50%, transparent); animation: vt-beat 2s infinite; }
|
||||||
.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; }
|
.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; }
|
||||||
@@ -504,10 +519,10 @@ h2, h3{
|
|||||||
.vt-app .vt-chip-ico .vt-ico { width: 15px; height: 15px; stroke-width: 2.1; }
|
.vt-app .vt-chip-ico .vt-ico { width: 15px; height: 15px; stroke-width: 2.1; }
|
||||||
.vt-app .vt-chip-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
.vt-app .vt-chip-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
||||||
.vt-app .vt-chip-top { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
|
.vt-app .vt-chip-top { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
|
||||||
.vt-app .vt-chip-title { font-size: 11px; line-height: 1.3; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
|
.vt-app .vt-chip-title { font-size: var(--vt-fs-micro); line-height: 1.3; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
|
||||||
.vt-app .vt-chip-value { font-size: 15px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
.vt-app .vt-chip-value { font-size: var(--vt-fs-md); font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
||||||
.vt-app .vt-chip-reason {
|
.vt-app .vt-chip-reason {
|
||||||
font-size: 12px; color: var(--vt-text-2); margin-top: 2px;
|
font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 2px;
|
||||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,9 +543,9 @@ h2, h3{
|
|||||||
.vt-app button.vt-kpi { cursor: pointer; }
|
.vt-app button.vt-kpi { cursor: pointer; }
|
||||||
.vt-app button.vt-kpi:hover { background: var(--vt-surface-2); }
|
.vt-app button.vt-kpi:hover { background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-kpi + .vt-kpi { box-shadow: -1px 0 0 var(--vt-line); border-radius: 0 var(--vt-r-sm) var(--vt-r-sm) 0; }
|
.vt-app .vt-kpi + .vt-kpi { box-shadow: -1px 0 0 var(--vt-line); border-radius: 0 var(--vt-r-sm) var(--vt-r-sm) 0; }
|
||||||
.vt-app .vt-kpi-label { font-size: 11px; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
|
.vt-app .vt-kpi-label { font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
|
||||||
.vt-app .vt-kpi-value { font-size: 22px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; }
|
.vt-app .vt-kpi-value { font-size: var(--vt-fs-xl); font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; }
|
||||||
.vt-app .vt-kpi-sub { font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-kpi-sub { font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-kpi-warn .vt-kpi-value { color: var(--vt-warn); }
|
.vt-app .vt-kpi-warn .vt-kpi-value { color: var(--vt-warn); }
|
||||||
@container vthero (max-width: 700px) {
|
@container vthero (max-width: 700px) {
|
||||||
.vt-app .vt-kpis { grid-template-columns: 1fr 1fr; }
|
.vt-app .vt-kpis { grid-template-columns: 1fr 1fr; }
|
||||||
@@ -577,9 +592,9 @@ h2, h3{
|
|||||||
}
|
}
|
||||||
.vt-app .vt-node:hover { border-color: color-mix(in oklab, var(--vt-accent-line) 60%, var(--vt-line)); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(12, 14, 17, .08); background: var(--vt-surface); }
|
.vt-app .vt-node:hover { border-color: color-mix(in oklab, var(--vt-accent-line) 60%, var(--vt-line)); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(12, 14, 17, .08); background: var(--vt-surface); }
|
||||||
.vt-app .vt-node-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
|
.vt-app .vt-node-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
|
||||||
.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; }
|
.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; }
|
||||||
.vt-app .vt-node-title { font-size: 13.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-node-title { font-size: var(--vt-fs-base); font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-node-ico {
|
.vt-app .vt-node-ico {
|
||||||
flex: none; width: 32px; height: 32px; border-radius: 9px;
|
flex: none; width: 32px; height: 32px; border-radius: 9px;
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
@@ -589,12 +604,12 @@ h2, h3{
|
|||||||
.vt-app .vt-node-gw { justify-self: stretch; align-self: center; }
|
.vt-app .vt-node-gw { justify-self: stretch; align-self: center; }
|
||||||
.vt-app .vt-node-ap { justify-self: stretch; align-self: center; padding: 12px 13px; border-color: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line)); }
|
.vt-app .vt-node-ap { justify-self: stretch; align-self: center; padding: 12px 13px; border-color: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line)); }
|
||||||
.vt-app .vt-node-ap .vt-node-ico { background: var(--vt-accent); color: var(--vt-accent-ink); }
|
.vt-app .vt-node-ap .vt-node-ico { background: var(--vt-accent); color: var(--vt-accent-ink); }
|
||||||
.vt-app .vt-node-ap .vt-node-title { font-size: 14.5px; }
|
.vt-app .vt-node-ap .vt-node-title { font-size: var(--vt-fs-md); }
|
||||||
.vt-app .vt-node-gen { text-transform: none; letter-spacing: 0; font-weight: 560; }
|
.vt-app .vt-node-gen { text-transform: none; letter-spacing: 0; font-weight: 560; }
|
||||||
.vt-app .vt-node-radio { padding: 8px 11px; }
|
.vt-app .vt-node-radio { padding: 8px 11px; }
|
||||||
.vt-app .vt-node-net, .vt-app .vt-node-net:hover { text-decoration: none; color: var(--vt-text); }
|
.vt-app .vt-node-net, .vt-app .vt-node-net:hover { text-decoration: none; color: var(--vt-text); }
|
||||||
.vt-app .vt-node-ssid { padding: 7px 10px; }
|
.vt-app .vt-node-ssid { padding: 7px 10px; }
|
||||||
.vt-app .vt-node-ssid .vt-node-title { display: flex; align-items: center; gap: 6px; font-size: 13px; }
|
.vt-app .vt-node-ssid .vt-node-title { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-base); }
|
||||||
.vt-app .vt-node-ssid .vt-node-title .vt-ico { color: var(--vt-text-3); }
|
.vt-app .vt-node-ssid .vt-node-title .vt-ico { color: var(--vt-text-3); }
|
||||||
.vt-app .vt-ssid-name span { overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-ssid-name span { overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-node-idle { background: var(--vt-surface-2); box-shadow: none; }
|
.vt-app .vt-node-idle { background: var(--vt-surface-2); box-shadow: none; }
|
||||||
@@ -618,14 +633,14 @@ h2, h3{
|
|||||||
padding: 0 6px; border-radius: 6px; background: var(--vt-surface);
|
padding: 0 6px; border-radius: 6px; background: var(--vt-surface);
|
||||||
}
|
}
|
||||||
.vt-app .vt-link-rate .vt-ico { width: 13px; height: 13px; align-self: center; stroke-width: 2.2; }
|
.vt-app .vt-link-rate .vt-ico { width: 13px; height: 13px; align-self: center; stroke-width: 2.2; }
|
||||||
.vt-app .vt-link-rate b { font-size: 17px; font-weight: 700; letter-spacing: -.01em; }
|
.vt-app .vt-link-rate b { font-size: var(--vt-fs-lg); font-weight: 700; letter-spacing: -.01em; }
|
||||||
.vt-app .vt-link-rate small { font-size: 11px; color: var(--vt-text-3); font-weight: 600; }
|
.vt-app .vt-link-rate small { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; }
|
||||||
.vt-app .vt-link-down { grid-row: 1; align-self: end; }
|
.vt-app .vt-link-down { grid-row: 1; align-self: end; }
|
||||||
.vt-app .vt-link-down .vt-ico { color: var(--vt-accent-text); }
|
.vt-app .vt-link-down .vt-ico { color: var(--vt-accent-text); }
|
||||||
.vt-app .vt-link-up { font-size: 12px; }
|
.vt-app .vt-link-up { font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-link-up b { font-size: 13px; font-weight: 650; color: var(--vt-text-2); }
|
.vt-app .vt-link-up b { font-size: var(--vt-fs-base); font-weight: 650; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); }
|
.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); }
|
||||||
.vt-app .vt-link-meta { font-size: 11px; color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; }
|
.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; }
|
||||||
|
|
||||||
/* the branches spread over the height the tile gets from its row, so a
|
/* the branches spread over the height the tile gets from its row, so a
|
||||||
tall neighbour leaves no empty band above or below the diagram */
|
tall neighbour leaves no empty band above or below the diagram */
|
||||||
@@ -664,8 +679,8 @@ h2, h3{
|
|||||||
|
|
||||||
.vt-app .vt-sys-mem { display: flex; flex-direction: column; gap: 7px; }
|
.vt-app .vt-sys-mem { display: flex; flex-direction: column; gap: 7px; }
|
||||||
.vt-app .vt-mem-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
.vt-app .vt-mem-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||||||
.vt-app .vt-mem-avail { font-size: 17px; font-weight: 680; }
|
.vt-app .vt-mem-avail { font-size: var(--vt-fs-lg); font-weight: 680; }
|
||||||
.vt-app .vt-mem-of { font-size: 12px; color: var(--vt-text-3); }
|
.vt-app .vt-mem-of { font-size: var(--vt-fs-sm); color: var(--vt-text-3); }
|
||||||
.vt-app .vt-seg { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--vt-surface-3); gap: 2px; }
|
.vt-app .vt-seg { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--vt-surface-3); gap: 2px; }
|
||||||
.vt-app .vt-seg-part { height: 100%; min-width: 0; transition: width .7s cubic-bezier(.2, .7, .2, 1); }
|
.vt-app .vt-seg-part { height: 100%; min-width: 0; transition: width .7s cubic-bezier(.2, .7, .2, 1); }
|
||||||
.vt-app .vt-seg-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; }
|
.vt-app .vt-seg-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; }
|
||||||
@@ -675,24 +690,24 @@ h2, h3{
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
.vt-app .vt-seg-free { background: transparent; }
|
.vt-app .vt-seg-free { background: transparent; }
|
||||||
.vt-app .vt-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--vt-text-2); }
|
.vt-app .vt-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
|
||||||
.vt-app .vt-legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
.vt-app .vt-legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||||
.vt-app .vt-legend-item b { font-weight: 650; color: var(--vt-text); }
|
.vt-app .vt-legend-item b { font-weight: 650; color: var(--vt-text); }
|
||||||
.vt-app .vt-key { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
.vt-app .vt-key { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
||||||
.vt-app .vt-key.vt-seg-free { background: var(--vt-surface-3); box-shadow: inset 0 0 0 1px var(--vt-line-2); }
|
.vt-app .vt-key.vt-seg-free { background: var(--vt-surface-3); box-shadow: inset 0 0 0 1px var(--vt-line-2); }
|
||||||
.vt-app .vt-swap { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
.vt-app .vt-swap { display: flex; align-items: center; gap: 8px; font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-swap:empty { display: none; }
|
.vt-app .vt-swap:empty { display: none; }
|
||||||
.vt-app .vt-thin { flex: 1; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
|
.vt-app .vt-thin { flex: 1; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
|
||||||
.vt-app .vt-thin-fill { display: block; height: 100%; background: var(--vt-mem); }
|
.vt-app .vt-thin-fill { display: block; height: 100%; background: var(--vt-mem); }
|
||||||
|
|
||||||
.vt-app .vt-sys-store { display: flex; flex-direction: column; gap: 8px; }
|
.vt-app .vt-sys-store { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; }
|
.vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: 12.5px; margin-bottom: 4px; }
|
.vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: var(--vt-fs-sm); margin-bottom: 4px; }
|
||||||
.vt-app .vt-bar { height: 6px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
|
.vt-app .vt-bar { height: 6px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
|
||||||
.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); transition: width .7s ease; }
|
.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); transition: width .7s ease; }
|
||||||
.vt-app .vt-bg-warn { background: var(--vt-warn); }
|
.vt-app .vt-bg-warn { background: var(--vt-warn); }
|
||||||
.vt-app .vt-bg-err { background: var(--vt-err); }
|
.vt-app .vt-bg-err { background: var(--vt-err); }
|
||||||
.vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: 12px; color: var(--vt-text-2); }
|
.vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
|
||||||
|
|
||||||
/* ------------------------------------------------------------- 7. chart */
|
/* ------------------------------------------------------------- 7. chart */
|
||||||
|
|
||||||
@@ -700,7 +715,7 @@ h2, h3{
|
|||||||
a shared time axis and one crosshair across all rows */
|
a shared time axis and one crosshair across all rows */
|
||||||
.vt-app .vt-chart { display: flex; flex-direction: column; --vt-mh: 136px; --vt-mg: 16px; }
|
.vt-app .vt-chart { display: flex; flex-direction: column; --vt-mh: 136px; --vt-mg: 16px; }
|
||||||
.vt-app .vt-chart .vt-tile-title { flex-wrap: wrap; row-gap: 0; }
|
.vt-app .vt-chart .vt-tile-title { flex-wrap: wrap; row-gap: 0; }
|
||||||
.vt-app .vt-chart-sub { font-size: 12px; font-weight: 500; color: var(--vt-text-3); }
|
.vt-app .vt-chart-sub { font-size: var(--vt-fs-sm); font-weight: 500; color: var(--vt-text-3); }
|
||||||
.vt-app .vt-chart-sub::before { content: "·"; margin: 0 6px 0 0; }
|
.vt-app .vt-chart-sub::before { content: "·"; margin: 0 6px 0 0; }
|
||||||
.vt-app .vt-lg-key { width: 12px; height: 3px; border-radius: 2px; background: var(--vt-sc); flex: none; }
|
.vt-app .vt-lg-key { width: 12px; height: 3px; border-radius: 2px; background: var(--vt-sc); flex: none; }
|
||||||
/* one colour per direction in every row (series, gradients, dots, keys) */
|
/* one colour per direction in every row (series, gradients, dots, keys) */
|
||||||
@@ -709,7 +724,7 @@ h2, h3{
|
|||||||
.vt-app .vt-arr { font-weight: 700; }
|
.vt-app .vt-arr { font-weight: 700; }
|
||||||
.vt-app .vt-arr-dn { color: color-mix(in oklab, var(--vt-dir-dn) 70%, var(--vt-text)); }
|
.vt-app .vt-arr-dn { color: color-mix(in oklab, var(--vt-dir-dn) 70%, var(--vt-text)); }
|
||||||
.vt-app .vt-arr-up { color: color-mix(in oklab, var(--vt-dir-up) 70%, var(--vt-text)); }
|
.vt-app .vt-arr-up { color: color-mix(in oklab, var(--vt-dir-up) 70%, var(--vt-text)); }
|
||||||
.vt-app .vt-dir-legend { font-size: 11.5px; gap: 4px 12px; }
|
.vt-app .vt-dir-legend { font-size: var(--vt-fs-micro); gap: 4px 12px; }
|
||||||
|
|
||||||
.vt-app .vt-mult-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 300px; border-radius: var(--vt-r-sm); outline-offset: 4px; }
|
.vt-app .vt-mult-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 300px; border-radius: var(--vt-r-sm); outline-offset: 4px; }
|
||||||
.vt-app .vt-mults { flex: 1; display: grid; grid-auto-rows: minmax(100px, 1fr); }
|
.vt-app .vt-mults { flex: 1; display: grid; grid-auto-rows: minmax(100px, 1fr); }
|
||||||
@@ -719,11 +734,11 @@ h2, h3{
|
|||||||
}
|
}
|
||||||
.vt-app .vt-mult:first-child { border-top: 0; padding-top: 2px; }
|
.vt-app .vt-mult:first-child { border-top: 0; padding-top: 2px; }
|
||||||
.vt-app .vt-mult-head { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
|
.vt-app .vt-mult-head { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
|
||||||
.vt-app .vt-mult-name { font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-mult-name { font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-mult-now { display: flex; align-items: center; gap: 6px; font-size: 17px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; }
|
.vt-app .vt-mult-now { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-lg); font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; }
|
||||||
.vt-app .vt-mult-now b { font-weight: inherit; }
|
.vt-app .vt-mult-now b { font-weight: inherit; }
|
||||||
.vt-app .vt-mult-now-up { font-size: 13px; font-weight: 650; line-height: 1.3; color: var(--vt-text-2); }
|
.vt-app .vt-mult-now-up { font-size: var(--vt-fs-base); font-weight: 650; line-height: 1.3; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-mult-stats { display: flex; flex-wrap: wrap; gap: 0 8px; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
|
.vt-app .vt-mult-stats { display: flex; flex-wrap: wrap; gap: 0 8px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
|
||||||
.vt-app .vt-mult-stats b { font-weight: 600; color: var(--vt-text-2); }
|
.vt-app .vt-mult-stats b { font-weight: 600; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-mult-plot { position: relative; min-height: 72px; margin: 12px 0; --vt-sc: inherit; }
|
.vt-app .vt-mult-plot { position: relative; min-height: 72px; margin: 12px 0; --vt-sc: inherit; }
|
||||||
.vt-app .vt-mult-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
.vt-app .vt-mult-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
||||||
@@ -734,7 +749,7 @@ h2, h3{
|
|||||||
.vt-app .vt-chart-mid { fill: none; stroke: color-mix(in oklab, var(--vt-chart-axis) 55%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
.vt-app .vt-chart-mid { fill: none; stroke: color-mix(in oklab, var(--vt-chart-axis) 55%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
||||||
.vt-app .vt-mult-max {
|
.vt-app .vt-mult-max {
|
||||||
position: absolute; right: 0; top: -3px; transform: translateY(-100%);
|
position: absolute; right: 0; top: -3px; transform: translateY(-100%);
|
||||||
font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
|
font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-mult-max-up { top: auto; bottom: -3px; transform: translateY(100%); }
|
.vt-app .vt-mult-max-up { top: auto; bottom: -3px; transform: translateY(100%); }
|
||||||
.vt-app .vt-mult-dot {
|
.vt-app .vt-mult-dot {
|
||||||
@@ -743,7 +758,7 @@ h2, h3{
|
|||||||
}
|
}
|
||||||
.vt-app .vt-mult-over { position: absolute; top: 0; bottom: 20px; left: calc(var(--vt-mh) + var(--vt-mg)); right: 0; pointer-events: none; z-index: 3; }
|
.vt-app .vt-mult-over { position: absolute; top: 0; bottom: 20px; left: calc(var(--vt-mh) + var(--vt-mg)); right: 0; pointer-events: none; z-index: 3; }
|
||||||
.vt-app .vt-mult-x { position: relative; height: 20px; margin-left: calc(var(--vt-mh) + var(--vt-mg)); }
|
.vt-app .vt-mult-x { position: relative; height: 20px; margin-left: calc(var(--vt-mh) + var(--vt-mg)); }
|
||||||
.vt-app .vt-mx { position: absolute; top: 5px; font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; transform: translateX(-50%); }
|
.vt-app .vt-mx { position: absolute; top: 5px; font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; transform: translateX(-50%); }
|
||||||
.vt-app .vt-mx-0 { left: 0; transform: none; }
|
.vt-app .vt-mx-0 { left: 0; transform: none; }
|
||||||
.vt-app .vt-mx-1 { right: 0; transform: none; }
|
.vt-app .vt-mx-1 { right: 0; transform: none; }
|
||||||
.vt-app .vt-mult-wrap { cursor: crosshair; touch-action: pan-y; }
|
.vt-app .vt-mult-wrap { cursor: crosshair; touch-action: pan-y; }
|
||||||
@@ -753,11 +768,11 @@ h2, h3{
|
|||||||
width: max-content; min-width: 180px; padding: 8px 10px 7px; border-radius: var(--vt-r);
|
width: max-content; min-width: 180px; padding: 8px 10px 7px; border-radius: var(--vt-r);
|
||||||
background: var(--vt-surface); border: 1px solid var(--vt-line-2);
|
background: var(--vt-surface); border: 1px solid var(--vt-line-2);
|
||||||
box-shadow: 0 10px 28px rgba(12, 14, 17, .16), 0 2px 6px rgba(12, 14, 17, .06);
|
box-shadow: 0 10px 28px rgba(12, 14, 17, .16), 0 2px 6px rgba(12, 14, 17, .06);
|
||||||
font-size: 12px; pointer-events: none;
|
font-size: var(--vt-fs-sm); pointer-events: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-tip-time { display: flex; justify-content: space-between; gap: 12px; color: var(--vt-text-3); font-size: 11px; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--vt-line); }
|
.vt-app .vt-tip-time { display: flex; justify-content: space-between; gap: 12px; color: var(--vt-text-3); font-size: var(--vt-fs-micro); margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--vt-line); }
|
||||||
.vt-app .vt-tip-link + .vt-tip-link { margin-top: 5px; }
|
.vt-app .vt-tip-link + .vt-tip-link { margin-top: 5px; }
|
||||||
.vt-app .vt-tip-label { color: var(--vt-text-3); font-size: 11px; white-space: nowrap; }
|
.vt-app .vt-tip-label { color: var(--vt-text-3); font-size: var(--vt-fs-micro); white-space: nowrap; }
|
||||||
.vt-app .vt-tip-row { display: grid; grid-template-columns: 12px auto minmax(0, 1fr); align-items: center; column-gap: 8px; line-height: 1.6; color: var(--vt-text-2); }
|
.vt-app .vt-tip-row { display: grid; grid-template-columns: 12px auto minmax(0, 1fr); align-items: center; column-gap: 8px; line-height: 1.6; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-tip-row b { font-weight: 700; color: var(--vt-text); white-space: nowrap; text-align: right; }
|
.vt-app .vt-tip-row b { font-weight: 700; color: var(--vt-text); white-space: nowrap; text-align: right; }
|
||||||
|
|
||||||
@@ -772,36 +787,36 @@ h2, h3{
|
|||||||
.vt-app .vt-radio::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--vt-bc); opacity: .85; }
|
.vt-app .vt-radio::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--vt-bc); opacity: .85; }
|
||||||
.vt-app .vt-radio-off { opacity: .75; }
|
.vt-app .vt-radio-off { opacity: .75; }
|
||||||
.vt-app .vt-radio-head { display: flex; align-items: center; gap: 8px; }
|
.vt-app .vt-radio-head { display: flex; align-items: center; gap: 8px; }
|
||||||
.vt-app .vt-radio-name { font-weight: 650; font-size: 13px; }
|
.vt-app .vt-radio-name { font-weight: 650; font-size: var(--vt-fs-base); }
|
||||||
.vt-app .vt-status { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); }
|
.vt-app .vt-status { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--vt-ok); }
|
.vt-app .vt-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--vt-ok); }
|
||||||
.vt-app .vt-status-warn::before { background: var(--vt-warn); }
|
.vt-app .vt-status-warn::before { background: var(--vt-warn); }
|
||||||
.vt-app .vt-status-err::before { background: var(--vt-err); }
|
.vt-app .vt-status-err::before { background: var(--vt-err); }
|
||||||
.vt-app .vt-icon-action {
|
.vt-app .vt-icon-action {
|
||||||
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
||||||
width: 26px; height: 26px; padding: 0; margin: 0; border: 0; border-radius: 7px;
|
width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0; border: 0; border-radius: var(--vt-r-ctl);
|
||||||
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
||||||
}
|
}
|
||||||
.vt-app .vt-icon-action:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
.vt-app .vt-icon-action:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
||||||
.vt-app .vt-radio-chan { display: flex; align-items: baseline; gap: 8px; }
|
.vt-app .vt-radio-chan { display: flex; align-items: baseline; gap: 8px; }
|
||||||
.vt-app .vt-radio-width { font-size: 14px; font-weight: 600; color: var(--vt-text-2); }
|
.vt-app .vt-radio-width { font-size: var(--vt-fs-md); font-weight: 600; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-spectrum { position: relative; height: 8px; border-radius: 4px; background: var(--vt-surface-3); margin-bottom: 12px; }
|
.vt-app .vt-spectrum { position: relative; height: 8px; border-radius: 4px; background: var(--vt-surface-3); margin-bottom: 12px; }
|
||||||
.vt-app .vt-spectrum-blk { position: absolute; top: -2px; bottom: -2px; border-radius: 3px; background: var(--vt-bc); box-shadow: 0 0 0 2px var(--vt-surface); }
|
.vt-app .vt-spectrum-blk { position: absolute; top: -2px; bottom: -2px; border-radius: 3px; background: var(--vt-bc); box-shadow: 0 0 0 2px var(--vt-surface); }
|
||||||
.vt-app .vt-spectrum-lo, .vt-app .vt-spectrum-hi { position: absolute; top: 11px; font-size: 10px; color: var(--vt-text-3); }
|
.vt-app .vt-spectrum-lo, .vt-app .vt-spectrum-hi { position: absolute; top: 11px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); }
|
||||||
.vt-app .vt-spectrum-lo { left: 0; }
|
.vt-app .vt-spectrum-lo { left: 0; }
|
||||||
.vt-app .vt-spectrum-hi { right: 0; }
|
.vt-app .vt-spectrum-hi { right: 0; }
|
||||||
.vt-app .vt-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
|
.vt-app .vt-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
|
||||||
.vt-app .vt-kv dt { font-size: 11px; color: var(--vt-text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
.vt-app .vt-kv dt { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
.vt-app .vt-kv dd { font-size: 13.5px; font-weight: 600; margin: 1px 0 0; }
|
.vt-app .vt-kv dd { font-size: var(--vt-fs-base); font-weight: 600; margin: 1px 0 0; }
|
||||||
.vt-app .vt-kv dd .vt-muted { font-weight: 500; font-size: 12.5px; }
|
.vt-app .vt-kv dd .vt-muted { font-weight: 500; font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-radio-live { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: center; padding: 8px 10px; border-radius: var(--vt-r-sm); background: var(--vt-surface-2); }
|
.vt-app .vt-radio-live { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: center; padding: 8px 10px; border-radius: var(--vt-r-sm); background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-radio-live .vt-spark { height: 30px; }
|
.vt-app .vt-radio-live .vt-spark { height: 30px; }
|
||||||
.vt-app .vt-rates { display: flex; flex-direction: column; gap: 1px; }
|
.vt-app .vt-rates { display: flex; flex-direction: column; gap: 1px; }
|
||||||
.vt-app .vt-rate { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 650; white-space: nowrap; }
|
.vt-app .vt-rate { display: inline-flex; align-items: center; gap: 4px; font-size: var(--vt-fs-sm); font-weight: 650; white-space: nowrap; }
|
||||||
.vt-app .vt-rate .vt-ico { width: 12px; height: 12px; stroke-width: 2.3; color: var(--vt-text-3); }
|
.vt-app .vt-rate .vt-ico { width: 12px; height: 12px; stroke-width: 2.3; color: var(--vt-text-3); }
|
||||||
.vt-app .vt-rate-up { font-weight: 500; color: var(--vt-text-2); }
|
.vt-app .vt-rate-up { font-weight: 500; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-notes { display: flex; flex-direction: column; gap: 4px; }
|
.vt-app .vt-notes { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.vt-app .vt-note { display: flex; align-items: flex-start; gap: 6px; font-size: 12px; color: var(--vt-text-2); }
|
.vt-app .vt-note { display: flex; align-items: flex-start; gap: 6px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
|
||||||
.vt-app .vt-note .vt-ico { margin-top: 2px; color: var(--vt-text-3); }
|
.vt-app .vt-note .vt-ico { margin-top: 2px; color: var(--vt-text-3); }
|
||||||
.vt-app .vt-note-warn .vt-ico { color: var(--vt-warn); }
|
.vt-app .vt-note-warn .vt-ico { color: var(--vt-warn); }
|
||||||
.vt-app .vt-note-err .vt-ico { color: var(--vt-err); }
|
.vt-app .vt-note-err .vt-ico { color: var(--vt-err); }
|
||||||
@@ -815,38 +830,38 @@ h2, h3{
|
|||||||
/* ----------------------------------------------------------- 9. clients */
|
/* ----------------------------------------------------------- 9. clients */
|
||||||
|
|
||||||
.vt-app .vt-clients { padding-bottom: 8px; }
|
.vt-app .vt-clients { padding-bottom: 8px; }
|
||||||
.vt-app .vt-count { font-size: 12px; color: var(--vt-text-3); font-weight: 560; }
|
.vt-app .vt-count { font-size: var(--vt-fs-sm); color: var(--vt-text-3); font-weight: 560; }
|
||||||
.vt-app .vt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; }
|
.vt-app .vt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||||
.vt-app .vt-filter {
|
.vt-app .vt-filter {
|
||||||
position: relative; display: flex; align-items: center; flex: 1 1 260px; max-width: 380px;
|
position: relative; display: flex; align-items: center; flex: 1 1 260px; max-width: 380px;
|
||||||
height: 34px; padding: 0 8px 0 10px; gap: 6px;
|
height: var(--vt-ctl); padding: 0 8px 0 10px; gap: 6px;
|
||||||
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface);
|
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface);
|
||||||
color: var(--vt-text-3); cursor: text;
|
color: var(--vt-text-3); cursor: text;
|
||||||
}
|
}
|
||||||
.vt-app .vt-filter:focus-within { border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
|
.vt-app .vt-filter:focus-within { border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
|
||||||
.vt-app .vt-filter .vt-ico { width: 16px; height: 16px; }
|
.vt-app .vt-filter .vt-ico { width: 16px; height: 16px; }
|
||||||
.vt-app .vt-filter .vt-filter-input {
|
.vt-app .vt-filter .vt-filter-input {
|
||||||
flex: 1; min-width: 0; width: auto; height: 32px; padding: 0; margin: 0;
|
flex: 1; min-width: 0; width: auto; height: calc(var(--vt-ctl) - 2px); padding: 0; margin: 0;
|
||||||
border: 0; background: transparent; box-shadow: none; outline: none;
|
border: 0; background: transparent; box-shadow: none; outline: none;
|
||||||
color: var(--vt-text); font: inherit; font-size: 13px;
|
color: var(--vt-text); font: inherit; font-size: var(--vt-fs-base);
|
||||||
}
|
}
|
||||||
.vt-app .vt-filter .vt-filter-input::placeholder { color: var(--vt-text-3); opacity: 1; }
|
.vt-app .vt-filter .vt-filter-input::placeholder { color: var(--vt-text-3); opacity: 1; }
|
||||||
.vt-app .vt-key-hint { font: 600 10.5px/1 var(--vt-mono); padding: 3px 5px; border-radius: 4px; border: 1px solid var(--vt-line-2); color: var(--vt-text-3); background: var(--vt-surface-2); }
|
.vt-app .vt-key-hint { font: 600 var(--vt-fs-micro)/1 var(--vt-mono); padding: 3px 5px; border-radius: 4px; border: 1px solid var(--vt-line-2); color: var(--vt-text-3); background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-filter:focus-within .vt-key-hint { display: none; }
|
.vt-app .vt-filter:focus-within .vt-key-hint { display: none; }
|
||||||
.vt-app .vt-filter-clear {
|
.vt-app .vt-filter-clear {
|
||||||
display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; margin: 0;
|
display: inline-flex; align-items: center; justify-content: center; width: calc(var(--vt-ctl) - 10px); height: calc(var(--vt-ctl) - 10px); padding: 0; margin: 0;
|
||||||
border: 0; border-radius: 5px; background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
border: 0; border-radius: var(--vt-r-ctl); background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
|
||||||
}
|
}
|
||||||
.vt-app .vt-filter-clear:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
.vt-app .vt-filter-clear:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
||||||
.vt-app .vt-filter-clear .vt-ico { width: 14px; height: 14px; }
|
.vt-app .vt-filter-clear .vt-ico { width: 14px; height: 14px; }
|
||||||
.vt-app .vt-fchips { display: flex; flex-wrap: wrap; gap: 6px; }
|
.vt-app .vt-fchips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
.vt-app .vt-fchip {
|
.vt-app .vt-fchip {
|
||||||
display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; margin: 0;
|
display: inline-flex; align-items: center; gap: 6px; height: var(--vt-ctl); padding: 0 12px; margin: 0;
|
||||||
border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface);
|
border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface);
|
||||||
color: var(--vt-text-2); font-size: 12px; font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none;
|
color: var(--vt-text-2); font-size: var(--vt-fs-sm); font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-fchip:hover { border-color: var(--vt-line-2); color: var(--vt-text); background: var(--vt-surface-2); }
|
.vt-app .vt-fchip:hover { border-color: var(--vt-line-2); color: var(--vt-text); background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-fchip-n { font-size: 11px; color: var(--vt-text-3); font-weight: 600; }
|
.vt-app .vt-fchip-n { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; }
|
||||||
.vt-app .vt-fchip-warn .vt-fchip-n { color: var(--vt-warn); }
|
.vt-app .vt-fchip-warn .vt-fchip-n { color: var(--vt-warn); }
|
||||||
.vt-app .vt-fchip.vt-fchip-on { background: var(--vt-text); border-color: var(--vt-text); color: var(--vt-surface); }
|
.vt-app .vt-fchip.vt-fchip-on { background: var(--vt-text); border-color: var(--vt-text); color: var(--vt-surface); }
|
||||||
.vt-app .vt-fchip.vt-fchip-on .vt-fchip-n { color: color-mix(in oklab, var(--vt-surface) 70%, transparent); }
|
.vt-app .vt-fchip.vt-fchip-on .vt-fchip-n { color: color-mix(in oklab, var(--vt-surface) 70%, transparent); }
|
||||||
@@ -855,18 +870,18 @@ h2, h3{
|
|||||||
.vt-app .vt-ctable { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: auto; min-width: 800px; }
|
.vt-app .vt-ctable { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: auto; min-width: 800px; }
|
||||||
.vt-app .vt-ctable th {
|
.vt-app .vt-ctable th {
|
||||||
position: sticky; top: 0; z-index: 1;
|
position: sticky; top: 0; z-index: 1;
|
||||||
padding: 0 10px; height: 34px; text-align: left; white-space: nowrap; letter-spacing: normal; text-transform: none;
|
padding: 0 10px; height: var(--vt-ctl); text-align: left; white-space: nowrap; letter-spacing: normal; text-transform: none;
|
||||||
background: var(--vt-surface); border-bottom: 1px solid var(--vt-line);
|
background: var(--vt-surface); border-bottom: 1px solid var(--vt-line);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.vt-app .vt-sort {
|
.vt-app .vt-sort {
|
||||||
display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 4px; margin: 0 -4px;
|
display: inline-flex; align-items: center; gap: 4px; height: var(--vt-ctl); padding: 0 4px; margin: 0 -4px;
|
||||||
border: 0; border-radius: 5px; background: transparent; box-shadow: none; min-width: 0;
|
border: 0; border-radius: var(--vt-r-ctl); background: transparent; box-shadow: none; min-width: 0;
|
||||||
color: var(--vt-text-3); font-size: 11px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; cursor: pointer;
|
color: var(--vt-text-3); font-size: var(--vt-fs-micro); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; cursor: pointer;
|
||||||
}
|
}
|
||||||
.vt-app .vt-sort:hover { color: var(--vt-text); background: var(--vt-surface-2); }
|
.vt-app .vt-sort:hover { color: var(--vt-text); background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-sort-on { color: var(--vt-text); }
|
.vt-app .vt-sort-on { color: var(--vt-text); }
|
||||||
.vt-app .vt-sort-ind { font-size: 11px; width: 8px; }
|
.vt-app .vt-sort-ind { font-size: var(--vt-fs-micro); width: 8px; }
|
||||||
.vt-app .vt-ctable td {
|
.vt-app .vt-ctable td {
|
||||||
height: auto; line-height: 1.4; overflow-wrap: normal; text-align: left;
|
height: auto; line-height: 1.4; overflow-wrap: normal; text-align: left;
|
||||||
padding: 9px 8px; vertical-align: middle;
|
padding: 9px 8px; vertical-align: middle;
|
||||||
@@ -888,36 +903,36 @@ h2, h3{
|
|||||||
.vt-app .vt-who-text { min-width: 0; }
|
.vt-app .vt-who-text { min-width: 0; }
|
||||||
/* names get two lines before they are cut; the full name is the title */
|
/* names get two lines before they are cut; the full name is the title */
|
||||||
.vt-app .vt-who-label {
|
.vt-app .vt-who-label {
|
||||||
font-weight: 640; font-size: 13.5px; line-height: 1.3; max-width: 280px; overflow-wrap: anywhere;
|
font-weight: 640; font-size: var(--vt-fs-base); line-height: 1.3; max-width: 280px; overflow-wrap: anywhere;
|
||||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
}
|
}
|
||||||
.vt-app .vt-who-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-top: 3px; color: var(--vt-text-3); font-size: 11.5px; }
|
.vt-app .vt-who-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-top: 3px; color: var(--vt-text-3); font-size: var(--vt-fs-micro); }
|
||||||
.vt-app .vt-who-id { white-space: nowrap; }
|
.vt-app .vt-who-id { white-space: nowrap; }
|
||||||
.vt-app .vt-td-name { min-width: 250px; }
|
.vt-app .vt-td-name { min-width: 250px; }
|
||||||
.vt-app .vt-net { display: flex; flex-direction: column; gap: 3px; }
|
.vt-app .vt-net { display: flex; flex-direction: column; gap: 3px; }
|
||||||
.vt-app .vt-net-ssid { font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
.vt-app .vt-net-ssid { font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
||||||
.vt-app .vt-net-badges { display: flex; align-items: center; gap: 6px; }
|
.vt-app .vt-net-badges { display: flex; align-items: center; gap: 6px; }
|
||||||
.vt-app .vt-phy { display: flex; align-items: center; gap: 4px; font-size: 12.5px; white-space: nowrap; }
|
.vt-app .vt-phy { display: flex; align-items: center; gap: 4px; font-size: var(--vt-fs-sm); white-space: nowrap; }
|
||||||
.vt-app .vt-phy b { font-weight: 620; }
|
.vt-app .vt-phy b { font-weight: 620; }
|
||||||
.vt-app .vt-phy .vt-ico { color: var(--vt-text-3); stroke-width: 2.2; width: 12px; height: 12px; }
|
.vt-app .vt-phy .vt-ico { color: var(--vt-text-3); stroke-width: 2.2; width: 12px; height: 12px; }
|
||||||
.vt-app .vt-phy-up { color: var(--vt-text-3); font-size: 11.5px; padding-left: 16px; }
|
.vt-app .vt-phy-up { color: var(--vt-text-3); font-size: var(--vt-fs-micro); padding-left: 16px; }
|
||||||
.vt-app .vt-live-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; }
|
.vt-app .vt-live-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; }
|
||||||
.vt-app .vt-live-total { font-weight: 650; }
|
.vt-app .vt-live-total { font-weight: 650; }
|
||||||
.vt-app .vt-mini { display: block; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
|
.vt-app .vt-mini { display: block; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
|
||||||
.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; transition: width .6s ease; }
|
.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; transition: width .6s ease; }
|
||||||
.vt-app .vt-live-split { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
|
.vt-app .vt-live-split { font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
|
||||||
.vt-app .vt-exp { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
.vt-app .vt-exp { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||||
.vt-app .vt-score {
|
.vt-app .vt-score {
|
||||||
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
||||||
width: 32px; height: 26px; border-radius: 7px; font-size: 12.5px; font-weight: 700;
|
width: 32px; height: 26px; border-radius: 7px; font-size: var(--vt-fs-sm); font-weight: 700;
|
||||||
--vt-xc: var(--vt-ok); background: color-mix(in oklab, var(--vt-xc) 15%, transparent); color: color-mix(in oklab, var(--vt-xc) 85%, var(--vt-text));
|
--vt-xc: var(--vt-ok); background: color-mix(in oklab, var(--vt-xc) 15%, transparent); color: color-mix(in oklab, var(--vt-xc) 85%, var(--vt-text));
|
||||||
}
|
}
|
||||||
.vt-app .vt-score-warn { --vt-xc: var(--vt-warn); }
|
.vt-app .vt-score-warn { --vt-xc: var(--vt-warn); }
|
||||||
.vt-app .vt-score-err { --vt-xc: var(--vt-err); }
|
.vt-app .vt-score-err { --vt-xc: var(--vt-err); }
|
||||||
.vt-app .vt-exp-text { display: flex; flex-direction: column; min-width: 0; }
|
.vt-app .vt-exp-text { display: flex; flex-direction: column; min-width: 0; }
|
||||||
.vt-app .vt-exp-text b { font-size: 12.5px; font-weight: 650; }
|
.vt-app .vt-exp-text b { font-size: var(--vt-fs-sm); font-weight: 650; }
|
||||||
.vt-app .vt-exp-text span {
|
.vt-app .vt-exp-text span {
|
||||||
font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); max-width: 170px;
|
font-size: var(--vt-fs-micro); line-height: 1.35; color: var(--vt-text-3); max-width: 170px;
|
||||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
}
|
}
|
||||||
.vt-app .vt-td-time { white-space: nowrap; color: var(--vt-text-2); }
|
.vt-app .vt-td-time { white-space: nowrap; color: var(--vt-text-2); }
|
||||||
@@ -935,15 +950,15 @@ h2, h3{
|
|||||||
text-align: left; cursor: pointer; color: var(--vt-text); height: auto; min-width: 0; text-transform: none; font-weight: 400;
|
text-align: left; cursor: pointer; color: var(--vt-text); height: auto; min-width: 0; text-transform: none; font-weight: 400;
|
||||||
}
|
}
|
||||||
.vt-app .vt-talk-row:hover, .vt-app .vt-ssid:hover, .vt-app .vt-dlist-row:hover { background: var(--vt-surface-2); }
|
.vt-app .vt-talk-row:hover, .vt-app .vt-ssid:hover, .vt-app .vt-dlist-row:hover { background: var(--vt-surface-2); }
|
||||||
.vt-app .vt-talk-rank { width: 14px; font-size: 12px; color: var(--vt-text-3); font-weight: 650; text-align: center; flex: none; }
|
.vt-app .vt-talk-rank { width: 14px; font-size: var(--vt-fs-sm); color: var(--vt-text-3); font-weight: 650; text-align: center; flex: none; }
|
||||||
.vt-app .vt-talk-main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
|
.vt-app .vt-talk-main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
|
||||||
.vt-app .vt-talk-top { display: flex; justify-content: space-between; gap: 10px; }
|
.vt-app .vt-talk-top { display: flex; justify-content: space-between; gap: 10px; }
|
||||||
.vt-app .vt-talk-name { font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
.vt-app .vt-talk-name { font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
.vt-app .vt-talk-share { font-size: 12px; font-weight: 650; color: var(--vt-text-2); white-space: nowrap; }
|
.vt-app .vt-talk-share { font-size: var(--vt-fs-sm); font-weight: 650; color: var(--vt-text-2); white-space: nowrap; }
|
||||||
.vt-app .vt-talk-sub b { color: var(--vt-text); font-weight: 650; }
|
.vt-app .vt-talk-sub b { color: var(--vt-text); font-weight: 650; }
|
||||||
.vt-app .vt-talk-bar { height: 5px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
|
.vt-app .vt-talk-bar { height: 5px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
|
||||||
.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); transition: width .6s ease; }
|
.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); transition: width .6s ease; }
|
||||||
.vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; overflow: hidden; }
|
.vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; overflow: hidden; }
|
||||||
|
|
||||||
.vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; }
|
.vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.vt-app .vt-ssid-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); }
|
.vt-app .vt-ssid-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); }
|
||||||
@@ -953,9 +968,9 @@ h2, h3{
|
|||||||
.vt-app .vt-ssid-top { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
.vt-app .vt-ssid-top { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||||
.vt-app .vt-ssid-name { font-weight: 640; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-ssid-name { font-weight: 640; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-app .vt-ssid-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
|
.vt-app .vt-ssid-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
|
||||||
.vt-app .vt-sec { font-size: 11px; font-weight: 600; color: var(--vt-text-2); }
|
.vt-app .vt-sec { font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-ssid-side { display: flex; flex-direction: column; align-items: flex-end; flex: none; }
|
.vt-app .vt-ssid-side { display: flex; flex-direction: column; align-items: flex-end; flex: none; }
|
||||||
.vt-app .vt-ssid-n { font-size: 17px; font-weight: 700; line-height: 1.1; }
|
.vt-app .vt-ssid-n { font-size: var(--vt-fs-lg); font-weight: 700; line-height: 1.1; }
|
||||||
.vt-app .vt-ssid-idle .vt-ssid-n { color: var(--vt-text-3); }
|
.vt-app .vt-ssid-idle .vt-ssid-n { color: var(--vt-text-3); }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ 11. drawer */
|
/* ------------------------------------------------------------ 11. drawer */
|
||||||
@@ -988,11 +1003,11 @@ h2, h3{
|
|||||||
.vt-app .vt-drawer-ico { width: 44px; height: 44px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); flex: none; }
|
.vt-app .vt-drawer-ico { width: 44px; height: 44px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); flex: none; }
|
||||||
.vt-app .vt-drawer-ico .vt-ico { width: 22px; height: 22px; }
|
.vt-app .vt-drawer-ico .vt-ico { width: 22px; height: 22px; }
|
||||||
.vt-app .vt-drawer-titles { min-width: 0; }
|
.vt-app .vt-drawer-titles { min-width: 0; }
|
||||||
.vt-app .vt-drawer-name { font-size: 17px; font-weight: 680; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; }
|
.vt-app .vt-drawer-name { font-size: var(--vt-fs-lg); font-weight: 680; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; }
|
||||||
.vt-app .vt-drawer-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; color: var(--vt-text-3); font-size: 12px; margin-top: 3px; }
|
.vt-app .vt-drawer-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 3px; }
|
||||||
.vt-app .vt-drawer-close {
|
.vt-app .vt-drawer-close {
|
||||||
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
||||||
width: 34px; height: 34px; padding: 0; margin: 0; border: 0; border-radius: 9px;
|
width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0; border: 0; border-radius: var(--vt-r-ctl);
|
||||||
background: transparent; color: var(--vt-text-2); cursor: pointer; box-shadow: none; min-width: 0;
|
background: transparent; color: var(--vt-text-2); cursor: pointer; box-shadow: none; min-width: 0;
|
||||||
}
|
}
|
||||||
.vt-app .vt-drawer-close:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
.vt-app .vt-drawer-close:hover { background: var(--vt-surface-3); color: var(--vt-text); }
|
||||||
@@ -1000,7 +1015,7 @@ h2, h3{
|
|||||||
.vt-app .vt-dactions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
.vt-app .vt-dactions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||||||
.vt-app .vt-dactions:has(> .vt-link:only-child) { justify-content: flex-end; }
|
.vt-app .vt-dactions:has(> .vt-link:only-child) { justify-content: flex-end; }
|
||||||
.vt-app .vt-rename-slot:empty { display: none; }
|
.vt-app .vt-rename-slot:empty { display: none; }
|
||||||
.vt-app .vt-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: var(--vt-r-sm); font-size: 12.5px; }
|
.vt-app .vt-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: var(--vt-r-sm); font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-banner-muted { background: var(--vt-surface-3); color: var(--vt-text-2); }
|
.vt-app .vt-banner-muted { background: var(--vt-surface-3); color: var(--vt-text-2); }
|
||||||
|
|
||||||
.vt-app .vt-expcard {
|
.vt-app .vt-expcard {
|
||||||
@@ -1015,52 +1030,52 @@ h2, h3{
|
|||||||
.vt-app .vt-ring-svg { width: 58px; height: 58px; transform: rotate(-90deg); }
|
.vt-app .vt-ring-svg { width: 58px; height: 58px; transform: rotate(-90deg); }
|
||||||
.vt-app .vt-ring-track { fill: none; stroke: color-mix(in oklab, var(--vt-xc) 18%, transparent); stroke-width: 3.5; }
|
.vt-app .vt-ring-track { fill: none; stroke: color-mix(in oklab, var(--vt-xc) 18%, transparent); stroke-width: 3.5; }
|
||||||
.vt-app .vt-ring-arc { fill: none; stroke: var(--vt-xc); stroke-width: 3.5; stroke-linecap: round; }
|
.vt-app .vt-ring-arc { fill: none; stroke: var(--vt-xc); stroke-width: 3.5; stroke-linecap: round; }
|
||||||
.vt-app .vt-ring-n { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 720; }
|
.vt-app .vt-ring-n { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: var(--vt-fs-lg); font-weight: 720; }
|
||||||
.vt-app .vt-expcard-grade { font-weight: 680; font-size: 14px; }
|
.vt-app .vt-expcard-grade { font-weight: 680; font-size: var(--vt-fs-md); }
|
||||||
.vt-app .vt-expcard-reason { font-size: 12.5px; color: var(--vt-text-2); margin-top: 1px; }
|
.vt-app .vt-expcard-reason { font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 1px; }
|
||||||
.vt-app .vt-factors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--vt-text-3); }
|
.vt-app .vt-factors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); }
|
||||||
.vt-app .vt-factors li { display: flex; gap: 6px; }
|
.vt-app .vt-factors li { display: flex; gap: 6px; }
|
||||||
.vt-app .vt-factors li .vt-tn { width: 26px; flex: none; color: var(--vt-text-2); font-weight: 650; }
|
.vt-app .vt-factors li .vt-tn { width: 26px; flex: none; color: var(--vt-text-2); font-weight: 650; }
|
||||||
|
|
||||||
.vt-app .vt-dgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
.vt-app .vt-dgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
.vt-app .vt-dcard { display: flex; flex-direction: column; gap: 1px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); min-width: 0; }
|
.vt-app .vt-dcard { display: flex; flex-direction: column; gap: 1px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); min-width: 0; }
|
||||||
.vt-app .vt-dcard-label { font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--vt-text-3); }
|
.vt-app .vt-dcard-label { font-size: var(--vt-fs-micro); font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--vt-text-3); }
|
||||||
.vt-app .vt-dcard-value { font-size: 20px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
|
.vt-app .vt-dcard-value { font-size: var(--vt-fs-xl); font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
|
||||||
.vt-app .vt-dcard-sub { font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); min-height: 16px; }
|
.vt-app .vt-dcard-sub { font-size: var(--vt-fs-micro); line-height: 1.35; color: var(--vt-text-3); min-height: 16px; }
|
||||||
.vt-app .vt-dcard .vt-sk { margin-top: auto; padding-top: 18px; }
|
.vt-app .vt-dcard .vt-sk { margin-top: auto; padding-top: 18px; }
|
||||||
.vt-app .vt-dcard .vt-sk-max { top: 16px; }
|
.vt-app .vt-dcard .vt-sk-max { top: 16px; }
|
||||||
.vt-app .vt-dcard .vt-spark { height: 36px; }
|
.vt-app .vt-dcard .vt-spark { height: 36px; }
|
||||||
.vt-app .vt-dspark { padding-top: 14px; }
|
.vt-app .vt-dspark { padding-top: 14px; }
|
||||||
.vt-app .vt-dspark .vt-spark { height: 48px; }
|
.vt-app .vt-dspark .vt-spark { height: 48px; }
|
||||||
.vt-app .vt-dsec-title { font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--vt-text-3); margin: 0 0 6px; padding: 0; border: 0; }
|
.vt-app .vt-dsec-title { font-size: var(--vt-fs-micro); font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--vt-text-3); margin: 0 0 6px; padding: 0; border: 0; }
|
||||||
.vt-app .vt-kvs { display: flex; flex-direction: column; }
|
.vt-app .vt-kvs { display: flex; flex-direction: column; }
|
||||||
.vt-app .vt-kvs .vt-kv { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--vt-line); }
|
.vt-app .vt-kvs .vt-kv { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--vt-line); }
|
||||||
.vt-app .vt-kvs .vt-kv:last-child { border-bottom: 0; }
|
.vt-app .vt-kvs .vt-kv:last-child { border-bottom: 0; }
|
||||||
.vt-app .vt-kvs .vt-kv dt { font-size: 12px; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--vt-text-3); }
|
.vt-app .vt-kvs .vt-kv dt { font-size: var(--vt-fs-sm); text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--vt-text-3); }
|
||||||
.vt-app .vt-kvs .vt-kv dd { font-size: 13px; font-weight: 520; margin: 0; white-space: pre-line; overflow-wrap: anywhere; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
|
.vt-app .vt-kvs .vt-kv dd { font-size: var(--vt-fs-base); font-weight: 520; margin: 0; white-space: pre-line; overflow-wrap: anywhere; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
|
||||||
.vt-app .vt-kvs .vt-kv dd small { font-size: 11.5px; color: var(--vt-text-3); font-weight: 400; }
|
.vt-app .vt-kvs .vt-kv dd small { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 400; }
|
||||||
.vt-app .vt-dlist { display: flex; flex-direction: column; }
|
.vt-app .vt-dlist { display: flex; flex-direction: column; }
|
||||||
.vt-app .vt-dlist-name { flex: 1; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vt-app .vt-dlist-name { flex: 1; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
.vt-app .vt-rename { display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); }
|
.vt-app .vt-rename { display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); }
|
||||||
.vt-app .vt-rename-label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; font-weight: 600; color: var(--vt-text-2); }
|
.vt-app .vt-rename-label { display: flex; flex-direction: column; gap: 5px; font-size: var(--vt-fs-sm); font-weight: 600; color: var(--vt-text-2); }
|
||||||
.vt-app .vt-rename .vt-rename-input {
|
.vt-app .vt-rename .vt-rename-input {
|
||||||
height: 36px; width: 100%; padding: 0 10px; margin: 0;
|
height: var(--vt-ctl); width: 100%; padding: 0 10px; margin: 0;
|
||||||
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface);
|
border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface);
|
||||||
color: var(--vt-text); font: inherit; font-size: 14px; box-shadow: none;
|
color: var(--vt-text); font: inherit; font-size: var(--vt-fs-md); box-shadow: none;
|
||||||
}
|
}
|
||||||
.vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
|
.vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
|
||||||
.vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; }
|
.vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
.vt-app .vt-icon-pick {
|
.vt-app .vt-icon-pick {
|
||||||
display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; margin: 0;
|
display: inline-flex; align-items: center; justify-content: center; width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0;
|
||||||
border: 1px solid var(--vt-line); border-radius: 8px; background: var(--vt-surface); color: var(--vt-text-2);
|
border: 1px solid var(--vt-line); border-radius: var(--vt-r-ctl); background: var(--vt-surface); color: var(--vt-text-2);
|
||||||
cursor: pointer; box-shadow: none; min-width: 0;
|
cursor: pointer; box-shadow: none; min-width: 0;
|
||||||
}
|
}
|
||||||
.vt-app .vt-icon-pick:hover { border-color: var(--vt-line-2); color: var(--vt-text); }
|
.vt-app .vt-icon-pick:hover { border-color: var(--vt-line-2); color: var(--vt-text); }
|
||||||
.vt-app .vt-icon-pick[aria-pressed="true"] { background: var(--vt-accent); border-color: var(--vt-accent); color: var(--vt-accent-ink); }
|
.vt-app .vt-icon-pick[aria-pressed="true"] { background: var(--vt-accent); border-color: var(--vt-accent); color: var(--vt-accent-ink); }
|
||||||
.vt-app .vt-rename-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
.vt-app .vt-rename-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||||
.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: 11px; }
|
.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: var(--vt-fs-micro); }
|
||||||
.vt-app .vt-form-err { color: var(--vt-err); font-size: 12px; }
|
.vt-app .vt-form-err { color: var(--vt-err); font-size: var(--vt-fs-sm); }
|
||||||
|
|
||||||
/* ------------------------------------------------ 12. small screens, motion */
|
/* ------------------------------------------------ 12. small screens, motion */
|
||||||
|
|
||||||
@@ -1071,7 +1086,7 @@ h2, h3{
|
|||||||
.vt-app .vt-fchips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; }
|
.vt-app .vt-fchips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; }
|
||||||
.vt-app .vt-fchip { flex: none; }
|
.vt-app .vt-fchip { flex: none; }
|
||||||
.vt-app .vt-head { gap: 10px; }
|
.vt-app .vt-head { gap: 10px; }
|
||||||
.vt-app .vt-headline { font-size: 18px; }
|
.vt-app .vt-headline { font-size: var(--vt-fs-lg); }
|
||||||
.vt-app .vt-orb { width: 32px; height: 32px; }
|
.vt-app .vt-orb { width: 32px; height: 32px; }
|
||||||
/* phones: a live connection is just the pulsing dot; paused or
|
/* phones: a live connection is just the pulsing dot; paused or
|
||||||
reconnecting still says so in words */
|
reconnecting still says so in words */
|
||||||
@@ -1082,10 +1097,10 @@ h2, h3{
|
|||||||
.vt-app .vt-health { grid-template-columns: 1fr 1fr; gap: 8px; }
|
.vt-app .vt-health { grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||||
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
|
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
|
||||||
.vt-app .vt-chip-ico { width: 22px; height: 22px; }
|
.vt-app .vt-chip-ico { width: 22px; height: 22px; }
|
||||||
.vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: 11.5px; }
|
.vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: var(--vt-fs-micro); }
|
||||||
.vt-app .vt-sys-cpu { grid-template-columns: auto minmax(0, 1fr); }
|
.vt-app .vt-sys-cpu { grid-template-columns: auto minmax(0, 1fr); }
|
||||||
.vt-app .vt-chart { --vt-mh: 108px; --vt-mg: 12px; }
|
.vt-app .vt-chart { --vt-mh: 108px; --vt-mg: 12px; }
|
||||||
.vt-app .vt-mult-now { font-size: 15px; }
|
.vt-app .vt-mult-now { font-size: var(--vt-fs-md); }
|
||||||
.vt-app .vt-mx-odd { display: none; }
|
.vt-app .vt-mx-odd { display: none; }
|
||||||
.vt-app .vt-tip { min-width: 160px; }
|
.vt-app .vt-tip { min-width: 160px; }
|
||||||
}
|
}
|
||||||
@@ -1119,11 +1134,11 @@ h2, h3{
|
|||||||
.vt-app .vt-exp { gap: 8px; }
|
.vt-app .vt-exp { gap: 8px; }
|
||||||
.vt-app .vt-exp-text { flex-direction: row; align-items: baseline; gap: 6px; min-width: 0; }
|
.vt-app .vt-exp-text { flex-direction: row; align-items: baseline; gap: 6px; min-width: 0; }
|
||||||
.vt-app .vt-exp-text b { flex: none; }
|
.vt-app .vt-exp-text b { flex: none; }
|
||||||
.vt-app .vt-score { width: 28px; height: 22px; font-size: 12px; }
|
.vt-app .vt-score { width: 28px; height: 22px; font-size: var(--vt-fs-sm); }
|
||||||
.vt-app .vt-td-name { min-width: 0; }
|
.vt-app .vt-td-name { min-width: 0; }
|
||||||
.vt-app .vt-ctable td.vt-td-rate, .vt-app .vt-ctable td.vt-td-time { display: none; }
|
.vt-app .vt-ctable td.vt-td-rate, .vt-app .vt-ctable td.vt-td-time { display: none; }
|
||||||
.vt-app .vt-who-label { max-width: 100%; }
|
.vt-app .vt-who-label { max-width: 100%; }
|
||||||
.vt-app .vt-net-ssid { font-size: 13px; max-width: none; }
|
.vt-app .vt-net-ssid { font-size: var(--vt-fs-base); max-width: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -1141,6 +1156,11 @@ h2, h3{
|
|||||||
.vt-app .vt-kvs .vt-kv { grid-template-columns: 112px minmax(0, 1fr); }
|
.vt-app .vt-kvs .vt-kv { grid-template-columns: 112px minmax(0, 1fr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* touch: 16px text in fields (iOS zooms into anything smaller) */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.vt-app .vt-filter .vt-filter-input, .vt-app .vt-rename .vt-rename-input { font-size: 16px; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; }
|
.vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; }
|
||||||
.vt-app .vt-flow-on { opacity: .8; stroke-dasharray: 1.5 6; }
|
.vt-app .vt-flow-on { opacity: .8; stroke-dasharray: 1.5 6; }
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ return view.extend({
|
|||||||
dhcp: uci.get('dhcp', 'lan', 'ignore') === '1' ? '0' : '1',
|
dhcp: uci.get('dhcp', 'lan', 'ignore') === '1' ? '0' : '1',
|
||||||
ipv6: uci.get('network', 'wan6', 'auto') === '0' ? '0' : '1',
|
ipv6: uci.get('network', 'wan6', 'auto') === '0' ? '0' : '1',
|
||||||
https: uci.get('wizard', 'default', 'https') || '0',
|
https: uci.get('wizard', 'default', 'https') || '0',
|
||||||
cookie_p: uci.get('luci', 'main', 'persistent_cookies') || '1',
|
persistent_cookies: uci.get('luci', 'main', 'persistent_cookies') || '1',
|
||||||
landing_page: uci.get('luci', 'main', 'landing_page') || 'auto',
|
landing_page: uci.get('luci', 'main', 'landing_page') || 'auto',
|
||||||
autoupgrade_fm: uci.get('luci', 'main', 'autoupgrade_fm') || '0',
|
autoupgrade_fm: uci.get('luci', 'main', 'autoupgrade_fm') || '0',
|
||||||
coremark: uci.get('wizard', 'default', 'coremark') || '0',
|
coremark: uci.get('wizard', 'default', 'coremark') || '0',
|
||||||
@@ -192,7 +192,7 @@ return view.extend({
|
|||||||
// 固件与系统设置
|
// 固件与系统设置
|
||||||
{ tab: 'firmware', type: form.Flag, id: 'autoupgrade_fm', title: _('Firmware Upgrade Notice'), desc: _('Check and display notices for newer firmware versions.') },
|
{ tab: 'firmware', type: form.Flag, id: 'autoupgrade_fm', title: _('Firmware Upgrade Notice'), desc: _('Check and display notices for newer firmware versions.') },
|
||||||
{ tab: 'firmware', type: form.Flag, id: 'coremark', title: _('Run CoreMark on Boot'), desc: _('Run CPU benchmark asynchronously upon router initialization.') },
|
{ tab: 'firmware', type: form.Flag, id: 'coremark', title: _('Run CoreMark on Boot'), desc: _('Run CPU benchmark asynchronously upon router initialization.') },
|
||||||
{ tab: 'firmware', type: form.Flag, id: 'cookie_p', title: _('Persistent Cookie Session'), desc: _('Maintain persistent login sessions in the web browser.') },
|
{ tab: 'firmware', type: form.Flag, id: 'persistent_cookies', title: _('Persistent Cookie Session'), desc: _('Maintain persistent login sessions in the web browser.') },
|
||||||
{ tab: 'firmware', type: form.Flag, id: 'https', title: _('Enforce HTTPS Access'), desc: _('Automatically redirect HTTP requests to secure HTTPS.') },
|
{ tab: 'firmware', type: form.Flag, id: 'https', title: _('Enforce HTTPS Access'), desc: _('Automatically redirect HTTP requests to secure HTTPS.') },
|
||||||
{ tab: 'firmware', type: form.ListValue, id: 'landing_page', title: _('Landing Dashboard Mode'),
|
{ tab: 'firmware', type: form.ListValue, id: 'landing_page', title: _('Landing Dashboard Mode'),
|
||||||
choices: { 'auto': _('Auto'), 'overview': _('Overview'), 'istoreos': _('iStoreOS'), 'dashboard': _('Dashboard'), routerdog: _('RouterDog'), nas: _('NAS'), 'next-nas': _('Next-NAS'), router: _('Router') } }
|
choices: { 'auto': _('Auto'), 'overview': _('Overview'), 'istoreos': _('iStoreOS'), 'dashboard': _('Dashboard'), routerdog: _('RouterDog'), nas: _('NAS'), 'next-nas': _('Next-NAS'), router: _('Router') } }
|
||||||
@@ -480,9 +480,9 @@ return view.extend({
|
|||||||
uci.set('wizard', 'default', 'https', cur.https);
|
uci.set('wizard', 'default', 'https', cur.https);
|
||||||
}
|
}
|
||||||
|
|
||||||
// I. 页面与会话设置(持久Cookie双向同步至luci系统底层与向导配置包)
|
// I. 页面与会话设置
|
||||||
if (has(changed, 'cookie_p')) {
|
if (has(changed, 'persistent_cookies')) {
|
||||||
uci.set('luci', 'main', 'persistent_cookies', cur.cookie_p);
|
uci.set('luci', 'main', 'persistent_cookies', cur.persistent_cookies);
|
||||||
}
|
}
|
||||||
if (has(changed, 'landing_page')) {
|
if (has(changed, 'landing_page')) {
|
||||||
uci.set('luci', 'main', 'landing_page', cur.landing_page);
|
uci.set('luci', 'main', 'landing_page', cur.landing_page);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-theme-vantage
|
PKG_NAME:=luci-theme-vantage
|
||||||
PKG_VERSION:=1.0.3
|
PKG_VERSION:=1.0.4
|
||||||
PKG_RELEASE:=16
|
PKG_RELEASE:=17
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
LUCI_TITLE:=Vantage theme
|
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' } ] ],
|
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' } ] ],
|
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' } ] ],
|
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) {
|
function iconFor(name) {
|
||||||
@@ -180,6 +181,11 @@ function technical(text) {
|
|||||||
return /\d/.test(rest) && /^[\s0-9a-fA-F.:,\/%()+\-x[\]~=]*$/.test(rest);
|
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
|
* Stock-page enhancers, only where CSS cannot restructure LuCI's markup
|
||||||
* (Status -> Overview: fused memory bar, per-mount storage bars, radio band
|
* (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';
|
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() {
|
function setupChannelAnalysis() {
|
||||||
const view = document.getElementById('view');
|
const view = document.getElementById('view');
|
||||||
const own = new Set();
|
const own = new Set();
|
||||||
const slots = new Map();
|
const slots = new Map();
|
||||||
let next = 0;
|
let next = 0;
|
||||||
|
let colourOf = null;
|
||||||
/* slot 1 (the accent) is the AP itself; neighbours take 2..6 in the order
|
/* 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) */
|
they first appear (the view sorts by channel, so neighbours differ) */
|
||||||
const slotFor = (colour, mine) => {
|
const slotFor = (colour, mine) => {
|
||||||
@@ -455,9 +468,17 @@ function setupChannelAnalysis() {
|
|||||||
const dot = tr.querySelector('span[style*="color"]');
|
const dot = tr.querySelector('span[style*="color"]');
|
||||||
const cells = tr.querySelectorAll(':scope > td, :scope > .td');
|
const cells = tr.querySelectorAll(':scope > td, :scope > .td');
|
||||||
if (!cells.length) return;
|
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);
|
const mine = own.has(bssid);
|
||||||
if (tr.classList.contains('v-ca-own') !== mine) tr.classList.toggle('v-ca-own', mine);
|
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;
|
if (!dot) return;
|
||||||
const colour = dot.style.color;
|
const colour = dot.style.color;
|
||||||
if (mine) mineByColour.set(colour, true);
|
if (mine) mineByColour.set(colour, true);
|
||||||
@@ -531,7 +552,15 @@ function setupChannelAnalysis() {
|
|||||||
busy = false;
|
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 r of Object.values(devs || {}))
|
||||||
for (const i of (r && Array.isArray(r.interfaces)) ? r.interfaces : [])
|
for (const i of (r && Array.isArray(r.interfaces)) ? r.interfaces : [])
|
||||||
[ i.iwinfo && i.iwinfo.bssid, i.config && i.config.macaddr ].forEach(b => {
|
[ i.iwinfo && i.iwinfo.bssid, i.config && i.config.macaddr ].forEach(b => {
|
||||||
@@ -558,10 +587,12 @@ return baseclass.extend({
|
|||||||
this.closeTimer = null;
|
this.closeTimer = null;
|
||||||
this.openTimer = null;
|
this.openTimer = null;
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
|
this.trailKeys = new Set();
|
||||||
|
|
||||||
this.setupTheme();
|
this.setupTheme();
|
||||||
this.setupIndicators();
|
this.setupIndicators();
|
||||||
this.setupNumerals();
|
this.setupNumerals();
|
||||||
|
this.setupForms();
|
||||||
setupStockEnhancers();
|
setupStockEnhancers();
|
||||||
setupCharts();
|
setupCharts();
|
||||||
this.setupKeys();
|
this.setupKeys();
|
||||||
@@ -605,6 +636,7 @@ return baseclass.extend({
|
|||||||
this.renderCrumbs();
|
this.renderCrumbs();
|
||||||
this.renderTabs();
|
this.renderTabs();
|
||||||
this.buildIndex();
|
this.buildIndex();
|
||||||
|
this.queueForms();
|
||||||
},
|
},
|
||||||
|
|
||||||
isPinned() {
|
isPinned() {
|
||||||
@@ -861,6 +893,7 @@ return baseclass.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.pageTitle = parts[parts.length - 1].title;
|
this.pageTitle = parts[parts.length - 1].title;
|
||||||
|
parts.forEach(p => this.trailKeys.add(headingKey(p.title)));
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1194,5 +1227,126 @@ return baseclass.extend({
|
|||||||
window.requestAnimationFrame(scan);
|
window.requestAnimationFrame(scan);
|
||||||
}).observe(main, { childList: true, subtree: true, characterData: true });
|
}).observe(main, { childList: true, subtree: true, characterData: true });
|
||||||
scan();
|
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: 56px;
|
||||||
--rail-w-pinned: 204px;
|
--rail-w-pinned: 204px;
|
||||||
--top-h: 52px;
|
--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;
|
--content-max: 1320px;
|
||||||
|
|
||||||
/* light: cool paper grey, olive accent text */
|
/* light: cool paper grey, olive accent text */
|
||||||
@@ -72,6 +74,9 @@
|
|||||||
--scrim: rgba(20, 23, 26, .38);
|
--scrim: rgba(20, 23, 26, .38);
|
||||||
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
|
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
|
||||||
--zone-mix: .22;
|
--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"] {
|
html[data-darkmode="true"] {
|
||||||
@@ -107,6 +112,14 @@ html[data-darkmode="true"] {
|
|||||||
--scrim: rgba(0, 0, 0, .58);
|
--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);
|
--shadow-pop: 0 16px 40px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .02);
|
||||||
--zone-mix: .2;
|
--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
|
/* 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-info-soft: var(--info-soft);
|
||||||
--v-font: var(--font);
|
--v-font: var(--font);
|
||||||
--v-mono: var(--mono);
|
--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-sm: var(--r-sm);
|
||||||
--v-r: var(--r);
|
--v-r: var(--r);
|
||||||
--v-r-lg: var(--r-lg);
|
--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);
|
--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 */
|
/* where sticky content may start: below the top bar and page tabs */
|
||||||
--v-stick-top: var(--stick-top);
|
--v-stick-top: var(--stick-top);
|
||||||
@@ -508,7 +529,7 @@ a.vt-crumb:hover { color: var(--text); }
|
|||||||
font-size: var(--fs-sm);
|
font-size: var(--fs-sm);
|
||||||
white-space: nowrap;
|
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 span { overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vt-host-sep { color: var(--text-3); }
|
.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;
|
min-width: 20px; height: 20px; padding: 0 6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--warn); color: #1a1200;
|
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); }
|
#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 {
|
.vt-tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex; align-items: center;
|
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);
|
color: var(--text-2);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
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
|
/* page = title block + a stack of cards. Cards carry a full-bleed header
|
||||||
row (h3) and full-bleed tables; nested sections lose their chrome. */
|
row (h3) and full-bleed tables; nested sections lose their chrome. */
|
||||||
:root { --card-px: 20px; --card-py: 16px; --stick-top: var(--top-h); }
|
: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 { margin-bottom: var(--sp-6); }
|
||||||
.cbi-map > h2, .vt-main > h2, #view > h2, #view > div > h2:first-child, .cbi-map > h2:first-child {
|
.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;
|
max-width: 76ch;
|
||||||
margin: 0 0 var(--sp-6);
|
margin: 0 0 var(--sp-6);
|
||||||
color: var(--text-2);
|
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-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-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 {
|
.cbi-section, .cbi-map > fieldset {
|
||||||
margin: 0 0 var(--sp-4);
|
margin: 0 0 var(--sp-4);
|
||||||
padding: var(--card-py) var(--card-px);
|
padding: var(--card-py) var(--card-px);
|
||||||
background: var(--s1);
|
background: var(--s1);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--r-lg);
|
||||||
box-shadow: var(--v-shadow);
|
box-shadow: var(--v-shadow);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background-clip: padding-box;
|
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 > 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); }
|
.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; }
|
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); }
|
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 {
|
.cbi-value {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(160px, 260px) minmax(0, 1fr);
|
grid-template-columns: var(--label-w) minmax(0, 1fr);
|
||||||
column-gap: var(--sp-8);
|
column-gap: var(--sp-5);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 14px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
}
|
||||||
.cbi-value:last-child, .cbi-value.cbi-value-last { border-bottom: 0; }
|
|
||||||
.cbi-value > .cbi-value-title {
|
.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);
|
color: var(--text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
overflow-wrap: anywhere;
|
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;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.cbi-value-helpicon img { width: 14px; height: 14px; opacity: .6; }
|
.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.cbi-value-error .cbi-value-title,
|
||||||
.cbi-value-field .cbi-input-invalid + .cbi-value-description { color: var(--err); }
|
.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);
|
backdrop-filter: saturate(1.3) blur(14px);
|
||||||
-webkit-backdrop-filter: saturate(1.3) blur(14px);
|
-webkit-backdrop-filter: saturate(1.3) blur(14px);
|
||||||
border: 1px solid var(--line-2);
|
border: 1px solid var(--line-2);
|
||||||
border-radius: 12px;
|
border-radius: var(--r-lg);
|
||||||
box-shadow: var(--shadow-pop);
|
box-shadow: var(--shadow-pop);
|
||||||
}
|
}
|
||||||
.cbi-page-actions > * { margin: 0 !important; }
|
.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[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 {
|
.vt-main input:not([type]), .modal input[type="text"], .modal input[type="password"], .modal input:not([type]), .modal select, .modal textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 440px;
|
max-width: var(--field-max);
|
||||||
min-height: var(--ctl-h);
|
min-height: var(--ctl-h);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
background: var(--s2);
|
background: var(--s2);
|
||||||
@@ -844,7 +928,7 @@ input, select, textarea, button { font: inherit; color: inherit; }
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
transition: border-color .12s, box-shadow .12s;
|
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 {
|
.vt-main select, .cbi-input-select, .modal select {
|
||||||
appearance: none; -webkit-appearance: none;
|
appearance: none; -webkit-appearance: none;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
@@ -853,6 +937,13 @@ input, select, textarea, button { font: inherit; color: inherit; }
|
|||||||
background-size: 5px 5px, 5px 5px;
|
background-size: 5px 5px, 5px 5px;
|
||||||
background-repeat: no-repeat;
|
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; }
|
.vt-main select[multiple] { background-image: none; padding-right: 10px; }
|
||||||
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 1; }
|
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 1; }
|
||||||
input:hover, select:hover, textarea:hover { border-color: var(--text-3); }
|
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 */
|
/* password + reveal and other control groups */
|
||||||
.control-group {
|
.control-group {
|
||||||
display: flex; align-items: stretch; flex-wrap: nowrap;
|
display: flex; align-items: stretch; flex-wrap: nowrap;
|
||||||
max-width: 440px;
|
max-width: var(--field-max);
|
||||||
}
|
}
|
||||||
.control-group > * { margin: 0 !important; }
|
.control-group > * { margin: 0 !important; }
|
||||||
.control-group > :not(:first-child) { margin-left: -1px !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; }
|
input[type="checkbox"][disabled]:checked { opacity: .5; }
|
||||||
|
|
||||||
/* range slider */
|
/* 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 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); }
|
.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;
|
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); }
|
.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); }
|
.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 {
|
.cbi-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex; align-items: stretch;
|
display: inline-flex; align-items: stretch;
|
||||||
width: 100%; max-width: 440px;
|
width: 100%; max-width: var(--field-max);
|
||||||
min-height: var(--ctl-h);
|
min-height: var(--ctl-h);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--s2);
|
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[style*="bottom"] { top: auto; }
|
||||||
.cbi-dropdown[open] > ul.dropdown > li {
|
.cbi-dropdown[open] > ul.dropdown > li {
|
||||||
display: flex;
|
display: flex; align-items: center;
|
||||||
min-height: 30px;
|
min-height: calc(var(--ctl-h) - 4px);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--r-sm);
|
border-radius: var(--r-sm);
|
||||||
white-space: normal;
|
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[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] > ul.dropdown > li input.create-item-input { max-width: none; min-height: 28px; }
|
||||||
.cbi-dropdown[open] > .open { color: var(--accent-text); }
|
.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 */
|
/* ComboButton: dropdown dressed as a split button */
|
||||||
.cbi-dropdown.btn, .cbi-dropdown.cbi-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); }
|
:is(.btn, .cbi-button).cbi-button-reset:hover { color: var(--text); }
|
||||||
|
|
||||||
/* dynamic list */
|
/* 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 {
|
.cbi-dynlist > .item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
min-height: 30px;
|
min-height: var(--ctl-h);
|
||||||
padding: 4px 34px 4px 10px;
|
padding: 4px calc(var(--ctl-h) + 4px) 4px 10px;
|
||||||
background: var(--s2);
|
background: var(--s2);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--r);
|
border-radius: var(--r);
|
||||||
@@ -1155,11 +1258,11 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
|
|||||||
.cbi-dynlist > .item::after {
|
.cbi-dynlist > .item::after {
|
||||||
content: "\00d7";
|
content: "\00d7";
|
||||||
position: absolute; top: 0; right: 0; bottom: 0;
|
position: absolute; top: 0; right: 0; bottom: 0;
|
||||||
width: 30px;
|
width: var(--ctl-h);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
border-left: 1px solid var(--line);
|
border-left: 1px solid var(--line);
|
||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
font: 400 16px/1 var(--font);
|
font: 400 var(--fs-lg)/1 var(--font);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.cbi-dynlist > .item:hover::after { color: var(--err); background: var(--err-soft); border-radius: 0 var(--r) var(--r) 0; }
|
.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[disabled] > .item::after { display: none; }
|
||||||
.cbi-dynlist > .add-item { max-width: none; }
|
.cbi-dynlist > .add-item { max-width: none; }
|
||||||
.cbi-dynlist > .add-item > .cbi-dropdown { 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 */
|
/* 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) { 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; }
|
.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*="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] {
|
.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;
|
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;
|
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) { display: flex; flex-direction: column; gap: 1px; }
|
||||||
.td > span:has(> span + br + span) > br { display: none; }
|
.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 { 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: 11.5px; }
|
.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 */
|
/* badges inside cells are content, not pills: icon + text */
|
||||||
.td .ifacebadge, .td .ifacebadge.large {
|
.td .ifacebadge, .td .ifacebadge.large {
|
||||||
@@ -1257,7 +1360,7 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
|
|||||||
font-size: inherit; white-space: nowrap;
|
font-size: inherit; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.td .ifacebadge img, .td .ifacebadge img[style] { width: 18px !important; height: 18px !important; flex: none; }
|
.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; }
|
.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-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; }
|
.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-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, .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 > 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-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-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); }
|
.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 { 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; }
|
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 {
|
.cbi-tabmenu {
|
||||||
display: flex; flex-wrap: nowrap; gap: var(--sp-1);
|
display: flex; flex-wrap: nowrap; align-items: center; gap: 2px;
|
||||||
margin: 0 0 var(--sp-4); padding: 0;
|
width: fit-content; max-width: 100%; min-height: var(--ctl-h);
|
||||||
|
margin: 0 0 var(--sp-5); padding: 2px;
|
||||||
list-style: none;
|
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;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.cbi-tabmenu::-webkit-scrollbar { display: none; }
|
.cbi-tabmenu::-webkit-scrollbar { display: none; }
|
||||||
.cbi-tabmenu > li { position: relative; flex: none; }
|
.cbi-tabmenu > li { position: relative; flex: none; }
|
||||||
.cbi-tabmenu > li > a {
|
.cbi-tabmenu > li > a {
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
height: 36px; padding: 0 var(--sp-3);
|
height: calc(var(--ctl-h) - 6px); padding: 0 var(--sp-3);
|
||||||
|
border-radius: var(--r);
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
.cbi-tabmenu > li > a:hover { color: var(--text); }
|
.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 > a { background: var(--seg-on); color: var(--text); font-weight: 600; box-shadow: var(--seg-on-ring); }
|
||||||
.cbi-tabmenu > li.cbi-tab::after {
|
|
||||||
content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: -1px;
|
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;
|
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 {
|
.cbi-tabmenu > li[data-errors] > a::after {
|
||||||
content: attr(data-errors);
|
content: attr(data-errors);
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
min-width: 18px; height: 18px; padding: 0 5px;
|
min-width: 18px; height: 18px; padding: 0 5px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--err); color: #fff;
|
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); }
|
.cbi-tabmenu > li.cbi-tab-disabled[data-errors] > a { color: var(--err); }
|
||||||
[data-tab-title]:not([data-tab-active="true"]) { display: none; }
|
[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;
|
min-width: 0;
|
||||||
white-space: normal; overflow-wrap: anywhere;
|
white-space: normal; overflow-wrap: anywhere;
|
||||||
color: var(--text);
|
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;
|
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:not(:has(> strong)) { display: block; }
|
||||||
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap > strong {
|
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap > strong {
|
||||||
color: var(--text-3);
|
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;
|
letter-spacing: .06em; text-transform: uppercase;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -1452,7 +1637,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border: 1px solid var(--line-2); border-radius: 999px;
|
border: 1px solid var(--line-2); border-radius: 999px;
|
||||||
color: var(--text-2);
|
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 { display: flex; flex-direction: column; gap: 14px; padding: 8px 16px 16px; }
|
||||||
.network-status-table .ifacebox-body > div:not(:empty) {
|
.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;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.network-status-table .ifacebox .ifacebadge > img { width: 18px; height: 18px; margin-top: 1px; }
|
.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; }
|
.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child > strong { display: none; }
|
||||||
|
|
||||||
/* zone colours arrive inline (--zone-color-rgb + background-color): tint instead */
|
/* 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;
|
display: flex; align-items: center;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: 500 11.5px/1 var(--mono);
|
font: 500 var(--fs-micro)/1 var(--mono);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
z-index: 1;
|
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.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.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.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 */
|
/* spinners */
|
||||||
.spinning { position: relative; }
|
.spinning { position: relative; }
|
||||||
@@ -1743,7 +1927,7 @@ body.modal-overlay-active #modal_overlay { display: block; }
|
|||||||
margin: 0 0 var(--sp-4);
|
margin: 0 0 var(--sp-4);
|
||||||
background: var(--s1);
|
background: var(--s1);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--r-lg);
|
||||||
box-shadow: var(--v-shadow);
|
box-shadow: var(--v-shadow);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
@@ -1755,7 +1939,8 @@ body.modal-overlay-active #modal_overlay { display: block; }
|
|||||||
.includes { margin-top: var(--sp-4); }
|
.includes { margin-top: var(--sp-4); }
|
||||||
#view > .cbi-section:first-child { margin-top: 0; }
|
#view > .cbi-section:first-child { margin-top: 0; }
|
||||||
.cbi-section > div[style*="display: none"] + div { display: block; }
|
.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); }
|
iframe { border: 1px solid var(--line); border-radius: var(--r); }
|
||||||
|
|
||||||
/* ------------------------------------------------------ 7b. stock charts
|
/* ------------------------------------------------------ 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 {
|
#view div[style*="height:300px"][style*="background"] svg text, #view div[id="channel_graph"] svg text {
|
||||||
fill: var(--v-chart-axis) !important;
|
fill: var(--v-chart-axis) !important;
|
||||||
text-shadow: none !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;
|
font-variant-numeric: tabular-nums;
|
||||||
letter-spacing: .02em;
|
letter-spacing: .02em;
|
||||||
paint-order: stroke; /* halo: labels stay legible over areas */
|
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)" */
|
/* "(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 > 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 .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 > .table ~ div[style*="height:300px"] { margin-top: var(--sp-6); }
|
||||||
body[data-page^="admin-status-realtime"] #view .cbi-section-create:empty { display: none; }
|
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)) {
|
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;
|
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;
|
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: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: 14px; }
|
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_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_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); }
|
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 > .th:first-child { padding-left: var(--card-px); }
|
||||||
#connections .tr:not(.table-titles) > .td:is(:nth-child(1), :nth-child(2)) {
|
#connections .tr:not(.table-titles) > .td:is(:nth-child(1), :nth-child(2)) {
|
||||||
color: var(--v-text-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(1), :nth-child(2)) { width: 9%; }
|
||||||
#connections .tr > .th:is(:nth-child(3), :nth-child(4)) { width: 31%; }
|
#connections .tr > .th:is(:nth-child(3), :nth-child(4)) { width: 31%; }
|
||||||
#connections .tr > .th:nth-child(5) { width: 20%; }
|
#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) */
|
/* 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 { 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; }
|
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) {
|
body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not(.cbi-section) {
|
||||||
padding: var(--card-py) var(--card-px) 0;
|
padding: var(--card-py) var(--card-px) 0;
|
||||||
background: var(--s1);
|
background: var(--s1);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--r-lg);
|
||||||
box-shadow: var(--v-shadow);
|
box-shadow: var(--v-shadow);
|
||||||
overflow: clip;
|
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; }
|
.v-ca-shape[data-v-own] { stroke-width: 2.25px; }
|
||||||
#view div[id="channel_graph"] svg text.v-ca-label {
|
#view div[id="channel_graph"] svg text.v-ca-label {
|
||||||
fill: var(--v-text-2) !important;
|
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;
|
letter-spacing: 0;
|
||||||
paint-order: stroke;
|
paint-order: stroke;
|
||||||
stroke: var(--s1); stroke-width: 3px; stroke-linejoin: round;
|
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;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
.cbi-section-table .ifacebox-body > br { display: none; }
|
.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 */
|
/* "( dev dev dev )": drop the parentheses, group the member ports */
|
||||||
.cbi-section-table .ifacebox-body > span:not(.cbi-tooltip-container) {
|
.cbi-section-table .ifacebox-body > span:not(.cbi-tooltip-container) {
|
||||||
display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px;
|
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 {
|
[id$="-ifc-description"] > .nowrap {
|
||||||
display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 0 1 auto;
|
display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 0 1 auto;
|
||||||
white-space: normal; overflow-wrap: anywhere;
|
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 {
|
[id$="-ifc-description"] > .nowrap > strong, .td[data-name="_stat"] .nowrap > strong {
|
||||||
color: var(--text-3);
|
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 */
|
/* 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 {
|
.cbi-progressbar::before {
|
||||||
inset: auto 0 calc(100% + 5px) 0; padding: 0;
|
inset: auto 0 calc(100% + 5px) 0; padding: 0;
|
||||||
color: var(--text-2);
|
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) */
|
/* 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 + .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-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
|
||||||
.v-meter-title { color: var(--text); font-weight: 600; }
|
.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-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 {
|
.v-meter-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2155,16 +2342,16 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
|||||||
.v-seg.is-crit { background: var(--err); }
|
.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 { 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 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-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 { 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-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-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 { 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 { 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-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-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; }
|
.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 {
|
.vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 10.5px; font-weight: 500;
|
font-size: var(--fs-micro); font-weight: 500;
|
||||||
text-align: center;
|
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; }
|
.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 { grid-template-columns: minmax(0, 1fr); row-gap: 6px; }
|
||||||
.cbi-value > .cbi-value-title { padding-top: 0; }
|
.cbi-value > .cbi-value-title { padding-top: 0; }
|
||||||
.cbi-value > .cbi-value-field { grid-column: 1; }
|
.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; }
|
.cbi-dropdown, .vt-main input, .vt-main select, .control-group, .cbi-dynlist { max-width: none; }
|
||||||
|
|
||||||
/* tables become cards */
|
/* 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; }
|
.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 */
|
/* ----------------------------------------------------------- motion */
|
||||||
|
|
||||||
/* phones: content in cards wraps instead of pushing the card wider */
|
/* phones: content in cards wraps instead of pushing the card wider */
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
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"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
|
||||||
msgid "Appearance"
|
msgid "Appearance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ msgstr ""
|
|||||||
msgid "Authorization Required"
|
msgid "Authorization Required"
|
||||||
msgstr ""
|
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"
|
msgid "Auto"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -23,52 +23,52 @@ msgstr ""
|
|||||||
msgid "Breadcrumb"
|
msgid "Breadcrumb"
|
||||||
msgstr ""
|
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"
|
msgid "Buffered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
|
||||||
msgid "Cache"
|
msgid "Cache"
|
||||||
msgstr ""
|
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"
|
msgid "Cached"
|
||||||
msgstr ""
|
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"
|
msgid "Channel"
|
||||||
msgstr ""
|
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"
|
msgid "Collapse navigation"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81
|
||||||
msgid "Colour theme"
|
msgid "Colour theme"
|
||||||
msgstr ""
|
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"
|
msgid "Command palette"
|
||||||
msgstr ""
|
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"
|
msgid "Dark"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54
|
||||||
msgid "Expand navigation"
|
msgid "Expand navigation"
|
||||||
msgstr ""
|
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"
|
msgid "Follow system theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:295
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -76,9 +76,9 @@ msgstr ""
|
|||||||
msgid "Home"
|
msgid "Home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:283
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293
|
||||||
msgid "In use"
|
msgid "In use"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -86,11 +86,11 @@ msgstr ""
|
|||||||
msgid "Invalid username and/or password! Please try again."
|
msgid "Invalid username and/or password! Please try again."
|
||||||
msgstr ""
|
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…"
|
msgid "Jump to a page or action…"
|
||||||
msgstr ""
|
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"
|
msgid "Light"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ msgstr ""
|
|||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -108,12 +108,12 @@ msgstr ""
|
|||||||
msgid "Main navigation"
|
msgid "Main navigation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:258
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:264
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:273
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
|
||||||
msgid "Memory"
|
msgid "Memory"
|
||||||
msgstr ""
|
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"
|
msgid "No matching pages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -125,57 +125,69 @@ msgstr ""
|
|||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
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"
|
msgid "Press"
|
||||||
msgstr ""
|
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"
|
msgid "Results"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74
|
||||||
msgid "Search pages"
|
msgid "Search pages"
|
||||||
msgstr ""
|
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"
|
msgid "Session"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:44
|
||||||
msgid "Skip to content"
|
msgid "Skip to content"
|
||||||
msgstr ""
|
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"
|
msgid "Storage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:296
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:304
|
||||||
msgid "Swap"
|
msgid "Swap"
|
||||||
msgstr ""
|
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"
|
msgid "Swap free"
|
||||||
msgstr ""
|
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"
|
msgid "Total"
|
||||||
msgstr ""
|
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"
|
msgid "Total Available"
|
||||||
msgstr ""
|
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"
|
msgid "Use dark theme"
|
||||||
msgstr ""
|
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"
|
msgid "Use light theme"
|
||||||
msgstr ""
|
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"
|
msgid "Used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -183,54 +195,54 @@ msgstr ""
|
|||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
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"
|
msgid "Wireless"
|
||||||
msgstr ""
|
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"
|
msgid "available"
|
||||||
msgstr ""
|
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"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
|
||||||
msgid "free"
|
msgid "free"
|
||||||
msgstr ""
|
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"
|
msgid "items"
|
||||||
msgstr ""
|
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"
|
msgid "navigate"
|
||||||
msgstr ""
|
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"
|
msgid "open"
|
||||||
msgstr ""
|
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"
|
msgid "page"
|
||||||
msgstr ""
|
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"
|
msgid "pages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
|
||||||
msgid "reclaimable"
|
msgid "reclaimable"
|
||||||
msgstr ""
|
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"
|
msgid "to search all pages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
|
||||||
msgid "used"
|
msgid "used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
+3
-6
@@ -9,13 +9,10 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=miniupnpd
|
PKG_NAME:=miniupnpd
|
||||||
PKG_VERSION:=2.3.11
|
PKG_VERSION:=2.3.11
|
||||||
PKG_RELEASE:=10
|
PKG_RELEASE:=11
|
||||||
|
|
||||||
#PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION))
|
# Build from GitHub archives, as release archives differ from Git in code comments; patches cannot be 1:1 applied
|
||||||
#PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE_VERSION:=f8c4f4c3320820e615b281147b3043f66c195f17
|
||||||
#PKG_HASH:=66cb3c3d697ab2bb3a61d3c48628166d6ba328d7c2dbeb95898fdf2a3202af7b
|
|
||||||
# Build from GitHub tag archives, as release archive differ in code comments to the public repository
|
|
||||||
PKG_SOURCE_VERSION:=63ac33eaa49332cde15fd509b539a260b531da05
|
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/miniupnp/miniupnp/tar.gz/$(PKG_SOURCE_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/miniupnp/miniupnp/tar.gz/$(PKG_SOURCE_VERSION)?
|
||||||
PKG_SOURCE:=miniupnp-$(PKG_SOURCE_VERSION).tar.gz
|
PKG_SOURCE:=miniupnp-$(PKG_SOURCE_VERSION).tar.gz
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ upnpd_generate_config() {
|
|||||||
log "IPv6 not ready yet; delay start"
|
log "IPv6 not ready yet; delay start"
|
||||||
sleep 5
|
sleep 5
|
||||||
pass=$((pass + 1))
|
pass=$((pass + 1))
|
||||||
[ "$pass" = "4" ] && log "IPv6 GUA not yet available, UPnP IGD mapping not possible" && break
|
[ "$pass" = "2" ] && log "IPv6 GUA not yet available, UPnP IGDv2 IPv6 mapping may not work" && break
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
# Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4
|
# Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ config upnpd 'settings'
|
|||||||
# Enable networks, multiple can be specified, separated by a space
|
# Enable networks, multiple can be specified, separated by a space
|
||||||
option internal_iface 'lan'
|
option internal_iface 'lan'
|
||||||
option access_defaults 'accept-high-ports' # default none
|
option access_defaults 'accept-high-ports' # default none
|
||||||
|
#option accept_ports ''
|
||||||
|
#option reject_ports '21 23 135 137-139 445 3389'
|
||||||
option check_acl '0' # default 1
|
option check_acl '0' # default 1
|
||||||
#option ipv6_disable '0'
|
|
||||||
#option allow_third_party_mapping '0'
|
#option allow_third_party_mapping '0'
|
||||||
# Extra logging by setting to info or debug (previously 1)
|
# Extra logging by setting to info or debug (previously 1)
|
||||||
option log_output 'default'
|
option log_output 'default'
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Ove
|
|||||||
|
|
||||||
--- a/upnphttp.c
|
--- a/upnphttp.c
|
||||||
+++ b/upnphttp.c
|
+++ b/upnphttp.c
|
||||||
@@ -319,9 +319,22 @@ ParseHttpHeaders(struct upnphttp * h)
|
@@ -321,9 +321,22 @@ ParseHttpHeaders(struct upnphttp * h)
|
||||||
}
|
}
|
||||||
else if(strncasecmp(line, "user-agent:", 11) == 0)
|
else if(strncasecmp(line, "user-agent:", 11) == 0)
|
||||||
{
|
{
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Executable
+93
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Functional tests for the miniupnpd IGD/NAT-PMP daemon variants. The package
|
||||||
|
# version is checked by the build framework, so it is not tested here.
|
||||||
|
|
||||||
|
bin=/usr/sbin/miniupnpd
|
||||||
|
|
||||||
|
# Binary, runtime linking and the OpenWrt integration files common to both
|
||||||
|
# firewall variants. "miniupnpd -h" exits non-zero but must print the usage
|
||||||
|
# banner, which also proves every shared-library dep resolves at runtime.
|
||||||
|
common_checks() {
|
||||||
|
[ -x "$bin" ] || { echo "FAIL: $bin missing or not executable"; exit 1; }
|
||||||
|
"$bin" -h 2>&1 | grep -q "^Usage:" \
|
||||||
|
|| { echo "FAIL: 'miniupnpd -h' printed no usage banner"; exit 1; }
|
||||||
|
[ -x /etc/init.d/miniupnpd ] || { echo "FAIL: init script missing"; exit 1; }
|
||||||
|
grep -q "USE_PROCD=1" /etc/init.d/miniupnpd \
|
||||||
|
|| { echo "FAIL: init script is not a procd service"; exit 1; }
|
||||||
|
grep -q "config upnpd" /etc/config/upnpd \
|
||||||
|
|| { echo "FAIL: /etc/config/upnpd is not valid UCI"; exit 1; }
|
||||||
|
[ -f /etc/hotplug.d/iface/50-miniupnpd ] \
|
||||||
|
|| { echo "FAIL: iface hotplug script missing"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# The config-file parser runs before any firewall or socket setup, so it can be
|
||||||
|
# exercised inside the CI container. A bogus option must be rejected non-zero
|
||||||
|
# and a minimal valid config must be accepted without a parse error.
|
||||||
|
config_checks() {
|
||||||
|
good="/tmp/miniupnpd-good.$$.conf"
|
||||||
|
bad="/tmp/miniupnpd-bad.$$.conf"
|
||||||
|
log="/tmp/miniupnpd.$$.log"
|
||||||
|
cat > "$good" <<-EOF
|
||||||
|
ext_ifname=lo
|
||||||
|
listening_ip=127.0.0.1
|
||||||
|
port=5000
|
||||||
|
enable_natpmp=yes
|
||||||
|
enable_upnp=yes
|
||||||
|
notify_interval=60
|
||||||
|
uuid=00000000-0000-0000-0000-000000000000
|
||||||
|
EOF
|
||||||
|
echo "not_a_real_option=1" > "$bad"
|
||||||
|
|
||||||
|
if "$bin" -f "$bad" -d > "$log" 2>&1; then
|
||||||
|
echo "FAIL: invalid config option was accepted"
|
||||||
|
rm -f "$good" "$bad" "$log"; exit 1
|
||||||
|
fi
|
||||||
|
grep -qiE "invalid option|Error reading configuration" "$log" \
|
||||||
|
|| { echo "FAIL: no parse error reported for bad config"
|
||||||
|
rm -f "$good" "$bad" "$log"; exit 1; }
|
||||||
|
|
||||||
|
# A valid config: launch briefly and confirm the parser accepted it. The
|
||||||
|
# daemon may later stop at firewall/socket setup in the container; that is
|
||||||
|
# past the parser and irrelevant to this check.
|
||||||
|
"$bin" -f "$good" -d > "$log" 2>&1 &
|
||||||
|
mpid=$!
|
||||||
|
sleep 2
|
||||||
|
kill "$mpid" 2>/dev/null
|
||||||
|
wait "$mpid" 2>/dev/null
|
||||||
|
if grep -qiE "invalid option|Error reading configuration" "$log"; then
|
||||||
|
echo "FAIL: valid config was rejected by the parser"
|
||||||
|
rm -f "$good" "$bad" "$log"; exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$good" "$bad" "$log"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
miniupnpd-iptables)
|
||||||
|
# iptables variant ships the fw3 shell include. Its uci-defaults seeder
|
||||||
|
# is not checked here: the postinst sources /etc/uci-defaults/* and
|
||||||
|
# deletes each script that succeeds, so it is gone by now.
|
||||||
|
common_checks
|
||||||
|
[ -f /usr/share/miniupnpd/firewall.include ] || { echo "FAIL: firewall.include missing"; exit 1; }
|
||||||
|
sh -n /usr/share/miniupnpd/firewall.include \
|
||||||
|
|| { echo "FAIL: firewall.include has a shell syntax error"; exit 1; }
|
||||||
|
config_checks
|
||||||
|
echo "miniupnpd-iptables: all functional tests passed"
|
||||||
|
;;
|
||||||
|
miniupnpd-nftables)
|
||||||
|
# nftables variant ships the nft rule fragments consumed by fw4.
|
||||||
|
common_checks
|
||||||
|
for f in table-post/20-miniupnpd.nft \
|
||||||
|
chain-post/dstnat/20-miniupnpd.nft \
|
||||||
|
chain-post/forward/20-miniupnpd.nft \
|
||||||
|
chain-post/srcnat/20-miniupnpd.nft; do
|
||||||
|
[ -s "/usr/share/nftables.d/$f" ] \
|
||||||
|
|| { echo "FAIL: nft fragment $f missing or empty"; exit 1; }
|
||||||
|
done
|
||||||
|
config_checks
|
||||||
|
echo "miniupnpd-nftables: all functional tests passed"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Untested package: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
|
|||||||
|
|
||||||
PKG_NAME:=my-default-settings
|
PKG_NAME:=my-default-settings
|
||||||
PKG_VERSION:=2
|
PKG_VERSION:=2
|
||||||
PKG_RELEASE:=35
|
PKG_RELEASE:=36
|
||||||
|
|
||||||
include $(INCLUDE_DIR)/package.mk
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ if [ "$version" -lt 1 ]; then
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
(echo root; echo root) | passwd root >/dev/null 2>&1
|
(echo root; echo root) | passwd root >/dev/null 2>&1
|
||||||
[ -f /usr/share/rpcd/acl.d/luci-theme-vantage.json ] && uci -q set luci.main.mediaurlbase='/luci-static/vantage'
|
[ -d /usr/share/ucode/luci/template/themes/vantage ] && uci -q set luci.main.mediaurlbase='/luci-static/vantage'
|
||||||
grep -q 'log-facility' /etc/dnsmasq.conf 2>/dev/null || echo "log-facility=/dev/null" >> /etc/dnsmasq.conf
|
grep -q 'log-facility' /etc/dnsmasq.conf 2>/dev/null || echo "log-facility=/dev/null" >> /etc/dnsmasq.conf
|
||||||
|
|
||||||
mkdir -p /etc/sysctl.d
|
mkdir -p /etc/sysctl.d
|
||||||
|
|||||||
Reference in new issue
Block a user