From 7cb3d8fcffdab72cd32902a02b189b573fc89a3c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Aug 2026 20:38:21 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=B8=20Sync=202026-08-19=2020:38:21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- baidudrive/Makefile | 4 +- ddns-go/Makefile | 4 +- linkease-common-bin/Makefile | 4 +- linkease/Makefile | 4 +- linkeasefull/Makefile | 4 +- luci-app-fchomo/root/etc/init.d/fchomo | 4 +- luci-app-passwall/Makefile | 4 +- .../luasrc/controller/passwall.lua | 4 +- .../model/cbi/passwall/server/type/2_xray.lua | 17 +- luci-app-passwall/luasrc/passwall/api.lua | 1 + .../luasrc/passwall/util_xray.lua | 25 ++- .../view/passwall/cbi/nodes_listvalue.htm | 4 +- .../passwall/node_config/link_share_man.htm | 6 +- .../etc/uci-defaults/luci-app-passwall_server | 4 + .../root/usr/share/passwall/monitor.sh | 6 +- .../root/usr/share/passwall/subscribe.lua | 29 ++-- luci-app-passwall2/Makefile | 2 +- .../cbi/passwall2/server/type/2_xray.lua | 17 +- .../luasrc/passwall2/util_xray.lua | 25 ++- .../root/usr/share/passwall2/monitor.sh | 5 +- luci-app-store/root/bin/is-opkg | 2 +- luci-theme-footstrap/Makefile | 2 +- .../luci-static/footstrap/app-icon-192.png | Bin 5513 -> 0 bytes .../luci-static/footstrap/app-icon-512.png | Bin 15709 -> 4679 bytes .../footstrap/apple-touch-icon.png | Bin 5255 -> 0 bytes .../luci-static/footstrap/manifest.json | 6 - .../htdocs/luci-static/resources/fs-chrome.js | 49 ++++-- .../htdocs/luci-static/resources/fs-fit.js | 146 +++++++++++++----- .../htdocs/luci-static/resources/fs-router.js | 25 ++- .../htdocs/luci-static/resources/fs-select.js | 4 +- .../resources/menu-footstrap-common.js | 52 ++++++- luci-theme-footstrap/styles/02-tokens.css | 12 ++ .../styles/base/20-typography.css | 2 +- luci-theme-footstrap/styles/base/30-forms.css | 2 +- .../styles/base/50-chrome.css | 2 +- luci-theme-footstrap/styles/base/60-modal.css | 2 +- luci-theme-footstrap/styles/base/95-luci.css | 14 +- .../styles/pages/10-login.css | 2 +- .../styles/pages/20-overview.css | 2 +- .../styles/pages/40-sshkeys.css | 2 +- .../styles/pages/80-appearance.css | 8 +- .../styles/theme/10-chrome.css | 2 +- .../styles/theme/20-shell.css | 20 +-- .../styles/theme/30-tables.css | 14 +- .../styles/theme/35-alerts.css | 6 +- luci-theme-footstrap/styles/theme/40-tabs.css | 2 +- luci-theme-footstrap/styles/theme/45-misc.css | 4 +- .../styles/theme/55-buttons.css | 16 +- .../styles/theme/60-inputs.css | 42 ++--- .../styles/theme/65-dropdown.css | 14 +- .../styles/theme/75-search.css | 4 +- .../themes/footstrap/partials/head.ut | 5 +- my-default-settings/Makefile | 2 +- .../etc/uci-defaults/99-default-settings | 3 +- wwand/Makefile | 14 +- 55 files changed, 403 insertions(+), 252 deletions(-) delete mode 100644 luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png delete mode 100644 luci-theme-footstrap/htdocs/luci-static/footstrap/apple-touch-icon.png diff --git a/baidudrive/Makefile b/baidudrive/Makefile index ba6c37581..0131fdf4e 100644 --- a/baidudrive/Makefile +++ b/baidudrive/Makefile @@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk PKG_ARCH_BAIDUDRIVE:=$(ARCH) PKG_NAME:=baidudrive -PKG_VERSION:=linkeasefull-runtime-v3.0.9 -PKG_RELEASE:=12 +PKG_VERSION:=linkeasefull-runtime-v3.0.10 +PKG_RELEASE:=13 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_HASH:=skip diff --git a/ddns-go/Makefile b/ddns-go/Makefile index cef77b30a..5d3bedecd 100644 --- a/ddns-go/Makefile +++ b/ddns-go/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ddns-go -PKG_VERSION:=6.17.5 -PKG_RELEASE:=22 +PKG_VERSION:=6.17.6 +PKG_RELEASE:=23 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? diff --git a/linkease-common-bin/Makefile b/linkease-common-bin/Makefile index 067584591..7ff414ab2 100644 --- a/linkease-common-bin/Makefile +++ b/linkease-common-bin/Makefile @@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkease-common-bin # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=1.7.5 -PKG_RELEASE:=7 +PKG_RELEASE:=8 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel) ARCH_HEXCODE=1b0c 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_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) diff --git a/linkease/Makefile b/linkease/Makefile index 1acaa9e47..f4494e77d 100644 --- a/linkease/Makefile +++ b/linkease/Makefile @@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkease # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=1.7.5 -PKG_RELEASE:=10 +PKG_RELEASE:=11 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel) ARCH_HEXCODE=1b0c endif -PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566 +PKG_SOURCE_VERSION:=e0457eac600cd37b128eddf251ff9344702b1d03 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_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE) diff --git a/linkeasefull/Makefile b/linkeasefull/Makefile index 2b3e258f6..e1aa15a4f 100644 --- a/linkeasefull/Makefile +++ b/linkeasefull/Makefile @@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH) PKG_NAME:=linkeasefull # use PKG_SOURCE_DATE instead of PKG_VERSION for compitable PKG_SOURCE_DATE:=3.0.5 -PKG_RELEASE:=9 +PKG_RELEASE:=10 ARCH_HEXCODE:= ifeq ($(ARCH),x86_64) @@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64 PKG_HASH:=skip endif -PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566 +PKG_SOURCE_VERSION:=e0457eac600cd37b128eddf251ff9344702b1d03 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_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH) diff --git a/luci-app-fchomo/root/etc/init.d/fchomo b/luci-app-fchomo/root/etc/init.d/fchomo index fcbc44aa4..de1617624 100755 --- a/luci-app-fchomo/root/etc/init.d/fchomo +++ b/luci-app-fchomo/root/etc/init.d/fchomo @@ -451,7 +451,7 @@ start_service() { } service_started() { - procd_set_config_changed dnsmasq + procd_set_config_changed dhcp 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 # Server - procd_set_config_changed dnsmasq + procd_set_config_changed dhcp procd_set_config_changed firewall } diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 78953b015..3e16f047b 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=26.8.12 -PKG_RELEASE:=229 +PKG_VERSION:=26.8.19 +PKG_RELEASE:=230 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/controller/passwall.lua b/luci-app-passwall/luasrc/controller/passwall.lua index 1e5f504fa..6391eaec4 100644 --- a/luci-app-passwall/luasrc/controller/passwall.lua +++ b/luci-app-passwall/luasrc/controller/passwall.lua @@ -466,12 +466,12 @@ function socks_status() local index = http.formvalue("index") local id = http.formvalue("id") 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 e.use_http = 0 if tonumber(use_http) > 0 then 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 http_write_json(e) end diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua index 01186b1c2..2e1b90bc4 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua @@ -63,15 +63,9 @@ o:value("shadowsocks", "Shadowsocks") o:value("trojan", "Trojan") o:value("hysteria2", "Hysteria2") o:value("wireguard", "WireGuard") -o:value("dokodemo-door", "dokodemo-door") +o:value("tunnel", "Tunnel") 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.datatype = "port" o:depends({ custom = false }) @@ -93,14 +87,14 @@ o = s:option(ListValue, "d_protocol", translate("Destination protocol")) o:value("tcp", "TCP") o:value("udp", "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:depends({ protocol = "dokodemo-door" }) +o:depends({ protocol = "tunnel" }) o = s:option(Value, "d_port", translate("Destination port")) o.datatype = "port" -o:depends({ protocol = "dokodemo-door" }) +o:depends({ protocol = "tunnel" }) o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") o.default = "none" @@ -411,7 +405,8 @@ end --[[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.default = "0" -o:depends({ custom = false, is_endpoint = "" }) +o:depends({ transport = "raw" }) +o:depends({ transport = "ws" }) --[[Fast Open]] o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index d77c229f9..5ab686ce9 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -1748,6 +1748,7 @@ function apply_redirect(m) end end else + uci:revert(c_config .. "_redirect") sys.call("/bin/rm -f " .. tmp_uci_file) end end diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 790ded8d1..b3b0793b0 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -567,11 +567,11 @@ function gen_config_server(node) version = 2, users = users } - elseif node.protocol == "dokodemo-door" then + elseif node.protocol == "tunnel" then settings = { - network = node.d_protocol, - address = node.d_address, - port = tonumber(node.d_port) + allowedNetwork = node.d_protocol, + rewriteAddress = node.d_address, + rewritePort = tonumber(node.d_port) } elseif node.protocol == "wireguard" then settings = { @@ -1231,8 +1231,8 @@ function gen_config(var) tag = in_tag, listen = "127.0.0.1", port = new_port, - protocol = "dokodemo-door", - settings = {network = "tcp,udp", address = to_node.address, port = tonumber(to_node.port)} + protocol = "tunnel", + settings = {allowedNetwork = "tcp,udp", rewriteAddress = to_node.address, rewritePort = tonumber(to_node.port)} }) if to_node.tls_serverName == nil then to_node.tls_serverName = to_node.address @@ -1587,8 +1587,8 @@ function gen_config(var) if tcp_redir_port or udp_redir_port then local inbound = { - protocol = "dokodemo-door", - settings = {network = "tcp,udp", followRedirect = true}, + protocol = "tunnel", + settings = {allowedNetwork = "tcp,udp", followRedirect = true}, streamSettings = {sockopt = {tproxy = "tproxy"}}, sniffing = { 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 local tcp_inbound = api.clone(inbound) tcp_inbound.tag = "tcp_redir" - tcp_inbound.settings.network = "tcp" + tcp_inbound.settings.allowedNetwork = "tcp" tcp_inbound.port = tonumber(tcp_redir_port) tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way table.insert(inbounds, tcp_inbound) @@ -1623,7 +1623,7 @@ function gen_config(var) if udp_redir_port then local udp_inbound = api.clone(inbound) udp_inbound.tag = "udp_redir" - udp_inbound.settings.network = "udp" + udp_inbound.settings.allowedNetwork = "udp" udp_inbound.port = tonumber(udp_redir_port) table.insert(inbounds, udp_inbound) end @@ -1790,11 +1790,10 @@ function gen_config(var) table.insert(inbounds, { listen = "127.0.0.1", port = tonumber(dns_listen_port), - protocol = "dokodemo-door", + protocol = "tunnel", tag = "dns-in", settings = { - address = "0.0.0.0", - network = "tcp,udp" + allowedNetwork = "tcp,udp" } }) diff --git a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue.htm b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue.htm index fe15cf6eb..dca74aee9 100644 --- a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue.htm +++ b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue.htm @@ -96,7 +96,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
- <%=pcdata("("..translate("Not set")..")")%> + <%=pcdata("( "..translate("Not Set").." )")%>
@@ -139,7 +139,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>; }); lv_registerAdaptive(cbid); 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); })(); //]]> diff --git a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm index d7c8910c4..1e6c71164 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm @@ -1639,6 +1639,7 @@ local current_node = map:get(section) opt.set(dom_prefix + 'hysteria2_hop', queryParam.mport || ""); opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm); opt.set(dom_prefix + 'finalmask', queryParam.fm || ""); + if (is_singbox && (queryParam.pcs || queryParam.pinSHA256)) queryParam.allowinsecure = '1'; } else if (has_hysteria2) { opt.set('type', "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 + 'hop', queryParam.mport || ""); + if (queryParam.pcs || queryParam.pinSHA256) queryParam.allowinsecure = '0'; } - opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); opt.set(dom_prefix + 'port', m.port || "443"); - opt.set(dom_prefix + 'tls_serverName', queryParam.sni || ""); opt.set( 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_CertByName', queryParam.vcn || ''); diff --git a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server index 195215eba..aaea6c7fe 100644 --- a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server +++ b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server @@ -39,6 +39,10 @@ for sid in $(uci -q show passwall_server | sed -n 's/^passwall_server\.\([^.=]*\ uci -q set "passwall_server.${sid}=server" fi 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 rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* diff --git a/luci-app-passwall/root/usr/share/passwall/monitor.sh b/luci-app-passwall/root/usr/share/passwall/monitor.sh index 5314b7d4f..b66a156b6 100755 --- a/luci-app-passwall/root/usr/share/passwall/monitor.sh +++ b/luci-app-passwall/root/usr/share/passwall/monitor.sh @@ -27,6 +27,8 @@ while [ 1 -eq 1 ]; do *dns2socks*) cmd_check=${cmd_check//:/ } ;; esac + cmd_check=$(printf '%s' "$cmd_check" | sed 's/[^a-zA-Z0-9 ]/\\&/g') + filename=$(basename "$file") stats_file="${RESTART_STATS_DIR}/${filename}.count" if [ -s "$stats_file" ]; then @@ -38,11 +40,11 @@ while [ 1 -eq 1 ]; do # 检查是否超过最大重启次数 [ "$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)) echo "$restart_count" > "$stats_file" #echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log - sh -c "nohup $cmd 2>&1 &" + sh -c "nohup ${cmd} 2>&1 &" sleep 1 fi done diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 20b0504ac..8f5d335da 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -145,13 +145,14 @@ do CONFIG[#CONFIG + 1] = { log = true, remarks = name .. "节点", - currentNode = node_id and (function() - local section = uci_get(node_id) or {} + currentNode = (function(id) + local section = id and uci_get(id) or nil + if not section then return nil end if section[".type"] == "socks" then - return { Socks = node_id } + return { Socks = id } end return section - end)() or nil, + end)(node_id), set = function(o, server) uci_set(szType, option, server) o.newNodeId = server @@ -264,13 +265,14 @@ do log = true, id = t[".name"], remarks = "访问控制列表[" .. i .. "]", - currentNode = node_id and (function() - local section = uci_get(node_id) or {} + currentNode = (function(id) + local section = id and uci_get(id) or nil + if not section then return nil end if section[".type"] == "socks" then - return { Socks = node_id } + return { Socks = id } end return section - end)() or nil, + end)(node_id), set = function(o, server) uci_set(t[".name"], option, 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_pinSHA256 = params.pcs or params.pinsha256 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_down_mbps = params.downmbps or (sub_cfg and sub_hy_down_mbps or nil) 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_MaxPacketSize = params.maxpacketsize or "1200" end - 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 result.type = is_singbox and 'sing-box' or 'Xray' result.protocol = "hysteria2" 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 + if is_singbox and (params.pcs or params.pinsha256) then + params.allowinsecure = "1" + end elseif has_hysteria2 then result.type = "Hysteria2" + if params.pcs or params.pinsha256 then + params.allowinsecure = "0" + end else log("跳过 Hysteria2 节点,因未适配到 Hysteria2 核心程序,或未正确设置节点使用类型。") return nil 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 if has_singbox then result.type = 'sing-box' diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index cb07e8991..16e3f4126 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.8.17 -PKG_RELEASE:=90 +PKG_RELEASE:=91 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua index 48107b1b3..892cde44e 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua @@ -63,15 +63,9 @@ o:value("shadowsocks", "Shadowsocks") o:value("trojan", "Trojan") o:value("hysteria2", "Hysteria2") o:value("wireguard", "WireGuard") -o:value("dokodemo-door", "dokodemo-door") +o:value("tunnel", "Tunnel") 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.datatype = "port" o:depends({ custom = false }) @@ -93,14 +87,14 @@ o = s:option(ListValue, "d_protocol", translate("Destination protocol")) o:value("tcp", "TCP") o:value("udp", "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:depends({ protocol = "dokodemo-door" }) +o:depends({ protocol = "tunnel" }) o = s:option(Value, "d_port", translate("Destination port")) o.datatype = "port" -o:depends({ protocol = "dokodemo-door" }) +o:depends({ protocol = "tunnel" }) o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") o.default = "none" @@ -416,7 +410,8 @@ end --[[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.default = "0" -o:depends({ custom = false, is_endpoint = "" }) +o:depends({ transport = "raw" }) +o:depends({ transport = "ws" }) --[[Fast Open]] o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 6b496f0ec..9fcee2e16 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -575,11 +575,11 @@ function gen_config_server(node) version = 2, users = users } - elseif node.protocol == "dokodemo-door" then + elseif node.protocol == "tunnel" then settings = { - network = node.d_protocol, - address = node.d_address, - port = tonumber(node.d_port) + allowedNetwork = node.d_protocol, + rewriteAddress = node.d_address, + rewritePort = tonumber(node.d_port) } elseif node.protocol == "wireguard" then settings = { @@ -1227,8 +1227,8 @@ function gen_config(var) tag = in_tag, listen = "127.0.0.1", port = new_port, - protocol = "dokodemo-door", - settings = {network = "tcp,udp", address = to_node.address, port = tonumber(to_node.port)} + protocol = "tunnel", + settings = {allowedNetwork = "tcp,udp", rewriteAddress = to_node.address, rewritePort = tonumber(to_node.port)} }) if to_node.tls_serverName == nil then to_node.tls_serverName = to_node.address @@ -1712,11 +1712,10 @@ function gen_config(var) table.insert(inbounds, { listen = "127.0.0.1", port = tonumber(dns_listen_port), - protocol = "dokodemo-door", + protocol = "tunnel", tag = "dns-in", settings = { - address = "0.0.0.0", - network = "tcp,udp" + allowedNetwork = "tcp,udp" } }) local direct_type_dns = { @@ -1949,8 +1948,8 @@ function gen_config(var) if redir_port then local inbound = { port = tonumber(redir_port), - protocol = "dokodemo-door", - settings = {network = "tcp,udp", followRedirect = true}, + protocol = "tunnel", + settings = {allowedNetwork = "tcp,udp", followRedirect = true}, streamSettings = {sockopt = {tproxy = "tproxy"}}, sniffing = { 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) tcp_inbound.tag = "tcp_redir" - tcp_inbound.settings.network = "tcp" + tcp_inbound.settings.allowedNetwork = "tcp" tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way table.insert(inbounds, tcp_inbound) local udp_inbound = api.clone(inbound) udp_inbound.tag = "udp_redir" - udp_inbound.settings.network = "udp" + udp_inbound.settings.allowedNetwork = "udp" table.insert(inbounds, udp_inbound) end diff --git a/luci-app-passwall2/root/usr/share/passwall2/monitor.sh b/luci-app-passwall2/root/usr/share/passwall2/monitor.sh index e072465ac..7799da982 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/monitor.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/monitor.sh @@ -18,9 +18,10 @@ while [ "$ENABLED" -eq 1 ]; do for filename in $(ls ${TMP_SCRIPT_FUNC_PATH} | grep -v "^_"); do cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename}) 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 - #echo "${cmd} crashed, restarting." >> /tmp/log/passwall2.log + log 0 "${cmd} crashed, restarting." eval $(echo "nohup ${cmd} 2>&1 &") >/dev/null 2>&1 & fi done diff --git a/luci-app-store/root/bin/is-opkg b/luci-app-store/root/bin/is-opkg index 338493c5f..49ab4ec0b 100755 --- a/luci-app-store/root/bin/is-opkg +++ b/luci-app-store/root/bin/is-opkg @@ -556,7 +556,7 @@ case $action in ;; "link_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 diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index ad711fd9e..86e20c904 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=25 +PKG_RELEASE:=26 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png b/luci-theme-footstrap/htdocs/luci-static/footstrap/app-icon-192.png deleted file mode 100644 index 708b9c565f0ce15dcc126bdd27033b45e3d418e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5513 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 WP%(}SiL}3e!BKwsTA@NRmx3c%|NB`K;>gF(M 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

