🛸 Sync 2026-08-19 20:38:21

This commit is contained in:
github-actions[bot] committed 2026-08-19 20:38:21 +08:00
1 parent 7549efdb5e
commit 7cb3d8fcff
55 files changed
+403 -252

No files matched your search

+2 -2
View File
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_BAIDUDRIVE:=$(ARCH) PKG_ARCH_BAIDUDRIVE:=$(ARCH)
PKG_NAME:=baidudrive PKG_NAME:=baidudrive
PKG_VERSION:=linkeasefull-runtime-v3.0.9 PKG_VERSION:=linkeasefull-runtime-v3.0.10
PKG_RELEASE:=12 PKG_RELEASE:=13
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/ PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
PKG_HASH:=skip PKG_HASH:=skip
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=ddns-go PKG_NAME:=ddns-go
PKG_VERSION:=6.17.5 PKG_VERSION:=6.17.6
PKG_RELEASE:=22 PKG_RELEASE:=23
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease-common-bin PKG_NAME:=linkease-common-bin
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5 PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=7 PKG_RELEASE:=8
ARCH_HEXCODE:= ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64) ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c ARCH_HEXCODE=1b0c
endif endif
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566 PKG_SOURCE_VERSION:=e0457eac600cd37b128eddf251ff9344702b1d03
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/ PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE) PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease PKG_NAME:=linkease
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5 PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=10 PKG_RELEASE:=11
ARCH_HEXCODE:= ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64) ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c ARCH_HEXCODE=1b0c
endif endif
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566 PKG_SOURCE_VERSION:=e0457eac600cd37b128eddf251ff9344702b1d03
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/ PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE) PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.5 PKG_SOURCE_DATE:=3.0.5
PKG_RELEASE:=9 PKG_RELEASE:=10
ARCH_HEXCODE:= ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64) ifeq ($(ARCH),x86_64)
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
PKG_HASH:=skip PKG_HASH:=skip
endif endif
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566 PKG_SOURCE_VERSION:=e0457eac600cd37b128eddf251ff9344702b1d03
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/ PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH) PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
+2 -2
View File
@@ -451,7 +451,7 @@ start_service() {
} }
service_started() { service_started() {
procd_set_config_changed dnsmasq procd_set_config_changed dhcp
procd_set_config_changed firewall procd_set_config_changed firewall
} }
@@ -528,7 +528,7 @@ service_stopped() {
[ -n "$(/etc/init.d/$CONF info | jsonfilter -q -e '@.'"$CONF"'.instances["mihomo-c"]')" ] || client_stopped [ -n "$(/etc/init.d/$CONF info | jsonfilter -q -e '@.'"$CONF"'.instances["mihomo-c"]')" ] || client_stopped
# Server # Server
procd_set_config_changed dnsmasq procd_set_config_changed dhcp
procd_set_config_changed firewall procd_set_config_changed firewall
} }
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.12 PKG_VERSION:=26.8.19
PKG_RELEASE:=229 PKG_RELEASE:=230
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -466,12 +466,12 @@ function socks_status()
local index = http.formvalue("index") local index = http.formvalue("index")
local id = http.formvalue("id") local id = http.formvalue("id")
e.index = index e.index = index
e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' > /dev/null", appname, id)) == 0 e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '/%s' > /dev/null", appname, id)) == 0
local use_http = uci_get(id, "http_port") or 0 local use_http = uci_get(id, "http_port") or 0
e.use_http = 0 e.use_http = 0
if tonumber(use_http) > 0 then if tonumber(use_http) > 0 then
e.use_http = 1 e.use_http = 1
e.http_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep -E '\\+http|_http' > /dev/null", appname, id)) == 0 e.http_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '/%s' | grep -E '\\+http|_http' > /dev/null", appname, id)) == 0
end end
http_write_json(e) http_write_json(e)
end end
@@ -63,15 +63,9 @@ o:value("shadowsocks", "Shadowsocks")
o:value("trojan", "Trojan") o:value("trojan", "Trojan")
o:value("hysteria2", "Hysteria2") o:value("hysteria2", "Hysteria2")
o:value("wireguard", "WireGuard") o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door") o:value("tunnel", "Tunnel")
o:depends({ custom = false }) o:depends({ custom = false })
o = s:option(DummyValue, "is_endpoint", "")
o.not_rewrite = true
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ custom = false, protocol = "wireguard" })
o = s:option(Value, "port", translate("Listen Port")) o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port" o.datatype = "port"
o:depends({ custom = false }) o:depends({ custom = false })
@@ -93,14 +87,14 @@ o = s:option(ListValue, "d_protocol", translate("Destination protocol"))
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("udp", "UDP") o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP") o:value("tcp,udp", "TCP,UDP")
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "d_address", translate("Destination address")) o = s:option(Value, "d_address", translate("Destination address"))
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "d_port", translate("Destination port")) o = s:option(Value, "d_port", translate("Destination port"))
o.datatype = "port" o.datatype = "port"
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)")
o.default = "none" o.default = "none"
@@ -411,7 +405,8 @@ end
--[[acceptProxyProtocol]] --[[acceptProxyProtocol]]
o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0" o.default = "0"
o:depends({ custom = false, is_endpoint = "" }) o:depends({ transport = "raw" })
o:depends({ transport = "ws" })
--[[Fast Open]] --[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
@@ -1748,6 +1748,7 @@ function apply_redirect(m)
end end
end end
else else
uci:revert(c_config .. "_redirect")
sys.call("/bin/rm -f " .. tmp_uci_file) sys.call("/bin/rm -f " .. tmp_uci_file)
end end
end end
+12 -13
View File
@@ -567,11 +567,11 @@ function gen_config_server(node)
version = 2, version = 2,
users = users users = users
} }
elseif node.protocol == "dokodemo-door" then elseif node.protocol == "tunnel" then
settings = { settings = {
network = node.d_protocol, allowedNetwork = node.d_protocol,
address = node.d_address, rewriteAddress = node.d_address,
port = tonumber(node.d_port) rewritePort = tonumber(node.d_port)
} }
elseif node.protocol == "wireguard" then elseif node.protocol == "wireguard" then
settings = { settings = {
@@ -1231,8 +1231,8 @@ function gen_config(var)
tag = in_tag, tag = in_tag,
listen = "127.0.0.1", listen = "127.0.0.1",
port = new_port, port = new_port,
protocol = "dokodemo-door", protocol = "tunnel",
settings = {network = "tcp,udp", address = to_node.address, port = tonumber(to_node.port)} settings = {allowedNetwork = "tcp,udp", rewriteAddress = to_node.address, rewritePort = tonumber(to_node.port)}
}) })
if to_node.tls_serverName == nil then if to_node.tls_serverName == nil then
to_node.tls_serverName = to_node.address to_node.tls_serverName = to_node.address
@@ -1587,8 +1587,8 @@ function gen_config(var)
if tcp_redir_port or udp_redir_port then if tcp_redir_port or udp_redir_port then
local inbound = { local inbound = {
protocol = "dokodemo-door", protocol = "tunnel",
settings = {network = "tcp,udp", followRedirect = true}, settings = {allowedNetwork = "tcp,udp", followRedirect = true},
streamSettings = {sockopt = {tproxy = "tproxy"}}, streamSettings = {sockopt = {tproxy = "tproxy"}},
sniffing = { sniffing = {
enabled = (xray_settings.sniffing_override_dest == "1") or (node and node.protocol == "_shunt") or false enabled = (xray_settings.sniffing_override_dest == "1") or (node and node.protocol == "_shunt") or false
@@ -1614,7 +1614,7 @@ function gen_config(var)
if tcp_redir_port then if tcp_redir_port then
local tcp_inbound = api.clone(inbound) local tcp_inbound = api.clone(inbound)
tcp_inbound.tag = "tcp_redir" tcp_inbound.tag = "tcp_redir"
tcp_inbound.settings.network = "tcp" tcp_inbound.settings.allowedNetwork = "tcp"
tcp_inbound.port = tonumber(tcp_redir_port) tcp_inbound.port = tonumber(tcp_redir_port)
tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way
table.insert(inbounds, tcp_inbound) table.insert(inbounds, tcp_inbound)
@@ -1623,7 +1623,7 @@ function gen_config(var)
if udp_redir_port then if udp_redir_port then
local udp_inbound = api.clone(inbound) local udp_inbound = api.clone(inbound)
udp_inbound.tag = "udp_redir" udp_inbound.tag = "udp_redir"
udp_inbound.settings.network = "udp" udp_inbound.settings.allowedNetwork = "udp"
udp_inbound.port = tonumber(udp_redir_port) udp_inbound.port = tonumber(udp_redir_port)
table.insert(inbounds, udp_inbound) table.insert(inbounds, udp_inbound)
end end
@@ -1790,11 +1790,10 @@ function gen_config(var)
table.insert(inbounds, { table.insert(inbounds, {
listen = "127.0.0.1", listen = "127.0.0.1",
port = tonumber(dns_listen_port), port = tonumber(dns_listen_port),
protocol = "dokodemo-door", protocol = "tunnel",
tag = "dns-in", tag = "dns-in",
settings = { settings = {
address = "0.0.0.0", allowedNetwork = "tcp,udp"
network = "tcp,udp"
} }
}) })
@@ -96,7 +96,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
<!-- 模拟 ListValue 控件外观 --> <!-- 模拟 ListValue 控件外观 -->
<div class="cbi-input-value cbi-input-select lv-dropdown-display" id="<%=cbid%>.display" tabindex="0"> <div class="cbi-input-value cbi-input-select lv-dropdown-display" id="<%=cbid%>.display" tabindex="0">
<span id="<%=cbid%>.label" class="lv-dropdown-label" title="<%=pcdata(current_label)%>"> <span id="<%=cbid%>.label" class="lv-dropdown-label" title="<%=pcdata(current_label)%>">
<%=pcdata("("..translate("Not set")..")")%> <%=pcdata("( "..translate("Not Set").." )")%>
</span> </span>
<span class="lv-arrow-down"></span> <span class="lv-arrow-down"></span>
</div> </div>
@@ -139,7 +139,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
}); });
lv_registerAdaptive(cbid); lv_registerAdaptive(cbid);
setTimeout(() => { setTimeout(() => {
labelSpan.textContent = lv_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or ("("..translate("Not set")..")"))%>'); labelSpan.textContent = lv_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or ("( "..translate("Not Set").." )"))%>');
}, 100); }, 100);
})(); })();
//]]> //]]>
@@ -1639,6 +1639,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'hysteria2_hop', queryParam.mport || ""); opt.set(dom_prefix + 'hysteria2_hop', queryParam.mport || "");
opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm); opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm);
opt.set(dom_prefix + 'finalmask', queryParam.fm || ""); opt.set(dom_prefix + 'finalmask', queryParam.fm || "");
if (is_singbox && (queryParam.pcs || queryParam.pinSHA256)) queryParam.allowinsecure = '1';
} else if (has_hysteria2) { } else if (has_hysteria2) {
opt.set('type', "Hysteria2"); opt.set('type', "Hysteria2");
dom_prefix = "hysteria2_" dom_prefix = "hysteria2_"
@@ -1652,15 +1653,14 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'obfs_MaxPacketSize', queryParam.maxPacketSize || "1200"); opt.set(dom_prefix + 'obfs_MaxPacketSize', queryParam.maxPacketSize || "1200");
} }
opt.set(dom_prefix + 'hop', queryParam.mport || ""); opt.set(dom_prefix + 'hop', queryParam.mport || "");
if (queryParam.pcs || queryParam.pinSHA256) queryParam.allowinsecure = '0';
} }
opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); opt.set(dom_prefix + 'address', unbracketIP(m.hostname));
opt.set(dom_prefix + 'port', m.port || "443"); opt.set(dom_prefix + 'port', m.port || "443");
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || ""); opt.set(dom_prefix + 'tls_serverName', queryParam.sni || "");
opt.set( opt.set(
dom_prefix + 'tls_allowInsecure', dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.allowInsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') (queryParam.allowinsecure ?? queryParam.allowInsecure ?? queryParam.insecure ?? '0') === '1'
); );
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || queryParam.pinSHA256 || ''); opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || queryParam.pinSHA256 || '');
opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || ''); opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || '');
@@ -39,6 +39,10 @@ for sid in $(uci -q show passwall_server | sed -n 's/^passwall_server\.\([^.=]*\
uci -q set "passwall_server.${sid}=server" uci -q set "passwall_server.${sid}=server"
fi fi
done done
for sid in $(uci -q show passwall_server | sed -n 's/^passwall_server\.\([^.=]*\)\.protocol=dokodemo-door$/\1/p'); do
uci -q set "passwall_server.${sid}.protocol=tunnel"
done
uci -q commit passwall_server uci -q commit passwall_server
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
@@ -27,6 +27,8 @@ while [ 1 -eq 1 ]; do
*dns2socks*) cmd_check=${cmd_check//:/ } ;; *dns2socks*) cmd_check=${cmd_check//:/ } ;;
esac esac
cmd_check=$(printf '%s' "$cmd_check" | sed 's/[^a-zA-Z0-9 ]/\\&/g')
filename=$(basename "$file") filename=$(basename "$file")
stats_file="${RESTART_STATS_DIR}/${filename}.count" stats_file="${RESTART_STATS_DIR}/${filename}.count"
if [ -s "$stats_file" ]; then if [ -s "$stats_file" ]; then
@@ -38,11 +40,11 @@ while [ 1 -eq 1 ]; do
# 检查是否超过最大重启次数 # 检查是否超过最大重启次数
[ "$restart_count" -ge "$MAX_RESTART_COUNT" ] && continue [ "$restart_count" -ge "$MAX_RESTART_COUNT" ] && continue
if ! busybox pgrep -f "$cmd_check" >/dev/null; then if ! busybox pgrep -f "${cmd_check}" >/dev/null; then
restart_count=$((restart_count + 1)) restart_count=$((restart_count + 1))
echo "$restart_count" > "$stats_file" echo "$restart_count" > "$stats_file"
#echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log #echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log
sh -c "nohup $cmd 2>&1 &" sh -c "nohup ${cmd} 2>&1 &"
sleep 1 sleep 1
fi fi
done done
@@ -145,13 +145,14 @@ do
CONFIG[#CONFIG + 1] = { CONFIG[#CONFIG + 1] = {
log = true, log = true,
remarks = name .. "节点", remarks = name .. "节点",
currentNode = node_id and (function() currentNode = (function(id)
local section = uci_get(node_id) or {} local section = id and uci_get(id) or nil
if not section then return nil end
if section[".type"] == "socks" then if section[".type"] == "socks" then
return { Socks = node_id } return { Socks = id }
end end
return section return section
end)() or nil, end)(node_id),
set = function(o, server) set = function(o, server)
uci_set(szType, option, server) uci_set(szType, option, server)
o.newNodeId = server o.newNodeId = server
@@ -264,13 +265,14 @@ do
log = true, log = true,
id = t[".name"], id = t[".name"],
remarks = "访问控制列表[" .. i .. "]", remarks = "访问控制列表[" .. i .. "]",
currentNode = node_id and (function() currentNode = (function(id)
local section = uci_get(node_id) or {} local section = id and uci_get(id) or nil
if not section then return nil end
if section[".type"] == "socks" then if section[".type"] == "socks" then
return { Socks = node_id } return { Socks = id }
end end
return section return section
end)() or nil, end)(node_id),
set = function(o, server) set = function(o, server)
uci_set(t[".name"], option, server) uci_set(t[".name"], option, server)
o.newNodeId = server o.newNodeId = server
@@ -1469,8 +1471,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.tls_serverName = params.sni result.tls_serverName = params.sni
result.tls_pinSHA256 = params.pcs or params.pinsha256 result.tls_pinSHA256 = params.pcs or params.pinsha256
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
result.hysteria2_up_mbps = params.upmbps or (sub_cfg and sub_hy_up_mbps or nil) result.hysteria2_up_mbps = params.upmbps or (sub_cfg and sub_hy_up_mbps or nil)
result.hysteria2_down_mbps = params.downmbps or (sub_cfg and sub_hy_down_mbps or nil) result.hysteria2_down_mbps = params.downmbps or (sub_cfg and sub_hy_down_mbps or nil)
result.hysteria2_hop = params.mport result.hysteria2_hop = params.mport
@@ -1482,19 +1482,26 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.hysteria2_obfs_MinPacketSize = params.minpacketsize or "512" result.hysteria2_obfs_MinPacketSize = params.minpacketsize or "512"
result.hysteria2_obfs_MaxPacketSize = params.maxpacketsize or "1200" result.hysteria2_obfs_MaxPacketSize = params.maxpacketsize or "1200"
end end
if (sub_hysteria2_type == "sing-box" and has_singbox) or (sub_hysteria2_type == "xray" and has_xray) then if (sub_hysteria2_type == "sing-box" and has_singbox) or (sub_hysteria2_type == "xray" and has_xray) then
local is_singbox = sub_hysteria2_type == "sing-box" and has_singbox local is_singbox = sub_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray' result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2" result.protocol = "hysteria2"
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
if is_singbox and (params.pcs or params.pinsha256) then
params.allowinsecure = "1"
end
elseif has_hysteria2 then elseif has_hysteria2 then
result.type = "Hysteria2" result.type = "Hysteria2"
if params.pcs or params.pinsha256 then
params.allowinsecure = "0"
end
else else
log("跳过 Hysteria2 节点,因未适配到 Hysteria2 核心程序,或未正确设置节点使用类型。") log("跳过 Hysteria2 节点,因未适配到 Hysteria2 核心程序,或未正确设置节点使用类型。")
return nil return nil
end end
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
elseif szType == 'tuic' then elseif szType == 'tuic' then
if has_singbox then if has_singbox then
result.type = 'sing-box' result.type = 'sing-box'
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.17 PKG_VERSION:=26.8.17
PKG_RELEASE:=90 PKG_RELEASE:=91
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -63,15 +63,9 @@ o:value("shadowsocks", "Shadowsocks")
o:value("trojan", "Trojan") o:value("trojan", "Trojan")
o:value("hysteria2", "Hysteria2") o:value("hysteria2", "Hysteria2")
o:value("wireguard", "WireGuard") o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door") o:value("tunnel", "Tunnel")
o:depends({ custom = false }) o:depends({ custom = false })
o = s:option(DummyValue, "is_endpoint", "")
o.not_rewrite = true
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ custom = false, protocol = "wireguard" })
o = s:option(Value, "port", translate("Listen Port")) o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port" o.datatype = "port"
o:depends({ custom = false }) o:depends({ custom = false })
@@ -93,14 +87,14 @@ o = s:option(ListValue, "d_protocol", translate("Destination protocol"))
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("udp", "UDP") o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP") o:value("tcp,udp", "TCP,UDP")
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "d_address", translate("Destination address")) o = s:option(Value, "d_address", translate("Destination address"))
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "d_port", translate("Destination port")) o = s:option(Value, "d_port", translate("Destination port"))
o.datatype = "port" o.datatype = "port"
o:depends({ protocol = "dokodemo-door" }) o:depends({ protocol = "tunnel" })
o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)")
o.default = "none" o.default = "none"
@@ -416,7 +410,8 @@ end
--[[acceptProxyProtocol]] --[[acceptProxyProtocol]]
o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0" o.default = "0"
o:depends({ custom = false, is_endpoint = "" }) o:depends({ transport = "raw" })
o:depends({ transport = "ws" })
--[[Fast Open]] --[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
@@ -575,11 +575,11 @@ function gen_config_server(node)
version = 2, version = 2,
users = users users = users
} }
elseif node.protocol == "dokodemo-door" then elseif node.protocol == "tunnel" then
settings = { settings = {
network = node.d_protocol, allowedNetwork = node.d_protocol,
address = node.d_address, rewriteAddress = node.d_address,
port = tonumber(node.d_port) rewritePort = tonumber(node.d_port)
} }
elseif node.protocol == "wireguard" then elseif node.protocol == "wireguard" then
settings = { settings = {
@@ -1227,8 +1227,8 @@ function gen_config(var)
tag = in_tag, tag = in_tag,
listen = "127.0.0.1", listen = "127.0.0.1",
port = new_port, port = new_port,
protocol = "dokodemo-door", protocol = "tunnel",
settings = {network = "tcp,udp", address = to_node.address, port = tonumber(to_node.port)} settings = {allowedNetwork = "tcp,udp", rewriteAddress = to_node.address, rewritePort = tonumber(to_node.port)}
}) })
if to_node.tls_serverName == nil then if to_node.tls_serverName == nil then
to_node.tls_serverName = to_node.address to_node.tls_serverName = to_node.address
@@ -1712,11 +1712,10 @@ function gen_config(var)
table.insert(inbounds, { table.insert(inbounds, {
listen = "127.0.0.1", listen = "127.0.0.1",
port = tonumber(dns_listen_port), port = tonumber(dns_listen_port),
protocol = "dokodemo-door", protocol = "tunnel",
tag = "dns-in", tag = "dns-in",
settings = { settings = {
address = "0.0.0.0", allowedNetwork = "tcp,udp"
network = "tcp,udp"
} }
}) })
local direct_type_dns = { local direct_type_dns = {
@@ -1949,8 +1948,8 @@ function gen_config(var)
if redir_port then if redir_port then
local inbound = { local inbound = {
port = tonumber(redir_port), port = tonumber(redir_port),
protocol = "dokodemo-door", protocol = "tunnel",
settings = {network = "tcp,udp", followRedirect = true}, settings = {allowedNetwork = "tcp,udp", followRedirect = true},
streamSettings = {sockopt = {tproxy = "tproxy"}}, streamSettings = {sockopt = {tproxy = "tproxy"}},
sniffing = { sniffing = {
enabled = xray_settings.sniffing_override_dest == "1" or node.protocol == "_shunt" enabled = xray_settings.sniffing_override_dest == "1" or node.protocol == "_shunt"
@@ -1975,13 +1974,13 @@ function gen_config(var)
local tcp_inbound = api.clone(inbound) local tcp_inbound = api.clone(inbound)
tcp_inbound.tag = "tcp_redir" tcp_inbound.tag = "tcp_redir"
tcp_inbound.settings.network = "tcp" tcp_inbound.settings.allowedNetwork = "tcp"
tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way
table.insert(inbounds, tcp_inbound) table.insert(inbounds, tcp_inbound)
local udp_inbound = api.clone(inbound) local udp_inbound = api.clone(inbound)
udp_inbound.tag = "udp_redir" udp_inbound.tag = "udp_redir"
udp_inbound.settings.network = "udp" udp_inbound.settings.allowedNetwork = "udp"
table.insert(inbounds, udp_inbound) table.insert(inbounds, udp_inbound)
end end
@@ -18,9 +18,10 @@ while [ "$ENABLED" -eq 1 ]; do
for filename in $(ls ${TMP_SCRIPT_FUNC_PATH} | grep -v "^_"); do for filename in $(ls ${TMP_SCRIPT_FUNC_PATH} | grep -v "^_"); do
cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename}) cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename})
cmd_check=$(echo $cmd | awk -F '>' '{print $1}') cmd_check=$(echo $cmd | awk -F '>' '{print $1}')
icount=$(busybox pgrep -f "$(echo $cmd_check)" | wc -l) escape_cmd="$(echo $cmd_check | sed 's/[^a-zA-Z0-9]/\\&/g')"
icount=$(busybox pgrep -f "${escape_cmd}" | wc -l)
if [ $icount = 0 ]; then if [ $icount = 0 ]; then
#echo "${cmd} crashed, restarting." >> /tmp/log/passwall2.log log 0 "${cmd} crashed, restarting."
eval $(echo "nohup ${cmd} 2>&1 &") >/dev/null 2>&1 & eval $(echo "nohup ${cmd} 2>&1 &") >/dev/null 2>&1 &
fi fi
done done
+1 -1
View File
@@ -556,7 +556,7 @@ case $action in
;; ;;
"link_meta") "link_meta")
[[ -d "$META_DIR/meta" ]] || mkdir -p "$META_DIR/meta" [[ -d "$META_DIR/meta" ]] || mkdir -p "$META_DIR/meta"
ln -sf "$META_DIR" /tmp/run/istore-meta [ -e /tmp/run/istore-meta ] || ln -sf "$META_DIR" /tmp/run/istore-meta
;; ;;
*) *)
usage usage
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?= FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),) ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION) PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=25 PKG_RELEASE:=26
endif endif
LUCI_TITLE:=Footstrap Theme LUCI_TITLE:=Footstrap Theme
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

@@ -8,12 +8,6 @@
"background_color": "#f6f8fa", "background_color": "#f6f8fa",
"theme_color": "#f6f8fa", "theme_color": "#f6f8fa",
"icons": [ "icons": [
{
"src": "/luci-static/footstrap/app-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{ {
"src": "/luci-static/footstrap/app-icon-512.png", "src": "/luci-static/footstrap/app-icon-512.png",
"sizes": "512x512", "sizes": "512x512",
@@ -149,7 +149,7 @@ function resolveLen(token, dflt) {
* re-measured against the widths of the density before it. */ * re-measured against the widths of the density before it. */
/* The last resort, stated ONCE so the fallbacks and the sanity net below cannot restate the /* The last resort, stated ONCE so the fallbacks and the sanity net below cannot restate the
* stylesheet's widths in two different places. Reaching for these means the measurement failed. */ * stylesheet's widths in two different places. Reaching for these means the measurement failed. */
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56 }; const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56, contentMax: 1280 };
let _geom = null, _geomDensity = null, _geomWarned = false; let _geom = null, _geomDensity = null, _geomWarned = false;
function shellGeometry() { function shellGeometry() {
@@ -162,10 +162,14 @@ function shellGeometry() {
contentMin: px('--fs-content-min', GEOM_DFLT.contentMin), contentMin: px('--fs-content-min', GEOM_DFLT.contentMin),
sidebarW: px('--fs-sidebar-w', GEOM_DFLT.sidebarW), sidebarW: px('--fs-sidebar-w', GEOM_DFLT.sidebarW),
railW: px('--fs-rail-w', GEOM_DFLT.railW), railW: px('--fs-rail-w', GEOM_DFLT.railW),
/* the token is ONE side's padding; the column loses it twice. Kept as `tokenPad` because it /* the token is ONE side's padding; the column loses it twice. It is also only the FALLBACK:
* is only the FALLBACK — the gutter that counts is measured, see measureShell() */ * measureShell() overwrites this field with the gutter the column actually got, and until it
tokenPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2, * has (the login page has no `.fs-content`, and nothing has measured before the first
contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2 * fitter) the token is the honest answer. */
contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
/* the cap the column stops growing at, so the model knows where the surplus becomes margin
* rather than gutter — see columnWidth() */
contentMax: px('--fs-content-max', GEOM_DFLT.contentMax)
}; };
/* Plausibility, and it costs one comparison: the rail IS the sidebar collapsed, so /* Plausibility, and it costs one comparison: the rail IS the sidebar collapsed, so
* 0 < railW < sidebarW holds by construction. Both known ways this measurement fails destroy * 0 < railW < sidebarW holds by construction. Both known ways this measurement fails destroy
@@ -186,7 +190,6 @@ function shellGeometry() {
+ 'renamed, or a foreign sheet is reaching the measurement probe.'); + 'renamed, or a foreign sheet is reaching the measurement probe.');
} }
_geom = sane ? g : Object.assign({}, GEOM_DFLT); _geom = sane ? g : Object.assign({}, GEOM_DFLT);
if (_geom.tokenPad === undefined) _geom.tokenPad = _geom.contentPad;
/* the token is only the fallback; the gutter that counts is the one measureShell() read */ /* the token is only the fallback; the gutter that counts is the one measureShell() read */
if (_shellPad != null) _geom.contentPad = _shellPad; if (_shellPad != null) _geom.contentPad = _shellPad;
return _geom; return _geom;
@@ -210,21 +213,34 @@ function shellGeometry() {
* under the reader's thumb). So the numbers are taken when a fitter runs — which is on every resize * under the reader's thumb). So the numbers are taken when a fitter runs — which is on every resize
* and every content mutation, and never during a flick, because `fitChrome()` defers exactly like * and every content mutation, and never during a flick, because `fitChrome()` defers exactly like
* every other measuring pass — and everything after that reads what was stored. Nothing they * every other measuring pass — and everything after that reads what was stored. Nothing they
* describe can change without a resize, and a resize schedules a fit. * describe can change without a resize, and a resize schedules a fit; a resize that lands DURING a
* scroll is deferred with the rest, so a mid-scroll answer is the geometry as of the last still
* moment, which is the trade this whole file is built on.
* *
* Unlike the token probe above, a hostile app's declaration is not a threat here: `.fs-content` * Unlike the token probe above, a hostile app's declaration is not a threat here: `.fs-content`
* carries no chrome mark, so an app CAN restyle it, and if it did then that padding IS the column's * carries no chrome mark, so an app CAN restyle it, and if it did then that padding IS the column's
* real gutter. What the probe must not report is a foreign answer for OUR layout; this reports the * real gutter. What the probe must not report is a foreign answer for OUR layout; this reports the
* layout as it stands. Before any fitter has run — the login page has no `.fs-content` at all — the * layout as it stands. Before any fitter has run — the login page has no `.fs-content` at all — the
* token stands in. */ * token stands in. */
let _shellOuter = 0, _shellPad = null; let _shellOuter = 0, _shellPad = null, _padAt = null;
function measureShell() { function measureShell() {
/* the window's own width, every time: fitShell read it before this function existed and the
* value is what everything downstream is measured against */
_shellOuter = document.documentElement.clientWidth; _shellOuter = document.documentElement.clientWidth;
/* The GUTTER, though, is resolved style, and this runs on every mutation batch — once a second
* on any polled page. The two things that move it are the width (a media query re-paddings the
* column below 767px) and the density (the token is `calc(28px * var(--fs-density-space))`), so
* both are the key and an unchanged page resolves nothing. Same trade as the token memo above,
* for the same reason. */
const key = (document.documentElement.getAttribute('data-density') || '') + '|' + _shellOuter;
if (_padAt === key && _shellPad != null) return;
const host = document.querySelector('.fs-content'); const host = document.querySelector('.fs-content');
const cs = host ? getComputedStyle(host) : null; const cs = host ? getComputedStyle(host) : null;
const v = cs ? parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) : NaN; const v = cs ? parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) : NaN;
/* a failed read leaves the key unset, so the next pass tries again rather than caching a miss */
if (Number.isFinite(v) && v >= 0) { if (Number.isFinite(v) && v >= 0) {
_shellPad = v; _shellPad = v;
_padAt = key;
if (_geom) _geom.contentPad = v; if (_geom) _geom.contentPad = v;
} }
} }
@@ -232,7 +248,15 @@ function measureShell() {
function columnWidth(g, state) { function columnWidth(g, state) {
/* narrow OR top: in both the chrome is above the content, not beside it */ /* narrow OR top: in both the chrome is above the content, not beside it */
const cut = (state.narrow || state.top) ? 0 : (state.rail ? g.railW : g.sidebarW); const cut = (state.narrow || state.top) ? 0 : (state.rail ? g.railW : g.sidebarW);
return Math.max(0, state.outerW - cut - g.contentPad); /* AND THE COLUMN STOPS GROWING. `.fs-content` is `max-width: var(--fs-content-max); margin: 0
* auto`, so past roughly a 1500px window the surplus becomes margin and not column: without the
* cap this answered ~2280 on a 2560px sidebar layout for a column that is 1224 wide. No caller
* can reach that today — both ask a LOWER bound (`< --fs-content-min` here, `< CRAMPED` in
* fs-select) and the cap only binds where both are clear by a factor of two — but this is the
* exported answer to "how wide is the content column", and an answer that is only true below
* 1500px is the kind of thing the next caller inherits without being told. */
const room = Math.min(state.outerW - cut, g.contentMax);
return Math.max(0, room - g.contentPad);
} }
function fitShell() { function fitShell() {
@@ -546,9 +570,10 @@ return baseclass.extend({
}); });
}, },
/* exported for the unit suite (tests/chrome-geometry.test.mjs): the cut is pure arithmetic over /* exported for the unit suite in the theme's own repository (tests/chrome-geometry.test.mjs — no
* four measured numbers, and driving it directly is the only way to hold every combination of * tests ship in the package): the cut is pure arithmetic over a handful of measured numbers, and
* layout, rail and width without a browser */ * driving it directly is the only way to hold every combination of layout, rail and width
* without a browser */
columnWidth, columnWidth,
wireRail wireRail
}); });
@@ -86,6 +86,9 @@ function fittersEnabled() {
function run() { function run() {
if (!fittersEnabled()) return; if (!fittersEnabled()) return;
runAll(_fitters, 'fitter'); runAll(_fitters, 'fitter');
/* the page is settled now, so this is the position the next mutation must be measured against —
* unless a correction is already on its way, which would make this reference the drifted one */
if (!_anchorPending) rememberRest();
} }
/* ---- IS THE PAGE MOVING RIGHT NOW? — ASKED OF THE POSITION, NEVER OF THE EVENTS ---- /* ---- IS THE PAGE MOVING RIGHT NOW? — ASKED OF THE POSITION, NEVER OF THE EVENTS ----
@@ -190,6 +193,8 @@ function sampleMotion() {
} }
if (scrolling()) { requestAnimationFrame(sampleMotion); return; } if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
_sampling = false; _sampling = false;
/* the reader has stopped: this is a still moment, and the reference for the next tick */
rememberRest();
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */ /* the page has held still for SCROLL_IDLE: whatever was put off may run now */
if (_deferred) { if (_deferred) {
_deferred = false; _deferred = false;
@@ -295,53 +300,100 @@ function watch(el) {
* *
* It never fights the user: a page at the very top has no offset to give back and is left alone, and * It never fights the user: a page at the very top has no offset to give back and is left alone, and
* a drift under a pixel is rounding rather than movement. */ * a drift under a pixel is rounding rather than movement. */
/* DOES THE ENGINE ANCHOR AT ALL? Chromium and Firefox compensate a height change above the viewport
* by themselves — the reader stays where they were and the scroll offset moves under them. WebKit
* has never implemented it, so on Safari and on every iPhone the same poll tick moves the page:
* measured with the engine's anchoring suppressed, a 120px growth above the fold moves the reader
* 120px, and 0px with it on.
*
* The theme cannot do this job everywhere, only where nobody else is: correcting the offset in an
* engine that also corrects it means two corrections and a page that jumps the other way. So it is
* asked of the platform rather than of a browser name — `overflow-anchor` is the property that turns
* the feature off, and an engine that does not know the property does not have the feature. */
const ENGINE_ANCHORS = (() => {
/* DEV SWITCH, and the reason the fallback can be gated at all: `localStorage.fsEngineAnchor =
* 'off'` makes any engine take the path Safari takes. Without it the branch could only be
* exercised on a machine with Safari on it, which is neither CI nor most of the people who work
* on this — the same reason `fsFit` and `fsAnchor` exist. */
try { if (localStorage.getItem('fsEngineAnchor') === 'off') return false; }
catch (e) { /* no storage, no switch */ }
try { return typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('overflow-anchor', 'auto'); }
catch (e) { return true; } /* unreadable: assume it is handled rather than fight it */
})();
/* WHAT THE READER WAS LOOKING AT, CAPTURED WHILE THE PAGE WAS STILL.
*
* `anchorRef()` below is called from the mutation observer, which runs AFTER the DOM changed: it
* captures a position the poll has already moved, which is exactly right for the FITTERS (they have
* not run yet) and blind to the mutation itself. On an engine that anchors, that split does not
* matter — the engine covers the other half. On one that does not, this is the half nobody covers,
* so the reference is kept from the last still moment instead. */
let _rest = null;
function rememberRest() {
if (ENGINE_ANCHORS || scrolling()) return;
const ref = anchorRef();
/* the offset it was taken at travels with it: a reference is only about the page, and the page
* moving under the reader is a different fact from the reader moving through it */
_rest = ref ? { el: ref.el, top: ref.top, at: scrollTop() } : null;
}
/* -> the reference to correct against: the pre-mutation one where the engine leaves that to us, the
* post-mutation one everywhere else, which is what this file has always used. A remembered
* reference is worth using only while it still describes the reader's position — an element that
* left the document, or that the reader has since scrolled a screen away from, is not one. */
function anchorFor() {
if (ENGINE_ANCHORS) return anchorRef();
if (!_rest || !_rest.el.isConnected) return anchorRef();
/* THE READER MOVED, NOT THE PAGE. A reference taken at one offset says nothing about a document
* seen from another: correcting against it would drag the page back to where the reader had
* scrolled FROM. Anything but the offset it was captured at means take a fresh one — which, on
* the mutation path, is the same as not correcting this tick. */
if (_rest.at !== scrollTop()) return anchorRef();
return _rest;
}
function anchorRef() { function anchorRef() {
/* NOT WHILE THE READER SCROLLS. Every rect read here is a forced layout, and this runs on every /* NOT WHILE THE READER SCROLLS. Every rect read here is a forced layout, and this runs on every
* content mutation — i.e. once a second, in the middle of a flick, which is the work iOS holds * content mutation — i.e. once a second, in the middle of a flick, which is the work iOS holds
* the main thread back to avoid. The compensation exists for a page the reader is looking at, * the main thread back to avoid. The compensation exists for a page the reader is looking at,
* not for one they are already moving; while they scroll there is nothing to keep still. */ * not for one they are already moving; while they scroll there is nothing to keep still. */
if (scrolling()) return null; if (scrolling()) return null;
const frames = document.querySelectorAll('#view .cbi-section, #view > .cbi-section-node, #view > div');
for (const el of frames) { /* WHAT THE READER IS LOOKING AT, ASKED OF THE PAGE RATHER THAN OF A SELECTOR LIST.
const r = el.getBoundingClientRect(); *
/* the frame the viewport's top edge cuts through */ * This walked a list of frames and took the one the fold cut through, which misses the case that
if (r.top <= 0 && r.bottom > 0) return { el, top: r.top }; * matters: a poll tick that grows something INSIDE that frame, above the viewport, leaves the
* frame's own top exactly where it was — drift 0, measured — while everything after it moves.
* Taking the deepest element AT the fold instead is both cheaper (one hit test, no rect walk)
* and the same thing the engine's own anchoring picks, so the two agree about what "still" means.
*
* A data table is never the anchor: the fit pass deliberately falsifies its layout inside one
* pass, so the theme excludes it from the engine's anchoring too (`overflow-anchor: none`,
* theme/30-tables.css). Climbing to the nearest ancestor that is not one keeps a reference that
* does not lie. */
const host = document.getElementById('view');
if (!host) return null;
const box = host.getBoundingClientRect();
const x = Math.round(box.left + (Math.min(box.width, window.innerWidth || box.width) / 2));
/* BELOW THE CHROME, not at y=1: the bar is sticky and owns the first rows of the viewport, so a
* hit test at the very top returns the chrome and the page gets no anchor at all (measured: at
* 390px the hit was `nav.fs-sidebar` every time and no correction ever ran). The chrome says
* where it ends — `[data-fs-chrome]` is the mark it already carries, so nothing here names a
* height or a selector that a layout change could move. */
let y = 1;
let el = document.elementFromPoint(x, y);
const chrome = el && el.closest ? el.closest('[data-fs-chrome]') : null;
if (chrome) {
y = Math.max(1, Math.round(chrome.getBoundingClientRect().bottom) + 1);
el = document.elementFromPoint(x, y);
} }
return null; if (!el || !host.contains(el)) return null;
const table = el.closest('.table.fs-dt');
if (table) el = table.parentElement;
if (!el || !host.contains(el)) return null;
return { el, top: el.getBoundingClientRect().top };
} }
/* THE CORRECTION IS UNCONDITIONAL, AND IT IS A NO-OP WHERE THE ENGINE DOES THE JOB.
*
* There used to be a watching phase here: the first three mutations were only measured, and a
* reference that had held still meant "this engine anchors, never touch the offset again". It was
* wrong in the way that mattered. WebKit does not anchor, but the drift it leaves is often small —
* a value grew a line, a row appeared — so three quiet samples were easy to come by, the
* compensation switched itself off for the life of the document, and the page walked downwards a
* few pixels at a time, once per poll tick. That is exactly how it was reported: "jerks a little
* downwards, all the time".
*
* Correcting always is safe because the correction is computed from the REFERENCE, not from the
* scroll offset: an engine that anchors has already put the reference back by the time this reads
* it — the read forces layout, and the adjustment happens during layout — so the drift is zero and
* nothing is written. Measuring the OFFSET instead reads an anchoring adjustment as a fault and
* corrects a correction, which is how an earlier version made Chromium worse (10 movements up to
* 1616px against 0 without it); that mistake is not in this shape.
*
* Two guards remain, and both are about not fighting the reader: a page at the very top has no
* offset to give back, and a drift under a pixel is rounding rather than movement. */
/* ONCE PER FRAME, AFTER EVERYTHING HAS LANDED — not once per mutation batch.
*
* A poll tick does not arrive as one mutation: luci-mod-status fills its sections one after another,
* and each fill wakes this observer. Correcting on each of them measures a page that is still being
* written: the reference is read between two batches, the drift is computed against a layout that
* the next batch immediately invalidates, and the correction is wrong by whatever the rest of the
* tick brought. The next tick corrects the other way. That is a page that jerks while nobody
* touches it, and it is what was reported — with a telling detail: it went away whenever a
* diagnostic probe that forced layout on EVERY frame was loaded, because then every reading this
* code took happened to be against a settled layout.
*
* So the reference is taken at the first mutation of a batch and the correction is applied in a
* requestAnimationFrame — after the batch, after style and layout have settled, once. A newer
* reference is not taken while one is pending: the first one is where the reader actually was. */
let _anchorPending = null; let _anchorPending = null;
let _anchorFrame = 0; let _anchorFrame = 0;
/* DEV SWITCH, for the device that has the problem: `localStorage.fsAnchor = 'off'` stops the theme /* DEV SWITCH, for the device that has the problem: `localStorage.fsAnchor = 'off'` stops the theme
@@ -362,6 +414,10 @@ function scheduleAnchor(ref) {
const pending = _anchorPending; const pending = _anchorPending;
_anchorPending = null; _anchorPending = null;
applyAnchor(pending); applyAnchor(pending);
/* AFTER the correction, never before: the reference this file keeps for engines with no
* anchoring of their own must describe the page as the reader now sees it, or the next tick
* measures a drift that has already been paid and pays it twice. */
rememberRest();
}); });
} }
function applyAnchor(ref) { function applyAnchor(ref) {
@@ -373,6 +429,11 @@ function applyAnchor(ref) {
if (at <= 0) return; if (at <= 0) return;
const drift = ref.el.getBoundingClientRect().top - ref.top; const drift = ref.el.getBoundingClientRect().top - ref.top;
if (Math.abs(drift) < 1) return; if (Math.abs(drift) < 1) return;
/* A CORRECTION IS A SCROLL THE READER DID NOT ASK FOR, so an absurd one is a bug rather than a
* fix: a view that replaced its whole subtree can move a reference by thousands of pixels, and
* jumping there is worse than leaving the page where it is. One viewport is the most a single
* tick can honestly account for. */
if (Math.abs(drift) > (window.innerHeight || 0) + 200) return;
if (sc) sc.scrollTop = at + drift; if (sc) sc.scrollTop = at + drift;
else window.scrollTo(0, at + drift); else window.scrollTo(0, at + drift);
} }
@@ -393,8 +454,11 @@ function applyAnchor(ref) {
function observeContent() { function observeContent() {
if (_mo) return; if (_mo) return;
_mo = new MutationObserver(() => { _mo = new MutationObserver(() => {
/* what the reader is looking at, before the poll's mutation and the fitters move anything */ /* what the reader is looking at, before the poll's mutation and the fitters move anything —
const ref = anchorRef(); * see anchorFor(): where the engine does no anchoring of its own that reference comes from
* the last still moment, because by the time this callback runs the poll has already moved
* the page */
const ref = anchorFor();
run(); run();
scheduleAnchor(ref); scheduleAnchor(ref);
}); });
@@ -22,7 +22,12 @@
* what a full load does anyway. docs/spa-router.md. * what a full load does anyway. docs/spa-router.md.
* *
* The path->node half lives in fs-menutree.js (the chrome needs it too); the "has a view poisoned * The path->node half lives in fs-menutree.js (the chrome needs it too); the "has a view poisoned
* this document with its CSS?" half in fs-sheets.js. */ * this document with its CSS?" half in fs-sheets.js.
*
* WHERE THE PATHS IN THESE COMMENTS RESOLVE: `docs/…`, `tools/…` and `tests/…` name the theme's own
* repository (https://github.com/VizzleTF/luci-theme-footstrap), not the tree this file is read in —
* the package ships the runtime and nothing else, so none of the three directories travels with it.
* Anything a user is asked to quote carries a URL instead; a comment may name the path. */
/* --- stray-interval teardown for SPA nav --- /* --- stray-interval teardown for SPA nav ---
* A full load kills every window.setInterval the outgoing page set; SPA nav does not, so a view's * A full load kills every window.setInterval the outgoing page set; SPA nav does not, so a view's
@@ -1393,9 +1398,13 @@ function wireRouter() {
const broken = contractBreaks(); const broken = contractBreaks();
if (broken.length) { if (broken.length) {
/* the URL, not a repository path: this package ships inside openwrt/luci as well, where a
* `docs/` directory does not exist — and this is the one line a stranger is asked to quote,
* on the branch where the theme has already switched half of itself off. */
console.error('footstrap: this luci-base has no ' + broken.join(', ') + console.error('footstrap: this luci-base has no ' + broken.join(', ') +
' — the client router stays off and every link is a full page load, which is what the ' + ' — the client router stays off and every link is a full page load, which is what the ' +
'theme did before it existed. Please report this line: docs/spa-router.md'); 'theme did before it existed. Please report this line: ' +
'https://github.com/VizzleTF/luci-theme-footstrap/blob/main/docs/spa-router.md');
return; return;
} }
@@ -1546,12 +1555,14 @@ return baseclass.extend({
wire: wireRouter, wire: wireRouter,
wireVisibility, wireVisibility,
onNavigate, onNavigate,
/* exported for the unit suite (tests/router-contract.test.mjs), which drives it against a /* exported for the unit suite in the theme's own repository (tests/router-contract.test.mjs —
* hand-broken `L` — the one way to see the OFF branch without a router that ships one */ * it is not part of the package, which ships no tests), where it is driven against a
* hand-broken `L`: the one way to see the OFF branch without a router that ships one */
contractBreaks, contractBreaks,
/* likewise: tests/interval-pause.test.mjs drives the navigation sweep around a visibilitychange, /* likewise, and likewise out-of-package: interval-pause.test.mjs drives the navigation sweep
* and tests/session-expiry.test.mjs reads the verdict the interceptors reached. navigate() is * around a visibilitychange and session-expiry.test.mjs reads the verdict the interceptors
* the real caller of the first and `_expired` gates the second — nothing else may call either. */ * reached. navigate() is the real caller of the first and `_expired` gates the second — nothing
* else may call either. */
clearViewIntervals, clearViewIntervals,
sessionExpired, sessionExpired,
/* fs-search warms the pages this admin actually uses (its recents) and the arrow-key-highlighted /* fs-search warms the pages this admin actually uses (its recents) and the arrow-key-highlighted
@@ -612,8 +612,8 @@ function fitTables() {
/* the column's width, not the window's: in the sidebar layout they differ by the sidebar, /* the column's width, not the window's: in the sidebar layout they differ by the sidebar,
* and an 800px window whose column is 520px used to clear CRAMPED and leave the table * and an 800px window whose column is 520px used to clear CRAMPED and leave the table
* unstacked at full width, clipped by `.fs-main { overflow-x: clip }` until the reader * unstacked at full width, clipped by `.fs-main { overflow-x: clip }` until the reader
* held still. `chrome.contentWidth()` answers from memoised tokens and attributes — no * held still. `chrome.contentWidth()` answers from what the last fitter measured plus the
* layout is read here either. */ * layout attributes — no layout is read here either. */
applyDecision(t, known || { stack: chrome.contentWidth() < CRAMPED, drop: false, breakCol: -1 }); applyDecision(t, known || { stack: chrome.contentWidth() < CRAMPED, drop: false, breakCol: -1 });
fit.deferMeasurement(); fit.deferMeasurement();
return; return;
@@ -5,12 +5,52 @@
'require fs-menutree as tree'; 'require fs-menutree as tree';
'require fs-chrome as chrome'; 'require fs-chrome as chrome';
'require fs-router as router'; 'require fs-router as router';
'require fs-appearance as appearance';
'require fs-overview as overview';
'require fs-prefs as prefs'; 'require fs-prefs as prefs';
'require fs-sheets as sheets'; 'require fs-sheets as sheets';
'require fs-search as search'; 'require fs-search as search';
/* PAGE MODULES: TWO OF THESE MODULES ARE FOR ONE PAGE EACH, AND USED TO SHIP WITH EVERY PAGE.
*
* `fs-appearance` draws the Appearance controls on System -> System and `fs-overview` reshapes
* Status -> Overview; each watches `body[data-page]` and does nothing anywhere else, which is the
* right design — a theme may not register a dispatcher node, so it cannot own a route. But they
* were `require`d in the directive prologue, and a pragma is a hard dependency: luci.js fetches and
* evaluates them before this file's factory runs, on EVERY admin page. Measured after terser: 11.5
* KB for the Appearance panel and 3.8 KB for the Overview one, on every cold visit to a page that
* has neither.
*
* So the pragma is replaced by the same observation they were doing for themselves, once, here: on
* the page they belong to — and only there — the module is required and wired. What each of them
* then does is unchanged, including its own observer: `wire()` re-checks the page synchronously, so
* a module that arrives after the stamp still starts watching. The dependency EDGE is what moved,
* not the behaviour.
*
* The map duplicates a page name that also lives inside each module, and `npm run page-modules`
* derives both sides and fails if they drift — the same trade as the Appearance axes, which are
* implemented twice on purpose and held by a gate. */
const PAGE_MODULES = {
'admin-system-system': 'fs-appearance',
'admin-status-overview': 'fs-overview'
};
const _pageModules = new Map();
function wirePageModules() {
/* through window.L, never the factory's `L`: that one carries no require() of its own */
const RT = window.L;
const load = () => {
const name = PAGE_MODULES[document.body.getAttribute('data-page') || ''];
if (!name || _pageModules.has(name)) return;
_pageModules.set(name, RT.require(name).then((m) => m.wire()).catch((e) => {
/* a page module that will not load costs its own page's extras and nothing else, so it
* is dropped from the map and retried the next time that page comes up */
_pageModules.delete(name);
console.error('footstrap: ' + name + ' did not load', e);
}));
};
/* the server's stamp is already in the DOM; every later one is the router's */
new MutationObserver(load).observe(document.body, { attributes: true, attributeFilter: [ 'data-page' ] });
load();
}
/* The chrome BOOTSTRAP: load the menu tree once, hand it to the parts that need it, and wire them /* The chrome BOOTSTRAP: load the menu tree once, hand it to the parts that need it, and wire them
* in the right order. It renders nothing itself — every piece lives in its own module: * in the right order. It renders nothing itself — every piece lives in its own module:
* *
@@ -58,21 +98,17 @@ return baseclass.extend({
fit.add(chrome.fitChrome); fit.add(chrome.fitChrome);
chrome.renderChrome(); chrome.renderChrome();
/* Both of these ADD to a stock page rather than owning a route: each watches
* body[data-page] and does nothing anywhere else. A theme may not register a
* dispatcher node — it outlives the theme that registered it. */
appearance.wire();
/* after setTree(): the palette indexes that tree — lazily, on its first open, but its /* after setTree(): the palette indexes that tree — lazily, on its first open, but its
* recent-pages list is recorded from the first navigation onwards */ * recent-pages list is recorded from the first navigation onwards */
search.wire(); search.wire();
chrome.wireRail(); chrome.wireRail();
chrome.wireIndicatorCounts(); chrome.wireIndicatorCounts();
/* BEFORE router.wire(): the router restamps body[data-page] on every SPA navigation, /* BEFORE router.wire(): the router restamps body[data-page] on every SPA navigation,
* and that attribute is what fs-overview keys off. Wiring its observer after the * and that attribute is what the page modules key off. Wiring the observer after the
* router's would still work today (the first stamp is the server's, already in the * router's would still work today (the first stamp is the server's, already in the
* DOM), but it would make a route change racy against a listener that is not attached * DOM), but it would make a route change racy against a listener that is not attached
* yet — cheap to order correctly, expensive to debug. */ * yet — cheap to order correctly, expensive to debug. */
overview.wire(); wirePageModules();
router.wire(); router.wire();
router.wireVisibility(); router.wireVisibility();
/* fs-chrome's renderTabMenu warns about exactly this, and the root chain was left bare: a /* fs-chrome's renderTabMenu warns about exactly this, and the root chain was left bare: a
+12
View File
@@ -69,6 +69,18 @@
* tools/mirror.mjs; the third restated the literal until it had a name. */ * tools/mirror.mjs; the third restated the literal until it had a name. */
--fs-row-rail: inset 2px 0 0 var(--fs-accent); --fs-row-rail: inset 2px 0 0 var(--fs-accent);
/* THE HAIRLINE, named because it is a decision and not an accident. Every separator in
* this theme is the same one-pixel line in the same border colour — 69 declarations said
* so in longhand, which is 69 places to miss when the answer changes (a 2px line for a
* high-contrast mode, a softer colour, a logical-property rewrite). The shorthand is the
* token, so `border`, `border-top` and friends all take it, and the private tier means it
* costs two characters in the shipped sheet rather than twenty-six. */
--fs-hairline: 1px solid var(--fs-border);
/* A FIELD REACTING TO FOCUS: the ring arrives and the edge changes colour together, and
* they must stay together — a transition list that drifts apart shows the border moving
* before the ring. Eleven controls ride this. */
--fs-field-transition: border-color var(--fs-dur), box-shadow var(--fs-dur);
/* the theme's ONE colour literal, deliberately: black at .7 is the absence of light /* the theme's ONE colour literal, deliberately: black at .7 is the absence of light
* behind a dialog, not a tint of any token — a palette-tinted scrim over a dark page * behind a dialog, not a tint of any token — a palette-tinted scrim over a dark page
* dims nothing. A token so the RULES stay literal-free. */ * dims nothing. A token so the RULES stay literal-free. */
@@ -112,7 +112,7 @@
hr { hr {
margin: var(--fs-space-5) 0; margin: var(--fs-space-5) 0;
border: 0; border: 0;
border-bottom: 1px solid var(--fs-border); border-bottom: var(--fs-hairline);
} }
strong { strong {
@@ -230,7 +230,7 @@
/* was 17px 20px 18px 17px — an asymmetric inset nobody chose; it is a plate, so it takes /* was 17px 20px 18px 17px — an asymmetric inset nobody chose; it is a plate, so it takes
* the card's own gutter on every side. */ * the card's own gutter on every side. */
padding: var(--fs-card-pad); padding: var(--fs-card-pad);
border-top: 1px solid var(--fs-border); border-top: var(--fs-hairline);
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg); border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
text-align: end; text-align: end;
} }
@@ -91,7 +91,7 @@
padding: var(--fs-space-2) var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3);
margin: 0 0 var(--fs-space-4); margin: 0 0 var(--fs-space-4);
background: linear-gradient(to bottom, var(--fs-panel), var(--fs-panel2)); background: linear-gradient(to bottom, var(--fs-panel), var(--fs-panel2));
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
display: flex; display: flex;
flex: 0; flex: 0;
@@ -22,7 +22,7 @@
/* a modal is a CARD — the large radius, like every other panel */ /* a modal is a CARD — the large radius, like every other panel */
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
/* box-shadow: theme/70-modal.css — absorbed. It was `0 0 3px var(--fs-panel2)`, a shadow in /* box-shadow: theme/70-modal.css — absorbed. It was `0 0 3px var(--fs-panel2)`, a shadow in
* the colour of the page (in footstrap light panel2 and bg are the same hex), so the theme * the colour of the page (in footstrap light panel2 and bg are the same hex), so the theme
* gives every .modal --fs-shadow-pop instead and this one was dead. */ * gives every .modal --fs-shadow-pop instead and this one was dead. */
+7 -7
View File
@@ -175,7 +175,7 @@
} }
.ifacebox { .ifacebox {
border: 1px solid var(--fs-border); border: var(--fs-hairline);
margin: 0 var(--fs-space-3); margin: 0 var(--fs-space-3);
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
@@ -190,7 +190,7 @@
} }
.ifacebox .ifacebox-head { .ifacebox .ifacebox-head {
border-bottom: 1px solid var(--fs-border); border-bottom: var(--fs-hairline);
/* the head sits INSIDE the box's 1px border, so its own radius is the box's less that /* the head sits INSIDE the box's 1px border, so its own radius is the box's less that
* border — restating a smaller literal is how the two drifted apart before. */ * border — restating a smaller literal is how the two drifted apart before. */
border-radius: calc(var(--fs-radius-sm) - 1px) calc(var(--fs-radius-sm) - 1px) 0 0; border-radius: calc(var(--fs-radius-sm) - 1px) calc(var(--fs-radius-sm) - 1px) 0 0;
@@ -233,7 +233,7 @@
.ifacebadge { .ifacebadge {
display: inline-block; display: inline-block;
flex-direction: row; flex-direction: row;
border: 1px solid var(--fs-border); border: var(--fs-hairline);
padding: var(--fs-space-0-5); padding: var(--fs-space-0-5);
/* surface: theme/45-misc.css (flat) — absorbed */ /* surface: theme/45-misc.css (flat) — absorbed */
color: var(--fs-text); color: var(--fs-text);
@@ -409,7 +409,7 @@
.uci-dialog div > ins, .uci-dialog div > ins,
.uci-dialog div > var { .uci-dialog div > var {
margin-bottom: var(--fs-space-0-5); margin-bottom: var(--fs-space-0-5);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
line-height: var(--fs-leading-tight); line-height: var(--fs-leading-tight);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -571,7 +571,7 @@
min-width: min(210px, 100%); min-width: min(210px, 100%);
max-width: 100%; max-width: 100%;
/* width/style only; radius: theme/60-inputs.css — absorbed */ /* width/style only; radius: theme/60-inputs.css — absorbed */
border: 1px solid var(--fs-border); border: var(--fs-hairline);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
opacity: 0; opacity: 0;
@@ -587,7 +587,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
margin: .25em .25em 0 .25em; margin: .25em .25em 0 .25em;
white-space: nowrap; white-space: nowrap;
border-bottom: 1px solid var(--fs-border); border-bottom: var(--fs-hairline);
} }
.cbi-filebrowser .cbi-button-positive { .cbi-filebrowser .cbi-button-positive {
@@ -625,7 +625,7 @@
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 -.125em .25em -.125em; margin: 0 -.125em .25em -.125em;
padding: 0 0 .125em 0; padding: 0 0 .125em 0;
border-bottom: 1px solid var(--fs-border); border-bottom: var(--fs-hairline);
} }
.cbi-filebrowser .upload > * { .cbi-filebrowser .upload > * {
@@ -23,7 +23,7 @@
* min-content, ~131px, and the description wrapped one word per line. */ * min-content, ~131px, and the description wrapped one word per line. */
width: min(400px, calc(100% - 32px)); width: min(400px, calc(100% - 32px));
margin: 7vh auto var(--fs-space-10); margin: 7vh auto var(--fs-space-10);
background: var(--fs-panel); border: 1px solid var(--fs-border); border-radius: var(--fs-radius-lg); background: var(--fs-panel); border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
/* the token, not a literal 45%-black 60px halo: that ignored the palette and drew /* the token, not a literal 45%-black 60px halo: that ignored the palette and drew
* a heavy grey cloud under a white card in light mode. */ * a heavy grey cloud under a white card in light mode. */
padding: var(--fs-space-6) var(--fs-space-6) var(--fs-space-6); box-shadow: var(--fs-shadow-pop); padding: var(--fs-space-6) var(--fs-space-6) var(--fs-space-6); box-shadow: var(--fs-shadow-pop);
@@ -140,7 +140,7 @@
align-self: stretch; align-self: stretch;
text-align: start; text-align: start;
background: var(--fs-panel2); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
padding: var(--fs-space-2) var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3);
/* NO `container-type` HERE, and its absence is the fix for a page that jumped every five /* NO `container-type` HERE, and its absence is the fix for a page that jumped every five
@@ -9,7 +9,7 @@
* their own entry here. */ * their own entry here. */
body[data-page="admin-system-admin-sshkeys"] #view > div { body[data-page="admin-system-admin-sshkeys"] #view > div {
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow); box-shadow: var(--fs-shadow);
padding: var(--fs-space-4); padding: var(--fs-space-4);
@@ -76,7 +76,7 @@
margin: 0; margin: 0;
font-size: var(--fs-type); font-weight: var(--fs-weight-bold); color: var(--fs-text); font-size: var(--fs-type); font-weight: var(--fs-weight-bold); color: var(--fs-text);
} }
.fs-ap-section + .fs-ap-section { padding-top: var(--fs-space-4); border-top: 1px solid var(--fs-border); } .fs-ap-section + .fs-ap-section { padding-top: var(--fs-space-4); border-top: var(--fs-hairline); }
/* THE ROWS ARE STOCK `.cbi-value` NOW and this page states no layout for them at all — caption /* THE ROWS ARE STOCK `.cbi-value` NOW and this page states no layout for them at all — caption
* column, field column and the hairline between rows come from base/30-forms.css and * column, field column and the hairline between rows come from base/30-forms.css and
* theme/60-inputs.css, the same rules every other LuCI form is drawn by. What was here before was * theme/60-inputs.css, the same rules every other LuCI form is drawn by. What was here before was
@@ -117,7 +117,7 @@
* border. Fixed 34px square so it lines up with the field beside it at every density. */ * border. Fixed 34px square so it lines up with the field beside it at every density. */
input[type="color"].fs-color-swatch { input[type="color"].fs-color-swatch {
flex: 0 0 auto; inline-size: 34px; block-size: 34px; padding: 0; cursor: pointer; flex: 0 0 auto; inline-size: 34px; block-size: 34px; padding: 0; cursor: pointer;
background: none; border: 1px solid var(--fs-border); border-radius: var(--fs-radius-sm); background: none; border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
} }
input[type="color"].fs-color-swatch::-webkit-color-swatch-wrapper { padding: 0; } input[type="color"].fs-color-swatch::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"].fs-color-swatch::-webkit-color-swatch { border: 0; border-radius: calc(var(--fs-radius-sm) - 1px); } input[type="color"].fs-color-swatch::-webkit-color-swatch { border: 0; border-radius: calc(var(--fs-radius-sm) - 1px); }
@@ -162,7 +162,7 @@
* letterboxed photograph. */ * letterboxed photograph. */
.fs-ap-bgprev { .fs-ap-bgprev {
display: block; width: 100%; max-width: var(--fs-field-max); height: 84px; object-fit: cover; display: block; width: 100%; max-width: var(--fs-field-max); height: 84px; object-fit: cover;
border: 1px solid var(--fs-border); border-radius: var(--fs-radius); border: var(--fs-hairline); border-radius: var(--fs-radius);
/* the preview follows the buttons in the field, so the gap belongs ABOVE it */ /* the preview follows the buttons in the field, so the gap belongs ABOVE it */
margin-top: var(--fs-space-2); margin-top: var(--fs-space-2);
} }
@@ -189,7 +189,7 @@
/* the version line at the foot of the page */ /* the version line at the foot of the page */
.fs-ap-footer { .fs-ap-footer {
display: flex; flex-direction: column; gap: var(--fs-space-2); display: flex; flex-direction: column; gap: var(--fs-space-2);
margin-top: var(--fs-space-0-5); padding-top: var(--fs-space-2-5); border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-0-5); padding-top: var(--fs-space-2-5); border-top: var(--fs-hairline);
} }
.fs-ap-verrow { display: flex; align-items: center; justify-content: space-between; gap: var(--fs-space-2-5); } .fs-ap-verrow { display: flex; align-items: center; justify-content: space-between; gap: var(--fs-space-2-5); }
/* links to the project repo — reads as muted text until hovered */ /* links to the project repo — reads as muted text until hovered */
@@ -68,7 +68,7 @@
/* No accent fill — a bordered box, same look as the square buttons in the right /* No accent fill — a bordered box, same look as the square buttons in the right
* cluster (.fs-logout, .fs-themerow: 1px var(--fs-border)). The glyph is the OpenWrt * cluster (.fs-logout, .fs-themerow: 1px var(--fs-border)). The glyph is the OpenWrt
* favicon mark (brand.ut), baked-colour, so color/currentColor is unused here. */ * favicon mark (brand.ut), baked-colour, so color/currentColor is unused here. */
border: 1px solid var(--fs-border); border: var(--fs-hairline);
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
flex: 0 0 auto; flex: 0 0 auto;
} }
+10 -10
View File
@@ -118,7 +118,7 @@
background: var(--fs-bar-bg); background: var(--fs-bar-bg);
backdrop-filter: var(--fs-blur); backdrop-filter: var(--fs-blur);
-webkit-backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur);
border-inline-end: 0; border-bottom: 1px solid var(--fs-border); border-inline-end: 0; border-bottom: var(--fs-hairline);
/* the submenu popups anchor to this bar and must escape it */ /* the submenu popups anchor to this bar and must escape it */
overflow: visible; overflow: visible;
} }
@@ -175,7 +175,7 @@
margin: 0; padding: var(--fs-space-2); margin: 0; padding: var(--fs-space-2);
background: var(--fs-glass); background: var(--fs-glass);
backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur); backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop); border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop);
} }
/* an invisible bridge across the 8px gap, so travelling from the item down into the panel /* an invisible bridge across the 8px gap, so travelling from the item down into the panel
@@ -210,7 +210,7 @@
/* see the :not(:empty) rule below — two auto margins would SPLIT the free space between /* see the :not(:empty) rule below — two auto margins would SPLIT the free space between
* them instead of pushing one cluster to the bar's far edge */ * them instead of pushing one cluster to the bar's far edge */
width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0; width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0;
border: 1px solid var(--fs-border); border-radius: var(--fs-radius); border: var(--fs-hairline); border-radius: var(--fs-radius);
background: var(--fs-panel2); color: var(--fs-dim); background: var(--fs-panel2); color: var(--fs-dim);
font: inherit; font-weight: var(--fs-weight); font-size: var(--fs-type); line-height: 1.1; font: inherit; font-weight: var(--fs-weight); font-size: var(--fs-type); line-height: 1.1;
text-align: start; cursor: pointer; text-align: start; cursor: pointer;
@@ -280,7 +280,7 @@
.fs-logout { .fs-logout {
flex: 0 0 auto; order: 2; justify-content: center; flex: 0 0 auto; order: 2; justify-content: center;
width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0; width: var(--fs-btn-size); height: var(--fs-btn-size); padding: 0;
border: 1px solid var(--fs-border); border-radius: var(--fs-radius); border: var(--fs-hairline); border-radius: var(--fs-radius);
background: var(--fs-panel2); background: var(--fs-panel2);
} }
/* on a square button the BORDER carries the state — the fill and the ink come from the /* on a square button the BORDER carries the state — the fill and the ink come from the
@@ -351,7 +351,7 @@
gap: normal; padding: var(--fs-space-5) var(--fs-space-3-5); gap: normal; padding: var(--fs-space-5) var(--fs-space-3-5);
background: var(--fs-panel); background: var(--fs-panel);
backdrop-filter: none; -webkit-backdrop-filter: none; backdrop-filter: none; -webkit-backdrop-filter: none;
border-inline-end: 1px solid var(--fs-border); border-bottom: 0; border-inline-end: var(--fs-hairline); border-bottom: 0;
box-shadow: none; box-shadow: none;
/* `overflow: auto`, not overflow-y alone: the bar sets `overflow: visible` for its /* `overflow: auto`, not overflow-y alone: the bar sets `overflow: visible` for its
* popups, and a lone overflow-y here would leave overflow-x visible — which the * popups, and a lone overflow-y here would leave overflow-x visible — which the
@@ -440,7 +440,7 @@
display: flex; display: flex;
margin: var(--fs-space-1) 0 var(--fs-space-3) var(--fs-space-7); margin: var(--fs-space-1) 0 var(--fs-space-3) var(--fs-space-7);
padding-inline-start: var(--fs-space-1); padding-inline-start: var(--fs-space-1);
border-inline-start: 1px solid var(--fs-border); border-inline-start: var(--fs-hairline);
} }
:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.has-sub.open > ul { display: flex; } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav > li.has-sub.open > ul { display: flex; }
:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul a { padding: var(--fs-space-2) var(--fs-space-3); } :root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul a { padding: var(--fs-space-2) var(--fs-space-3); }
@@ -544,7 +544,7 @@
position: absolute; inset-inline-start: 100%; top: 50%; transform: translateY(-50%); position: absolute; inset-inline-start: 100%; top: 50%; transform: translateY(-50%);
margin-inline-start: var(--fs-space-2-5); z-index: var(--fs-z-flyout); margin-inline-start: var(--fs-space-2-5); z-index: var(--fs-z-flyout);
padding: var(--fs-space-1-5) var(--fs-space-2-5); border-radius: var(--fs-radius-sm); padding: var(--fs-space-1-5) var(--fs-space-2-5); border-radius: var(--fs-radius-sm);
background: var(--fs-panel); border: 1px solid var(--fs-border); background: var(--fs-panel); border: var(--fs-hairline);
box-shadow: var(--fs-shadow-pop); box-shadow: var(--fs-shadow-pop);
color: var(--fs-text); font-size: var(--fs-type); font-weight: var(--fs-weight); white-space: nowrap; color: var(--fs-text); font-size: var(--fs-type); font-weight: var(--fs-weight); white-space: nowrap;
opacity: 0; pointer-events: none; transition: opacity var(--fs-dur); opacity: 0; pointer-events: none; transition: opacity var(--fs-dur);
@@ -559,7 +559,7 @@
display: none; display: none;
position: absolute; inset-inline-start: 100%; top: -8px; margin: 0; margin-inline-start: var(--fs-space-2-5); position: absolute; inset-inline-start: 100%; top: -8px; margin: 0; margin-inline-start: var(--fs-space-2-5);
min-width: 196px; padding: var(--fs-space-2); z-index: var(--fs-z-flyout); min-width: 196px; padding: var(--fs-space-2); z-index: var(--fs-z-flyout);
background: var(--fs-panel); border: 1px solid var(--fs-border); border-radius: var(--fs-radius-lg); background: var(--fs-panel); border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop); box-shadow: var(--fs-shadow-pop);
animation: fs-fade var(--fs-dur); animation: fs-fade var(--fs-dur);
} }
@@ -733,7 +733,7 @@
* from base, which keeps only the flex skeleton), so a third-party view emitting its own * from base, which keeps only the flex skeleton), so a third-party view emitting its own
* <footer> stays themed too. */ * <footer> stays themed too. */
footer { footer {
padding: var(--fs-space-3-5) var(--fs-content-pad); border-top: 1px solid var(--fs-border); padding: var(--fs-space-3-5) var(--fs-content-pad); border-top: var(--fs-hairline);
color: var(--fs-dim); font-size: var(--fs-type); background: var(--fs-bg); color: var(--fs-dim); font-size: var(--fs-type); background: var(--fs-bg);
/* CENTRE THE FLEX LINE, not just the text: `text-align: center` alone did nothing. /* CENTRE THE FLEX LINE, not just the text: `text-align: center` alone did nothing.
* base makes the footer a flex container and used to add `justify-content: * base makes the footer a flex container and used to add `justify-content:
@@ -822,7 +822,7 @@
text-transform: none; text-transform: none;
font-weight: var(--fs-weight); font-size: var(--fs-type); font-weight: var(--fs-weight); font-size: var(--fs-type);
background: var(--fs-panel2); background-image: none; background: var(--fs-panel2); background-image: none;
border: 1px solid var(--fs-border); border: var(--fs-hairline);
color: var(--fs-dim); color: var(--fs-dim);
border-radius: var(--fs-radius); padding: 0 var(--fs-space-3); box-shadow: none; border-radius: var(--fs-radius); padding: 0 var(--fs-space-3); box-shadow: none;
} }
@@ -2,7 +2,7 @@
/* ---------- panels / cards (cbi) ---------- */ /* ---------- panels / cards (cbi) ---------- */
.cbi-section, .cbi-map > .cbi-section-node, fieldset.cbi-section, .panel { .cbi-section, .cbi-map > .cbi-section-node, fieldset.cbi-section, .panel {
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow); box-shadow: var(--fs-shadow);
} }
@@ -96,7 +96,7 @@
.cbi-section .table .td, .cbi-section .table .td,
.cbi-section .table .th { .cbi-section .table .th {
padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-2-5) 0; padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-2-5) 0;
border: 0; border-bottom: 1px solid var(--fs-border); border: 0; border-bottom: var(--fs-hairline);
line-height: 1.45; vertical-align: middle; line-height: 1.45; vertical-align: middle;
text-align: start; text-align: start;
} }
@@ -269,7 +269,7 @@
.cbi-section .table[id], #view .table[id], .cbi-section .table[id], #view .table[id],
.cbi-section .table.fs-dt, .cbi-section table.fs-dt, table.fs-dt { .cbi-section .table.fs-dt, .cbi-section table.fs-dt, table.fs-dt {
border-collapse: separate; border-spacing: 0; border-collapse: separate; border-spacing: 0;
border: 1px solid var(--fs-border); border-radius: var(--fs-radius-lg); border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
/* EXPLICIT, not an omission: the generic `.table` rule below still says `overflow: hidden` /* EXPLICIT, not an omission: the generic `.table` rule below still says `overflow: hidden`
* — which clipped the last column of a wide table — and not repeating it here does not * — which clipped the last column of a wide table — and not repeating it here does not
* stop it applying, which is what kept the bug alive. The corner cells (below) round the * stop it applying, which is what kept the bug alive. The corner cells (below) round the
@@ -439,7 +439,7 @@
/* Standalone data tables (Startup, Processes, Routes) -> rounded card. Inside a CBI section /* Standalone data tables (Startup, Processes, Routes) -> rounded card. Inside a CBI section
* the section already IS the card, so the frame is stripped there (above): no double border. */ * the section already IS the card, so the frame is stripped there (above): no double border. */
.table { .table {
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
overflow: hidden; overflow: hidden;
background: var(--fs-panel); background: var(--fs-panel);
@@ -458,9 +458,9 @@
* line the full width and pokes past the frame's rounded bottom corners (overflow: visible). */ * line the full width and pokes past the frame's rounded bottom corners (overflow: visible). */
.table .tr.table-titles > .th, .table .tr.table-titles > .th,
.table .tr.cbi-section-table-titles > .th, .table .tr.cbi-section-table-titles > .th,
.table .tr.cbi-section-table-titles::before { border-bottom: 1px solid var(--fs-border); } .table .tr.cbi-section-table-titles::before { border-bottom: var(--fs-hairline); }
.table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child) > .td, .table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child) > .td,
.table .tr.cbi-section-table-row:not(:last-child)::before { border-bottom: 1px solid var(--fs-border); } .table .tr.cbi-section-table-row:not(:last-child)::before { border-bottom: var(--fs-hairline); }
/* descendant combinator (not >) so a real <table>'s browser-inserted <tbody> is covered /* descendant combinator (not >) so a real <table>'s browser-inserted <tbody> is covered
* too — one rule themes every data table, div- or table-based. */ * too — one rule themes every data table, div- or table-based. */
.table .tr.table-titles, .table .tr.cbi-section-table-titles { .table .tr.table-titles, .table .tr.cbi-section-table-titles {
@@ -767,7 +767,7 @@
#modal_overlay .table.fs-xscroll tr > :first-child { #modal_overlay .table.fs-xscroll tr > :first-child {
position: sticky; left: 0; z-index: 2; position: sticky; left: 0; z-index: 2;
background: var(--fs-panel); background: var(--fs-panel);
border-inline-end: 1px solid var(--fs-border); border-inline-end: var(--fs-hairline);
} }
/* a keyboard user reaches the scroll box by tabbing to it (fs-select.js gives it tabindex and a /* a keyboard user reaches the scroll box by tabbing to it (fs-select.js gives it tabindex and a
* name); it has to show that it has focus — outline, not box-shadow, so forced-colors keeps it */ * name); it has to show that it has focus — outline, not box-shadow, so forced-colors keeps it */
@@ -6,7 +6,7 @@
.alert-message { .alert-message {
padding: var(--fs-space-3-5) var(--fs-space-4); padding: var(--fs-space-3-5) var(--fs-space-4);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
/* logical: the stripe marks the READING start of the alert, so it swaps sides with the /* logical: the stripe marks the READING start of the alert, so it swaps sides with the
* document (LuCI ships ar/fa/he catalogues) — physical, it sat on the far edge in RTL. */ * document (LuCI ships ar/fa/he catalogues) — physical, it sat on the far edge in RTL. */
border-inline-start: 3px solid var(--fs-accent); border-inline-start: 3px solid var(--fs-accent);
@@ -201,7 +201,7 @@
.label { .label {
display: inline-block; padding: var(--fs-space-1) var(--fs-space-2); border-radius: var(--fs-radius-pill); display: inline-block; padding: var(--fs-space-1) var(--fs-space-2); border-radius: var(--fs-radius-pill);
font-size: var(--fs-type-xs); font-weight: var(--fs-weight); line-height: 1.4; font-size: var(--fs-type-xs); font-weight: var(--fs-weight); line-height: 1.4;
background: var(--fs-panel2); border: 1px solid var(--fs-border); background: var(--fs-panel2); border: var(--fs-hairline);
color: var(--fs-dim); text-shadow: none; text-transform: none; color: var(--fs-dim); text-shadow: none; text-transform: none;
/* carried over from the absorbed styles/base .label rule */ /* carried over from the absorbed styles/base .label rule */
white-space: nowrap; margin: .125em 0 .125em .4em; white-space: nowrap; margin: .125em 0 .125em .4em;
@@ -228,7 +228,7 @@
.cbi-title .label { .cbi-title .label {
font-size: var(--fs-type-xs); font-weight: var(--fs-weight); font-size: var(--fs-type-xs); font-weight: var(--fs-weight);
padding: var(--fs-space-1) var(--fs-space-3); border-radius: var(--fs-radius-pill); padding: var(--fs-space-1) var(--fs-space-3); border-radius: var(--fs-radius-pill);
background: var(--fs-panel2); border: 1px solid var(--fs-border); background: var(--fs-panel2); border: var(--fs-hairline);
color: var(--fs-dim); cursor: pointer; line-height: 1.4; color: var(--fs-dim); cursor: pointer; line-height: 1.4;
text-transform: none; letter-spacing: 0; text-transform: none; letter-spacing: 0;
margin: 0; min-width: 0; box-shadow: none; margin: 0; min-width: 0; box-shadow: none;
@@ -23,7 +23,7 @@
margin: 0 0 var(--fs-card-gap); margin: 0 0 var(--fs-card-gap);
padding: var(--fs-space-2); padding: var(--fs-space-2);
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow); box-shadow: var(--fs-shadow);
/* kills a folder-tab baseline gradient (the 1px x 29px background image the strip /* kills a folder-tab baseline gradient (the 1px x 29px background image the strip
@@ -187,7 +187,7 @@
.cbi-map > h2, .cbi-map > h2,
#view > h2 { #view > h2 {
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow); box-shadow: var(--fs-shadow);
margin: 0 0 var(--fs-card-gap); margin: 0 0 var(--fs-card-gap);
@@ -209,7 +209,7 @@
.cbi-map > h2 + .cbi-map-descr, .cbi-map > h2 + .cbi-map-descr,
#view > h2 + .cbi-map-descr { #view > h2 + .cbi-map-descr {
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-top: 0; border-top: 0;
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg); border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
box-shadow: var(--fs-shadow); box-shadow: var(--fs-shadow);
@@ -22,12 +22,12 @@
font-family: var(--fs-font-sans); font-weight: var(--fs-weight); line-height: 1.1; font-family: var(--fs-font-sans); font-weight: var(--fs-weight); line-height: 1.1;
color: var(--fs-text); color: var(--fs-text);
background: var(--fs-panel2); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
box-shadow: none; text-shadow: none; box-shadow: none; text-shadow: none;
/* border-COLOR, not `border`: the shorthand animates border-width too — a layout /* border-COLOR, not `border`: the shorthand animates border-width too — a layout
* animation on every hover instead of a paint one (absorbed from base with the ring) */ * animation on every hover instead of a paint one (absorbed from base with the ring) */
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* bare `button` rides along: a third-party app's unclassed <button> hovered the same border /* bare `button` rides along: a third-party app's unclassed <button> hovered the same border
* cue in base's old glow rule, and border-color is a no-op on its border: 0 box — harmless * cue in base's old glow rule, and border-color is a no-op on its border: 0 box — harmless
@@ -111,7 +111,7 @@
.cbi-section .table[id] .td .cbi-button { padding: var(--fs-space-1) var(--fs-space-2-5); font-size: var(--fs-type); } .cbi-section .table[id] .td .cbi-button { padding: var(--fs-space-1) var(--fs-space-2-5); font-size: var(--fs-type); }
.alert-message .btn, .alert-message .cbi-button { .alert-message .btn, .alert-message .cbi-button {
border-radius: var(--fs-radius); padding: var(--fs-space-2) var(--fs-space-3); font-weight: var(--fs-weight); border-radius: var(--fs-radius); padding: var(--fs-space-2) var(--fs-space-3); font-weight: var(--fs-weight);
background: var(--fs-panel2); border: 1px solid var(--fs-border); color: var(--fs-text); background: var(--fs-panel2); border: var(--fs-hairline); color: var(--fs-text);
} }
.alert-message .btn:hover { border-color: var(--fs-accent); color: var(--fs-accent); } .alert-message .btn:hover { border-color: var(--fs-accent); color: var(--fs-accent); }
@@ -168,7 +168,7 @@
margin-inline-end: var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3-5); margin-inline-end: var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3-5);
font-family: var(--fs-font-sans); font-size: var(--fs-type); font-weight: var(--fs-weight); font-family: var(--fs-font-sans); font-size: var(--fs-type); font-weight: var(--fs-weight);
color: var(--fs-text); background: var(--fs-panel2); color: var(--fs-text); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border-radius: var(--fs-radius-sm); cursor: pointer; border: var(--fs-hairline); border-radius: var(--fs-radius-sm); cursor: pointer;
} }
input[type="file"]::file-selector-button:hover { border-color: var(--fs-accent); color: var(--fs-accent); } input[type="file"]::file-selector-button:hover { border-color: var(--fs-accent); color: var(--fs-accent); }
@@ -190,7 +190,7 @@
} }
.td .cbi-button, .cbi-section-actions .cbi-button, .cbi-section-table .cbi-button { .td .cbi-button, .cbi-section-actions .cbi-button, .cbi-section-table .cbi-button {
background: transparent; background-image: none; background: transparent; background-image: none;
border: 1px solid var(--fs-border); color: var(--fs-dim); border: var(--fs-hairline); color: var(--fs-dim);
} }
.td .cbi-button:hover { border-color: var(--fs-dim); color: var(--fs-text); } .td .cbi-button:hover { border-color: var(--fs-dim); color: var(--fs-text); }
@@ -276,7 +276,7 @@
* is the drift this ladder exists to prevent, and it also sat out the Density axis. */ * is the drift this ladder exists to prevent, and it also sat out the Density axis. */
flex: 0 0 auto; width: auto; min-width: var(--fs-ctl-h); flex: 0 0 auto; width: auto; min-width: var(--fs-ctl-h);
min-height: var(--fs-ctl-h-sm); padding: 0 var(--fs-space-3); margin: 0; min-height: var(--fs-ctl-h-sm); padding: 0 var(--fs-space-3); margin: 0;
border: 0; border-inline-start: 1px solid var(--fs-border); border: 0; border-inline-start: var(--fs-hairline);
border-radius: 0; border-radius: 0;
background: transparent; color: var(--fs-dim); background: transparent; color: var(--fs-dim);
} }
@@ -286,7 +286,7 @@
border: 0; border-radius: 0; margin: 0; border: 0; border-radius: 0; margin: 0;
background: transparent; color: var(--fs-dim); background: transparent; color: var(--fs-dim);
padding: 0 var(--fs-space-3); min-height: var(--fs-ctl-h); padding: 0 var(--fs-space-3); min-height: var(--fs-ctl-h);
border-inline-start: 1px solid var(--fs-border); border-inline-start: var(--fs-hairline);
} }
.control-group:has(> .cbi-input-password) > button.cbi-button-neutral:hover { color: var(--fs-accent); } .control-group:has(> .cbi-input-password) > button.cbi-button-neutral:hover { color: var(--fs-accent); }
/* SQUARE, ON THE LADDER: these were the only buttons in the theme set in bare pixels /* SQUARE, ON THE LADDER: these were the only buttons in the theme set in bare pixels
@@ -296,7 +296,7 @@
* --fs-density-box and the pager did not. Square because the glyph is an arrow, not a label. */ * --fs-density-box and the pager did not. Square because the glyph is an arrow, not a label. */
.pager .btn.prev, .pager .btn.next { .pager .btn.prev, .pager .btn.next {
flex: 0 0 auto; width: var(--fs-ctl-h); min-width: var(--fs-ctl-h); height: var(--fs-ctl-h); padding: 0; flex: 0 0 auto; width: var(--fs-ctl-h); min-width: var(--fs-ctl-h); height: var(--fs-ctl-h); padding: 0;
border-radius: var(--fs-radius); border: 1px solid var(--fs-border); border-radius: var(--fs-radius); border: var(--fs-hairline);
background: var(--fs-panel2); color: var(--fs-text); background: var(--fs-panel2); color: var(--fs-text);
display: inline-flex; align-items: center; justify-content: center; display: inline-flex; align-items: center; justify-content: center;
} }
+21 -21
View File
@@ -5,7 +5,7 @@
* sits under it, and center then pushed the label to the middle of control+description, * sits under it, and center then pushed the label to the middle of control+description,
* visibly BELOW the control. baseline lines the label up with the field's FIRST line for * visibly BELOW the control. baseline lines the label up with the field's FIRST line for
* every widget (select, dropdown, toggle, dynlist, textarea). */ * every widget (select, dropdown, toggle, dynlist, textarea). */
.cbi-value { justify-content: space-between; gap: var(--fs-space-4); padding: var(--fs-space-2) 0; margin: 0; border-bottom: 1px solid var(--fs-border); } .cbi-value { justify-content: space-between; gap: var(--fs-space-4); padding: var(--fs-space-2) 0; margin: 0; border-bottom: var(--fs-hairline); }
/* stacked controls in a field (Local Time: date input + Sync buttons) need breathing room — /* stacked controls in a field (Local Time: date input + Sync buttons) need breathing room —
* the raw <br> collapses them. Exclude the password reveal group: it must stay seamless. */ * the raw <br> collapses them. Exclude the password reveal group: it must stay seamless. */
.cbi-value-field > .control-group { margin-top: var(--fs-space-2); } .cbi-value-field > .control-group { margin-top: var(--fs-space-2); }
@@ -28,7 +28,7 @@
* 10.8 -> 13px on the firewall). border-COLOR, not `border`: the shorthand animates * 10.8 -> 13px on the firewall). border-COLOR, not `border`: the shorthand animates
* border-width too — a layout animation on every hover instead of a paint one. */ * border-width too — a layout animation on every hover instead of a paint one. */
input, button { input, button {
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* keep base's padding-top (--fs-space-1) — it is tuned for base's align-items: baseline. */ /* keep base's padding-top (--fs-space-1) — it is tuned for base's align-items: baseline. */
@@ -181,7 +181,7 @@
--sw-pad: calc(2px * var(--fs-density-box)); --sw-pad: calc(2px * var(--fs-density-box));
display: inline-block; position: relative; margin: 0; display: inline-block; position: relative; margin: 0;
width: var(--sw-w); height: var(--sw-h); border-radius: var(--fs-radius-pill); width: var(--sw-w); height: var(--sw-h); border-radius: var(--fs-radius-pill);
background: var(--fs-track); border: 1px solid var(--fs-border); background: var(--fs-track); border: var(--fs-hairline);
cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur); vertical-align: middle; cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur); vertical-align: middle;
} }
/* The input is opacity:0/0x0, so this label IS the whole hit box of LuCI's most common /* The input is opacity:0/0x0, so this label IS the whole hit box of LuCI's most common
@@ -308,7 +308,7 @@
height: var(--box); height: var(--box);
background: linear-gradient(var(--fs-panel), var(--fs-panel2)); background: linear-gradient(var(--fs-panel), var(--fs-panel2));
border: 1px solid var(--bd-color); border: 1px solid var(--bd-color);
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* each box states its own radius — one shared 2px re-declared to 50% for the radio would /* each box states its own radius — one shared 2px re-declared to 50% for the radio would
* be a same-selector shadow inside this layer */ * be a same-selector shadow inside this layer */
@@ -376,7 +376,7 @@
padding: var(--fs-space-1) var(--fs-space-2); padding: var(--fs-space-1) var(--fs-space-2);
background: var(--fs-panel); background: var(--fs-panel);
color: var(--fs-text); color: var(--fs-text);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
} }
/* base's old `#localtime { width: 300px }` media rule was NOT moved here: the elastic /* base's old `#localtime { width: 300px }` media rule was NOT moved here: the elastic
@@ -410,7 +410,7 @@
* position and two spellings of one number is how they would drift apart */ * position and two spellings of one number is how they would drift apart */
background-repeat: no-repeat; background-position: right var(--fs-space-3) center; background-size: 12px; background-repeat: no-repeat; background-position: right var(--fs-space-3) center; background-size: 12px;
padding: var(--fs-space-2) var(--fs-space-8) var(--fs-space-2) var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-8) var(--fs-space-2) var(--fs-space-3);
border: 1px solid var(--fs-border); border-radius: var(--fs-radius); border: var(--fs-hairline); border-radius: var(--fs-radius);
color: var(--fs-text); font-family: var(--fs-font-sans); font-size: var(--fs-type); color: var(--fs-text); font-family: var(--fs-font-sans); font-size: var(--fs-type);
line-height: 1.3; min-height: var(--fs-ctl-h); cursor: pointer; line-height: 1.3; min-height: var(--fs-ctl-h); cursor: pointer;
/* width/display absorbed from base's generic field box: a select's natural width is its /* width/display absorbed from base's generic field box: a select's natural width is its
@@ -419,7 +419,7 @@
* inline-block the widget has always computed */ * inline-block the widget has always computed */
width: 210px; width: 210px;
display: inline-block; display: inline-block;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* group-then-refine, directly under its group: the listbox variant must NOT take the closed /* group-then-refine, directly under its group: the listbox variant must NOT take the closed
* control's one-line height. A SURFACE token, not an ink one: this once read --fs-on-accent * control's one-line height. A SURFACE token, not an ink one: this once read --fs-on-accent
@@ -449,7 +449,7 @@
.cbi-value-field select::picker-icon { display: none; } .cbi-value-field select::picker-icon { display: none; }
::picker(select) { ::picker(select) {
appearance: base-select; appearance: base-select;
background: var(--fs-panel); border: 1px solid var(--fs-border); background: var(--fs-panel); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop); border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow-pop);
padding: var(--fs-space-1-5); margin-top: var(--fs-space-1-5); padding: var(--fs-space-1-5); margin-top: var(--fs-space-1-5);
} }
@@ -500,7 +500,7 @@
height: var(--fs-ctl-h); height: var(--fs-ctl-h);
color: var(--fs-text); color: var(--fs-text);
font-size: var(--fs-type); font-size: var(--fs-type);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
padding: 0; padding: 0;
position: relative; position: relative;
@@ -509,7 +509,7 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right var(--fs-space-3) center; background-position: right var(--fs-space-3) center;
background-size: 12px; background-size: 12px;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* The disabled cue used to live ENTIRELY on the ▾ plate: base/30-forms.css dims /* The disabled cue used to live ENTIRELY on the ▾ plate: base/30-forms.css dims
* `.cbi-select[disabled]::before` and never the shell itself. With the plate gone the state had * `.cbi-select[disabled]::before` and never the shell itself. With the plate gone the state had
@@ -571,13 +571,13 @@
input[type="color"], input[type="color"],
textarea { textarea {
background: var(--fs-panel2); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
color: var(--fs-text); color: var(--fs-text);
padding: var(--fs-space-2) var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3);
min-height: var(--fs-ctl-h); min-height: var(--fs-ctl-h);
box-shadow: none; box-shadow: none;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* width/font-size absorbed from base's generic field box, same numbers — a textarea has /* width/font-size absorbed from base's generic field box, same numbers — a textarea has
* always sat at the fixed 210px (the elastic rule above names only text/url/search) */ * always sat at the fixed 210px (the elastic rule above names only text/url/search) */
@@ -707,12 +707,12 @@
} }
.cbi-dynlist > .item { .cbi-dynlist > .item {
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-sm); border-radius: var(--fs-radius-sm);
color: var(--fs-text); color: var(--fs-text);
box-shadow: none; box-shadow: none;
margin: 0; margin: 0;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* a chip is keyboard-removable (ui.js gives it a tabindex), so it needs the ring */ /* a chip is keyboard-removable (ui.js gives it a tabindex), so it needs the ring */
.cbi-dynlist > .item:focus { .cbi-dynlist > .item:focus {
@@ -730,9 +730,9 @@
padding: 0 var(--fs-space-3); padding: 0 var(--fs-space-3);
color: var(--fs-text); color: var(--fs-text);
line-height: 2em; line-height: 2em;
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
/* the hover cue is the lift, not a colour: the plate's fill is a gradient (see /* the hover cue is the lift, not a colour: the plate's fill is a gradient (see
* --fs-hover-lift in 02-tokens.css) — replaces base's old focus-glow-on-hover */ * --fs-hover-lift in 02-tokens.css) — replaces base's old focus-glow-on-hover */
@@ -748,9 +748,9 @@
/* text add-item = one bordered input group (input + + button attached) */ /* text add-item = one bordered input group (input + + button attached) */
.cbi-dynlist > .add-item { .cbi-dynlist > .add-item {
align-items: stretch; align-items: stretch;
background: var(--fs-panel2); border: 1px solid var(--fs-border); background: var(--fs-panel2); border: var(--fs-hairline);
border-radius: var(--fs-radius-sm); overflow: hidden; border-radius: var(--fs-radius-sm); overflow: hidden;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
.cbi-dynlist > .add-item:focus-within { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); } .cbi-dynlist > .add-item:focus-within { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); }
.cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > input,
@@ -768,10 +768,10 @@
.control-group:has(> .cbi-input-password) { .control-group:has(> .cbi-input-password) {
display: inline-flex; align-items: stretch; gap: 0; display: inline-flex; align-items: stretch; gap: 0;
background: var(--fs-panel2); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
overflow: hidden; overflow: hidden;
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
} }
.control-group:has(> .cbi-input-password) > .cbi-input-password { .control-group:has(> .cbi-input-password) > .cbi-input-password {
border: 0; border-radius: 0; border: 0; border-radius: 0;
@@ -829,7 +829,7 @@
* to the card's right edge. Every stretching control in a form row now ends at one x. */ * to the card's right edge. Every stretching control in a form row now ends at one x. */
.cbi-range-slider { display: inline-flex; align-items: center; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); } .cbi-range-slider { display: inline-flex; align-items: center; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
.cbi-range-slider-value { .cbi-range-slider-value {
background: var(--fs-panel2); border: 1px solid var(--fs-border); border-radius: var(--fs-radius-sm); background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono); padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono);
/* NOT 600: no face is bundled, so 600 either drags a 20 KB file back in or is faked by /* NOT 600: no face is bundled, so 600 either drags a 20 KB file back in or is faked by
* synthetic emboldening, which smears a monospace grid. * synthetic emboldening, which smears a monospace grid.
@@ -130,7 +130,7 @@
.cbi-dropdown > ul > li .hide-close { display: none; } .cbi-dropdown > ul > li .hide-close { display: none; }
/* the chip divider on a BUTTON dropdown's preview; the form dropdown zeroes it below */ /* the chip divider on a BUTTON dropdown's preview; the form dropdown zeroes it below */
.cbi-dropdown > ul > li[display]:not([display="0"]) { .cbi-dropdown > ul > li[display]:not([display="0"]) {
border-inline-start: 1px solid var(--fs-border); border-inline-start: var(--fs-hairline);
} }
.cbi-dropdown[empty] > ul { .cbi-dropdown[empty] > ul {
max-width: 1px; max-width: 1px;
@@ -206,7 +206,7 @@
.cbi-dropdown:not(.btn):not(.cbi-button) { .cbi-dropdown:not(.btn):not(.cbi-button) {
background: var(--fs-panel2); background: var(--fs-panel2);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius); border-radius: var(--fs-radius);
color: var(--fs-text); color: var(--fs-text);
min-height: var(--fs-ctl-h); min-height: var(--fs-ctl-h);
@@ -216,7 +216,7 @@
* 38 -> 42px on the gallery). */ * 38 -> 42px on the gallery). */
height: var(--fs-control-h); height: var(--fs-control-h);
font-size: var(--fs-type); font-size: var(--fs-type);
transition: border-color var(--fs-dur), box-shadow var(--fs-dur); transition: var(--fs-field-transition);
/* Width absorbed from base/30-forms.css, where it was grouped with .cbi-dynlist's — so the /* Width absorbed from base/30-forms.css, where it was grouped with .cbi-dynlist's — so the
* theme sized the dynlist and base still sized the dropdown, two halves of the same * theme sized the dynlist and base still sized the dropdown, two halves of the same
* decision in two layers. Same numbers, and they are NOT the dynlist's (240/440): a * decision in two layers. Same numbers, and they are NOT the dynlist's (240/440): a
@@ -325,7 +325,7 @@
transition: max-height var(--fs-dur-move); transition: max-height var(--fs-dur-move);
overflow-y: auto; overflow-y: auto;
background: var(--fs-panel); background: var(--fs-panel);
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop); box-shadow: var(--fs-shadow-pop);
padding: var(--fs-space-1-5); padding: var(--fs-space-1-5);
@@ -410,10 +410,10 @@
box-shadow: inset 0 0 0 2px var(--fs-accent-soft); box-shadow: inset 0 0 0 2px var(--fs-accent-soft);
} }
.cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-1); } .cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: var(--fs-hairline); margin-top: var(--fs-space-1); }
/* The dropdown's "add item" field is a text input and takes the same radius as every other /* The dropdown's "add item" field is a text input and takes the same radius as every other
* one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */ * one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */
.cbi-dropdown .create-item-input { background: var(--fs-panel2); border: 1px solid var(--fs-border); border-radius: var(--fs-radius); color: var(--fs-text); } .cbi-dropdown .create-item-input { background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius); color: var(--fs-text); }
/* dropdown add-item (e.g. Community Lists) — the .cbi-dropdown styles itself, so the /* dropdown add-item (e.g. Community Lists) — the .cbi-dropdown styles itself, so the
* wrapper stays transparent: no double frame. */ * wrapper stays transparent: no double frame. */
.cbi-dynlist > .add-item:has(> .cbi-dropdown) { .cbi-dynlist > .add-item:has(> .cbi-dropdown) {
@@ -457,7 +457,7 @@
* same height when one wraps; see the data card's .tr in theme/30-tables.css. */ * same height when one wraps; see the data card's .tr in theme/30-tables.css. */
.table.cbi-section-table .tr { .table.cbi-section-table .tr {
display: flex; flex-wrap: wrap; width: auto; display: flex; flex-wrap: wrap; width: auto;
border: 1px solid var(--fs-border); border: var(--fs-hairline);
border-radius: var(--fs-radius-lg); border-radius: var(--fs-radius-lg);
background: var(--fs-panel2); background: var(--fs-panel2);
padding: var(--fs-space-2) var(--fs-space-3-5) var(--fs-space-3); padding: var(--fs-space-2) var(--fs-space-3-5) var(--fs-space-3);
@@ -27,7 +27,7 @@
width: 100%; max-width: 560px; max-height: min(70dvh, 560px); width: 100%; max-width: 560px; max-height: min(70dvh, 560px);
background: var(--fs-glass); background: var(--fs-glass);
backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur); backdrop-filter: var(--fs-blur); -webkit-backdrop-filter: var(--fs-blur);
border: 1px solid var(--fs-border); border-radius: var(--fs-radius-lg); border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop); box-shadow: var(--fs-shadow-pop);
overflow: hidden; overflow: hidden;
} }
@@ -35,7 +35,7 @@
.fs-search-row { .fs-search-row {
display: flex; align-items: center; gap: var(--fs-space-2-5); flex: 0 0 auto; display: flex; align-items: center; gap: var(--fs-space-2-5); flex: 0 0 auto;
padding: var(--fs-space-3) var(--fs-space-3-5); padding: var(--fs-space-3) var(--fs-space-3-5);
border-bottom: 1px solid var(--fs-border); border-bottom: var(--fs-hairline);
} }
.fs-search-ico { display: inline-flex; color: var(--fs-faint); } .fs-search-ico { display: inline-flex; color: var(--fs-faint); }
.fs-search-ico .fs-ico { width: 18px; height: 18px; } .fs-search-ico .fs-ico { width: 18px; height: 18px; }
@@ -463,7 +463,10 @@
what every browser accepts a manifest as. There is no per-extension setting to fix from what every browser accepts a manifest as. There is no per-extension setting to fix from
here: the map is compiled into uhttpd. #} here: the map is compiled into uhttpd. #}
<link rel="manifest" href="{{ media }}/manifest.json?v={{ pkgs_update_time }}"> <link rel="manifest" href="{{ media }}/manifest.json?v={{ pkgs_update_time }}">
<link rel="apple-touch-icon" href="{{ media }}/apple-touch-icon.png"> {# iOS reads THIS link and never the manifest's icons array — and it is happy with the 512
square the manifest also uses, which it scales. One raster serves both, see
tools/build-icons.mjs. #}
<link rel="apple-touch-icon" href="{{ media }}/app-icon-512.png">
{% if (dispatched?.css): %} {% if (dispatched?.css): %}
{# The `css` property of the dispatched menu.d node — an app naming a stylesheet the server {# The `css` property of the dispatched menu.d node — an app naming a stylesheet the server
should link for its page, instead of the view injecting one at module eval (which is what should link for its page, instead of the view injecting one at module eval (which is what
+1 -1
View File
@@ -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:=6 PKG_RELEASE:=7
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
@@ -155,8 +155,7 @@ if [ -z "$version" ] || [ "$version" -lt 1 ]; then
uci -q set dhcp.@dnsmasq[0].rebind_protection='0' uci -q set dhcp.@dnsmasq[0].rebind_protection='0'
uci -q set dhcp.@dnsmasq[0].rebind_localhost='1' uci -q set dhcp.@dnsmasq[0].rebind_localhost='1'
uci commit dhcp uci commit dhcp
uci -q set system.@system[0].zram_comp_algo='zstd'
uci -q set system.@system[0].zram_size_mb="$((memtotal / 1024 / 3))" uci -q set system.@system[0].zram_size_mb="$((memtotal / 1024 / 3))"
uci commit system uci commit system
+9 -5
View File
@@ -24,7 +24,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=wwand PKG_NAME:=wwand
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
@@ -58,7 +58,7 @@ CMAKE_OPTIONS += \
# every install section takes the ucode tree from here: the bytecode output of # every install section takes the ucode tree from here: the bytecode output of
# the cmake build, or the plain sources when CONFIG_WWAND_UCODE_SOURCE is set. # the cmake build, or the plain sources when CONFIG_WWAND_UCODE_SOURCE is set.
WWAND_UCODE=$(if $(CONFIG_WWAND_UCODE_SOURCE),$(PKG_BUILD_DIR)/src-ucode,$(PKG_BUILD_DIR)/$(CMAKE_BINARY_SUBDIR)/ucode/wwand) WWAND_UCODE=$(if $(CONFIG_WWAND_UCODE_SOURCE),$(PKG_BUILD_DIR)/src-ucode,$(CMAKE_BINARY_DIR)/ucode/wwand)
UCDIR:=/usr/share/ucode/wwand UCDIR:=/usr/share/ucode/wwand
@@ -188,7 +188,8 @@ endef
define Package/wwand-qmi/description define Package/wwand-qmi/description
QMI control backend for wwand: talks QMI natively over /dev/cdc-wdmX (qmi_wwan QMI control backend for wwand: talks QMI natively over /dev/cdc-wdmX (qmi_wwan
driver) with QMAP multiplexing (rmnet), without spawning uqmi/qmicli. This is driver) with QMAP multiplexing (rmnet), without spawning uqmi/qmicli. This is
the common case for most cellular routers. the common case for most cellular routers. Pair with wwand-mhi for modems
attached over PCIe/MHI.
endef endef
define Package/wwand-qmi/install define Package/wwand-qmi/install
@@ -220,7 +221,8 @@ define Package/wwand-mbim/description
MBIM control backend for wwand (cdc_mbim driver): native MS Basic Connect (+ MBIM control backend for wwand (cdc_mbim driver): native MS Basic Connect (+
Extensions v2/v3) plus a QMI-over-MBIM passthrough that tunnels the whole QMI Extensions v2/v3) plus a QMI-over-MBIM passthrough that tunnels the whole QMI
stack over the open MBIM channel for full CDC-level telemetry/config. The stack over the open MBIM channel for full CDC-level telemetry/config. The
passthrough reuses qmi_backend, hence the dependency on wwand-qmi. passthrough reuses qmi_backend, hence the dependency on wwand-qmi. Pair
with wwand-mhi for modems attached over PCIe/MHI.
endef endef
define Package/wwand-mbim/install define Package/wwand-mbim/install
@@ -243,7 +245,9 @@ define Package/wwand-ncm
CATEGORY:=Network CATEGORY:=Network
SUBMENU:=WWAN SUBMENU:=WWAN
TITLE:=NCM/ECM backend for wwand TITLE:=NCM/ECM backend for wwand
DEPENDS:=+wwand +kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-ether # +kmod-usb-net-rndis: the RNDIS datapath (rndis_host) used by e.g. the
# Fibocom FM350-GL (MediaTek T700) — same AT-driven backend, RNDIS netdev.
DEPENDS:=+wwand +kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-ether +kmod-usb-net-rndis
# coexists with the stock OpenWrt NCM stack (netifd `ncm` proto, package # coexists with the stock OpenWrt NCM stack (netifd `ncm` proto, package
# comgt-ncm): wwand drives an AT/NCM modem only once its interface has been # comgt-ncm): wwand drives an AT/NCM modem only once its interface has been
# migrated to `proto wwand` (LuCI modem list / migrate CLI), so both stacks can # migrated to `proto wwand` (LuCI modem list / migrate CLI), so both stacks can