!kdFnSBd?i}aDZKFM2fCY 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 KTQ2?Kl!PFNbO=nOySqgMMG!$^Y=VHm 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 diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json b/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json index 6d587bd6b..d6b161ef1 100644 --- a/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json +++ b/luci-theme-footstrap/htdocs/luci-static/footstrap/manifest.json @@ -8,12 +8,6 @@ "background_color": "#f6f8fa", "theme_color": "#f6f8fa", "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", "sizes": "512x512", diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js index 05354ef66..acc1cd00a 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js @@ -149,7 +149,7 @@ function resolveLen(token, dflt) { * 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 * 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; function shellGeometry() { @@ -162,10 +162,14 @@ function shellGeometry() { contentMin: px('--fs-content-min', GEOM_DFLT.contentMin), sidebarW: px('--fs-sidebar-w', GEOM_DFLT.sidebarW), 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 - * is only the FALLBACK — the gutter that counts is measured, see measureShell() */ - tokenPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2, - contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2 + /* the token is ONE side's padding; the column loses it twice. It is also only the FALLBACK: + * measureShell() overwrites this field with the gutter the column actually got, and until it + * has (the login page has no `.fs-content`, and nothing has measured before the first + * 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 * 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.'); } _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 */ if (_shellPad != null) _geom.contentPad = _shellPad; 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 * 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 - * 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` * 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 * layout as it stands. Before any fitter has run — the login page has no `.fs-content` at all — the * token stands in. */ -let _shellOuter = 0, _shellPad = null; +let _shellOuter = 0, _shellPad = null, _padAt = null; 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; + /* 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 cs = host ? getComputedStyle(host) : null; 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) { _shellPad = v; + _padAt = key; if (_geom) _geom.contentPad = v; } } @@ -232,7 +248,15 @@ function measureShell() { function columnWidth(g, state) { /* 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); - 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() { @@ -546,9 +570,10 @@ return baseclass.extend({ }); }, - /* exported for the unit suite (tests/chrome-geometry.test.mjs): the cut is pure arithmetic over - * four measured numbers, and driving it directly is the only way to hold every combination of - * layout, rail and width without a browser */ + /* exported for the unit suite in the theme's own repository (tests/chrome-geometry.test.mjs — no + * tests ship in the package): the cut is pure arithmetic over a handful of measured numbers, and + * driving it directly is the only way to hold every combination of layout, rail and width + * without a browser */ columnWidth, wireRail }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index 33437861f..020506644 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -86,6 +86,9 @@ function fittersEnabled() { function run() { if (!fittersEnabled()) return; 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 ---- @@ -190,6 +193,8 @@ function sampleMotion() { } if (scrolling()) { requestAnimationFrame(sampleMotion); return; } _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 */ if (_deferred) { _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 * 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() { /* 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 * 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. */ if (scrolling()) return null; - const frames = document.querySelectorAll('#view .cbi-section, #view > .cbi-section-node, #view > div'); - for (const el of frames) { - const r = el.getBoundingClientRect(); - /* the frame the viewport's top edge cuts through */ - if (r.top <= 0 && r.bottom > 0) return { el, top: r.top }; + + /* WHAT THE READER IS LOOKING AT, ASKED OF THE PAGE RATHER THAN OF A SELECTOR LIST. + * + * This walked a list of frames and took the one the fold cut through, which misses the case that + * 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 _anchorFrame = 0; /* 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; _anchorPending = null; 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) { @@ -373,6 +429,11 @@ function applyAnchor(ref) { if (at <= 0) return; const drift = ref.el.getBoundingClientRect().top - ref.top; 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; else window.scrollTo(0, at + drift); } @@ -393,8 +454,11 @@ function applyAnchor(ref) { function observeContent() { if (_mo) return; _mo = new MutationObserver(() => { - /* what the reader is looking at, before the poll's mutation and the fitters move anything */ - const ref = anchorRef(); + /* what the reader is looking at, before the poll's mutation and the fitters move anything — + * 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(); scheduleAnchor(ref); }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js index 8a6c4fd14..126cc79cf 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js @@ -22,7 +22,12 @@ * 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 - * 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 --- * 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(); 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(', ') + ' — 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; } @@ -1546,12 +1555,14 @@ return baseclass.extend({ wire: wireRouter, wireVisibility, onNavigate, - /* exported for the unit suite (tests/router-contract.test.mjs), which drives it against a - * hand-broken `L` — the one way to see the OFF branch without a router that ships one */ + /* exported for the unit suite in the theme's own repository (tests/router-contract.test.mjs — + * 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, - /* likewise: tests/interval-pause.test.mjs drives the navigation sweep around a visibilitychange, - * and tests/session-expiry.test.mjs reads the verdict the interceptors reached. navigate() is - * the real caller of the first and `_expired` gates the second — nothing else may call either. */ + /* likewise, and likewise out-of-package: interval-pause.test.mjs drives the navigation sweep + * around a visibilitychange and session-expiry.test.mjs reads the verdict the interceptors + * reached. navigate() is the real caller of the first and `_expired` gates the second — nothing + * else may call either. */ clearViewIntervals, sessionExpired, /* fs-search warms the pages this admin actually uses (its recents) and the arrow-key-highlighted diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js index 39df4ba99..dcd3cc767 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js @@ -612,8 +612,8 @@ function fitTables() { /* 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 * 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 - * layout is read here either. */ + * held still. `chrome.contentWidth()` answers from what the last fitter measured plus the + * layout attributes — no layout is read here either. */ applyDecision(t, known || { stack: chrome.contentWidth() < CRAMPED, drop: false, breakCol: -1 }); fit.deferMeasurement(); return; diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js index 01d4427a4..010a53b81 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js @@ -5,12 +5,52 @@ 'require fs-menutree as tree'; 'require fs-chrome as chrome'; 'require fs-router as router'; -'require fs-appearance as appearance'; -'require fs-overview as overview'; 'require fs-prefs as prefs'; 'require fs-sheets as sheets'; '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 * 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); 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 * recent-pages list is recorded from the first navigation onwards */ search.wire(); chrome.wireRail(); chrome.wireIndicatorCounts(); /* 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 * DOM), but it would make a route change racy against a listener that is not attached * yet — cheap to order correctly, expensive to debug. */ - overview.wire(); + wirePageModules(); router.wire(); router.wireVisibility(); /* fs-chrome's renderTabMenu warns about exactly this, and the root chain was left bare: a diff --git a/luci-theme-footstrap/styles/02-tokens.css b/luci-theme-footstrap/styles/02-tokens.css index 7f8b38c72..53381f896 100644 --- a/luci-theme-footstrap/styles/02-tokens.css +++ b/luci-theme-footstrap/styles/02-tokens.css @@ -69,6 +69,18 @@ * tools/mirror.mjs; the third restated the literal until it had a name. */ --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 * 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. */ diff --git a/luci-theme-footstrap/styles/base/20-typography.css b/luci-theme-footstrap/styles/base/20-typography.css index 7ca0d9b9b..62fcc4fe3 100644 --- a/luci-theme-footstrap/styles/base/20-typography.css +++ b/luci-theme-footstrap/styles/base/20-typography.css @@ -112,7 +112,7 @@ hr { margin: var(--fs-space-5) 0; border: 0; - border-bottom: 1px solid var(--fs-border); + border-bottom: var(--fs-hairline); } strong { diff --git a/luci-theme-footstrap/styles/base/30-forms.css b/luci-theme-footstrap/styles/base/30-forms.css index 5af6d689a..277bf5ba6 100644 --- a/luci-theme-footstrap/styles/base/30-forms.css +++ b/luci-theme-footstrap/styles/base/30-forms.css @@ -230,7 +230,7 @@ /* 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. */ 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); text-align: end; } diff --git a/luci-theme-footstrap/styles/base/50-chrome.css b/luci-theme-footstrap/styles/base/50-chrome.css index d7603b1a1..f5d25192e 100644 --- a/luci-theme-footstrap/styles/base/50-chrome.css +++ b/luci-theme-footstrap/styles/base/50-chrome.css @@ -91,7 +91,7 @@ padding: var(--fs-space-2) var(--fs-space-3); margin: 0 0 var(--fs-space-4); 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); display: flex; flex: 0; diff --git a/luci-theme-footstrap/styles/base/60-modal.css b/luci-theme-footstrap/styles/base/60-modal.css index 2b90f6a11..3dc1528cc 100644 --- a/luci-theme-footstrap/styles/base/60-modal.css +++ b/luci-theme-footstrap/styles/base/60-modal.css @@ -22,7 +22,7 @@ /* a modal is a CARD — the large radius, like every other panel */ border-radius: var(--fs-radius-lg); 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 * 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. */ diff --git a/luci-theme-footstrap/styles/base/95-luci.css b/luci-theme-footstrap/styles/base/95-luci.css index 5f8e30034..ccd799ce3 100644 --- a/luci-theme-footstrap/styles/base/95-luci.css +++ b/luci-theme-footstrap/styles/base/95-luci.css @@ -175,7 +175,7 @@ } .ifacebox { - border: 1px solid var(--fs-border); + border: var(--fs-hairline); margin: 0 var(--fs-space-3); text-align: center; white-space: nowrap; @@ -190,7 +190,7 @@ } .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 * 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; @@ -233,7 +233,7 @@ .ifacebadge { display: inline-block; flex-direction: row; - border: 1px solid var(--fs-border); + border: var(--fs-hairline); padding: var(--fs-space-0-5); /* surface: theme/45-misc.css (flat) — absorbed */ color: var(--fs-text); @@ -409,7 +409,7 @@ .uci-dialog div > ins, .uci-dialog div > var { margin-bottom: var(--fs-space-0-5); - border: 1px solid var(--fs-border); + border: var(--fs-hairline); line-height: var(--fs-leading-tight); overflow: hidden; text-overflow: ellipsis; @@ -571,7 +571,7 @@ min-width: min(210px, 100%); max-width: 100%; /* width/style only; radius: theme/60-inputs.css — absorbed */ - border: 1px solid var(--fs-border); + border: var(--fs-hairline); display: flex; flex-direction: column; opacity: 0; @@ -587,7 +587,7 @@ text-overflow: ellipsis; margin: .25em .25em 0 .25em; white-space: nowrap; - border-bottom: 1px solid var(--fs-border); + border-bottom: var(--fs-hairline); } .cbi-filebrowser .cbi-button-positive { @@ -625,7 +625,7 @@ flex-wrap: wrap; margin: 0 -.125em .25em -.125em; padding: 0 0 .125em 0; - border-bottom: 1px solid var(--fs-border); + border-bottom: var(--fs-hairline); } .cbi-filebrowser .upload > * { diff --git a/luci-theme-footstrap/styles/pages/10-login.css b/luci-theme-footstrap/styles/pages/10-login.css index c5a7e796f..a1d15f14e 100644 --- a/luci-theme-footstrap/styles/pages/10-login.css +++ b/luci-theme-footstrap/styles/pages/10-login.css @@ -23,7 +23,7 @@ * min-content, ~131px, and the description wrapped one word per line. */ width: min(400px, calc(100% - 32px)); 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 * 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); diff --git a/luci-theme-footstrap/styles/pages/20-overview.css b/luci-theme-footstrap/styles/pages/20-overview.css index d928a9691..5a89b909a 100644 --- a/luci-theme-footstrap/styles/pages/20-overview.css +++ b/luci-theme-footstrap/styles/pages/20-overview.css @@ -140,7 +140,7 @@ align-self: stretch; text-align: start; background: var(--fs-panel2); - border: 1px solid var(--fs-border); + border: var(--fs-hairline); border-radius: var(--fs-radius-lg); 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 diff --git a/luci-theme-footstrap/styles/pages/40-sshkeys.css b/luci-theme-footstrap/styles/pages/40-sshkeys.css index c4ba6acf5..34c46d89c 100644 --- a/luci-theme-footstrap/styles/pages/40-sshkeys.css +++ b/luci-theme-footstrap/styles/pages/40-sshkeys.css @@ -9,7 +9,7 @@ * their own entry here. */ body[data-page="admin-system-admin-sshkeys"] #view > div { background: var(--fs-panel); - border: 1px solid var(--fs-border); + border: var(--fs-hairline); border-radius: var(--fs-radius-lg); box-shadow: var(--fs-shadow); padding: var(--fs-space-4); diff --git a/luci-theme-footstrap/styles/pages/80-appearance.css b/luci-theme-footstrap/styles/pages/80-appearance.css index 2c9db5ee0..5426c9185 100644 --- a/luci-theme-footstrap/styles/pages/80-appearance.css +++ b/luci-theme-footstrap/styles/pages/80-appearance.css @@ -76,7 +76,7 @@ margin: 0; 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 * 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 @@ -117,7 +117,7 @@ * border. Fixed 34px square so it lines up with the field beside it at every density. */ input[type="color"].fs-color-swatch { 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 { border: 0; border-radius: calc(var(--fs-radius-sm) - 1px); } @@ -162,7 +162,7 @@ * letterboxed photograph. */ .fs-ap-bgprev { 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 */ margin-top: var(--fs-space-2); } @@ -189,7 +189,7 @@ /* the version line at the foot of the page */ .fs-ap-footer { 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); } /* links to the project repo — reads as muted text until hovered */ diff --git a/luci-theme-footstrap/styles/theme/10-chrome.css b/luci-theme-footstrap/styles/theme/10-chrome.css index da3a2f8e3..6ceff5f6b 100644 --- a/luci-theme-footstrap/styles/theme/10-chrome.css +++ b/luci-theme-footstrap/styles/theme/10-chrome.css @@ -68,7 +68,7 @@ /* 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 * 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; flex: 0 0 auto; } diff --git a/luci-theme-footstrap/styles/theme/20-shell.css b/luci-theme-footstrap/styles/theme/20-shell.css index 1b0819ae6..bcac17519 100644 --- a/luci-theme-footstrap/styles/theme/20-shell.css +++ b/luci-theme-footstrap/styles/theme/20-shell.css @@ -118,7 +118,7 @@ background: var(--fs-bar-bg); 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 */ overflow: visible; } @@ -175,7 +175,7 @@ margin: 0; padding: var(--fs-space-2); background: var(--fs-glass); 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); } /* 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 * them instead of pushing one cluster to the bar's far edge */ 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); font: inherit; font-weight: var(--fs-weight); font-size: var(--fs-type); line-height: 1.1; text-align: start; cursor: pointer; @@ -280,7 +280,7 @@ .fs-logout { flex: 0 0 auto; order: 2; justify-content: center; 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); } /* 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); background: var(--fs-panel); 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; /* `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 @@ -440,7 +440,7 @@ display: flex; margin: var(--fs-space-1) 0 var(--fs-space-3) var(--fs-space-7); 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 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%); 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); - background: var(--fs-panel); border: 1px solid var(--fs-border); + background: var(--fs-panel); border: var(--fs-hairline); box-shadow: var(--fs-shadow-pop); 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); @@ -559,7 +559,7 @@ display: none; 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); - 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); 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 *