Compare commits

..
8 Commits
Author SHA1 Message Date
github-actions[bot] 5f9dbf5e2d 🎄 Sync 2026-09-05 15:13:21
Merge-upstream / merge (push) Canceled after 0s
2026-09-05 15:13:21 +08:00
github-actions[bot] 70e84ddf56 💐 Sync 2026-09-05 14:55:36 2026-09-05 14:55:36 +08:00
kiddin9 e0826e34a3 Update depends.patch 2026-09-05 14:53:24 +08:00
kiddin9 b92e3e9efb Update Makefile 2026-09-05 14:52:09 +08:00
github-actions[bot] 24345f777c Sync 2026-09-05 14:47:29 2026-09-05 14:47:29 +08:00
kiddin9 695b29172f Delete .github/diy/patches/pgyvpn.patch 2026-09-05 14:45:25 +08:00
kiddin9 aac1b49f77 Update depends.patch 2026-09-05 14:44:49 +08:00
github-actions[bot] 8eaa48a277 💐 Sync 2026-09-05 10:39:08 2026-09-05 10:39:08 +08:00
38 changed files with 788 additions and 551 deletions
+11 -11
View File
@@ -1,3 +1,14 @@
--- a/pgyvpn/Makefile
+++ b/pgyvpn/Makefile
@@ -67,7 +67,6 @@ ifeq ($(ARCH),x86_64)
OPENSSL_COMPAT_TARGET:=linux-x86_64
endif
-PKG_FLAGS:=nonshared
PKG_MAINTAINER:=Oray <developer@oray.com>
include $(INCLUDE_DIR)/package.mk
--- a/luci-app-easytier/Makefile --- a/luci-app-easytier/Makefile
+++ b/luci-app-easytier/Makefile +++ b/luci-app-easytier/Makefile
@@ -10,7 +10,7 @@ PKG_VERSION:=2.3.0 @@ -10,7 +10,7 @@ PKG_VERSION:=2.3.0
@@ -33,17 +44,6 @@
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
--- a/pgyvpn/Makefile
+++ b/pgyvpn/Makefile
@@ -36,7 +36,6 @@ PKG_SOURCE:=pgyvpnsvr
PKG_SOURCE_URL:=https://mirrors.oray.com/orayos/packages/$(PKG_NAME)/$(PKG_ARCH_PGYYPN)/$(PKG_VERSION)/bin
PKG_HASH:=skip
-PKG_FLAGS:=nonshared
PKG_MAINTAINER:=Oray <developer@oray.com>
include $(INCLUDE_DIR)/package.mk
--- a/luci-app-rclone/Makefile --- a/luci-app-rclone/Makefile
+++ b/luci-app-rclone/Makefile +++ b/luci-app-rclone/Makefile
@@ -9,7 +9,7 @@ PKG_VERSION:=1.4.1 @@ -9,7 +9,7 @@ PKG_VERSION:=1.4.1
-11
View File
@@ -1,11 +0,0 @@
--- a/pgyvpn/Makefile
+++ b/pgyvpn/Makefile
@@ -78,7 +78,7 @@ define Package/pgyvpn/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_DIR) $(1)/usr/share/pgyvpn
- $(CP) $(PKG_BUILD_DIR)/{pgyvpn_oraysl,pgyvpnsvr} $(1)/usr/sbin
+ $(CP) $(PKG_BUILD_DIR)/pgyvpn_oraysl $(1)/usr/sbin
$(INSTALL_BIN) ./files/etc/init.d/* $(1)/etc/init.d
$(INSTALL_CONF) ./files/etc/config/* $(1)/etc/config
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/* $(1)/etc/hotplug.d/iface
+2 -2
View File
@@ -6,11 +6,11 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=3proxy PKG_NAME:=3proxy
PKG_VERSION:=0.9.5 PKG_VERSION:=0.9.5
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/3proxy/3proxy/tar.gz/$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/3proxy/3proxy/tar.gz/$(PKG_VERSION)?
PKG_HASH:=6f6da51d9bba93231e12acd707bb6cf86a1ab9491dc6dd0c79750cb3641541a3 PKG_HASH:=skip
PKG_CPE_ID:=cpe:/a:3proxy:3proxy PKG_CPE_ID:=cpe:/a:3proxy:3proxy
PKG_LICENSE:=3proxy Apache-2.0 GPL-2.0-or-later LGPL-2.1-or-later PKG_LICENSE:=3proxy Apache-2.0 GPL-2.0-or-later LGPL-2.1-or-later
+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser PKG_NAME:=filebrowser
PKG_VERSION:=1.5.5-stable PKG_VERSION:=1.5.6-stable
PKG_RELEASE=1 PKG_RELEASE=1
ifeq ($(ARCH),aarch64) ifeq ($(ARCH),aarch64)
+8 -151
View File
@@ -1,156 +1,13 @@
server: server:
minSearchLength: 3 # minimum length of search query to begin searching (default: 3)
disableUpdateCheck: false # disables backend update check service
numImageProcessors: 4 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
socket: "" # socket to listen on
tlsKey: "" # path to TLS key
tlsCert: "" # path to TLS cert
disablePreviews: false # disable all previews thumbnails, simple icons will be used
disablePreviewResize: false # disable resizing of previews for faster loading over slow connections
disableTypeDetectionByHeader: false # disable type detection by header, useful if filesystem is slow.
port: 8989 port: 8989
baseURL: "/" database: "/etc/filebrowser/database.db"
logging: sources:
- levels: "" # separated list of log levels to enable. (eg. "info|warning|error|debug") - path: "/" # Do not use a root "/" directory or include the "/var" folder
apiLevels: "" # separated list of log levels to enable for the API. (eg. "info|warning|error")
output: "stdout" # output location. (eg. "stdout" or "path/to/file.log")
noColors: false # disable colors in the output
json: false # output in json format
utc: false # use UTC time in the output instead of local time
debugMedia: false # output ffmpeg stdout for media integration -- careful can produces lots of output!
database: "/etc/filebrowser/database.db" # path to the database file
sources: # validate:required,dive
- path: "/"
name: "Root" # display name
config: config:
denyByDefault: false # deny access unless an "allow" access rule was specifically created. defaultEnabled: true
private: false # designate as source as private -- currently just means no sharing permitted.
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
disableIndexing: false # disable the indexing of this source
maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported
neverWatchPaths: []
exclude: # exclude files and folders from indexing, if include is not set
hidden: false # exclude hidden files and folders.
ignoreZeroSizeFolders: false # ignore folders with 0 size
filePaths: []
folderPaths: []
fileNames: []
folderNames: []
fileEndsWith: []
folderEndsWith: []
fileStartsWith: []
folderStartsWith: []
include: # include files and folders from indexing, if exclude is not set
rootFolders: []
rootFiles: []
defaultUserScope: "/" # default "/" should match folders under path
defaultEnabled: false # should be added as a default source for new users?
createUserDir: false # create a user directory for each user
externalUrl: "" # used by share links if set (eg. http://mydomain.com)
internalUrl: "" # used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080)
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
maxArchiveSize: 50 # max pre-archive combined size of files/folder that are allowed to be archived (in GB)
auth: auth:
tokenExpirationHours: 2 # time in hours each web UI session token is valid for. Default is 2 hours. adminUsername: admin
methods: adminPassword: "admin"
proxy: # validate:omitempty
enabled: false
createUser: false # create user if not exists
header: "" # required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
noauth: false # if set to true, overrides all other auth methods and disables authentication
password: # validate:omitempty
enabled: true
minLength: 3 # minimum pasword length required, default is 5. validate:omitempty
signup: false # allow signups on login page if enabled -- not secure. validate:omitempty
recaptcha: # recaptcha config, only used if signup is enabled validate:omitempty
host: "" # validate:required
key: "" # validate:required
secret: "" # validate:required
enforcedOtp: false # if set to true, TOTP is enforced for all password users users. Otherwise, users can choose to enable TOTP.
oidc: # validate:omitempty
enabled: false # whether to enable OIDC authentication
clientId: "**hidden**" # secret: client id of the OIDC application
clientSecret: "**hidden**" # secret: client secret of the OIDC application
issuerUrl: "" # authorization URL of the OIDC provider
scopes: "openid email profile" # scopes to request from the OIDC provider
userIdentifier: "preferred_username" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification for the OIDC provider. This is insecure and should only be used for testing.
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
createUser: false # create user if not exists
adminGroup: "" # if set, users in this group will be granted admin privileges.
groupsClaim: "groups" # the JSON field name to read groups from. Default is "groups"
key: "**hidden**" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: "admin" # secret: the username of the admin user. If not set, the default is "admin".
adminPassword: "admin" # secret: the password of the admin user. If not set, the default is "admin".
totpSecret: "**hidden**" # secret: secret used to encrypt TOTP secrets
frontend:
name: "FileBrowser Quantum" # display name
disableDefaultLinks: false # disable default links in the sidebar
disableUsedPercentage: false # disable used percentage for the sources in the sidebar
externalLinks:
- text: "Help" # the text to display on the link validate:required
title: "" # the title to display on hover
url: "https://github.com/gtsteffaniak/filebrowser" # the url to link to validate:required
disableNavButtons: false # disable the nav buttons in the sidebar
styling:
customCSS: "" # if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
lightBackground: "#f5f5f5" # specify a valid CSS color property value to use as the background color in light mode
darkBackground: "#141D24" # Specify a valid CSS color property value to use as the background color in dark mode
customThemes: # A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
default:
description: "The default theme" # The description of the theme to display in the UI.
css: "" # The css file path and filename to use for the theme.
alternative:
description: "Reduce rounded corners" # The description of the theme to display in the UI.
css: "reduce-rounded-corners.css" # The css file path and filename to use for the theme.
favicon: "" # path to a favicon to use for the frontend
description: "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server" # description that shows up in html head meta description
userDefaults: userDefaults:
editorQuickSave: false # show quick save button in editor ui:
hideSidebarFileActions: false # hide the file actions in the sidebar locale: "cn"
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
darkMode: true # should dark mode be enabled
locale: "zhCN" # language to use: eg. de, en, or fr
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
singleClick: false # open directory on single click, also enables middle click to open in new tab
showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disablePreviewExt: "" # space separated list of file extensions to disable preview for
disableViewingExt: "" # space separated list of file extensions to disable viewing for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
disableHideSidebar: false
highQuality: false
image: false
video: false
motionVideoPreview: false
office: false
popup: false
autoplayMedia: false
defaultMediaPlayer: false
folder: false
permissions:
api: false
admin: false
modify: false
share: false
realtime: false
loginMethod: "password" # login method to use: eg. password, proxy, oidc
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
fileLoading: # upload and download settings
maxConcurrentUpload: 10
uploadChunkSizeMb: 10
clearAll: false
disableOnlyOfficeExt: "" # list of file extensions to disable onlyoffice editor for
customTheme: "" # Name of theme to use chosen from custom themes config.
showSelectMultiple: false # show select multiple files on desktop
debugOffice: false # debug onlyoffice editor
-2
View File
@@ -2,6 +2,4 @@ config filebrowser 'config'
option enabled '0' option enabled '0'
option listen_port '8989' option listen_port '8989'
option root_path '/' option root_path '/'
option base_url '/'
option disable_exec '1'
+3 -1
View File
@@ -18,9 +18,11 @@ start() {
local listen_port root_path base_url local listen_port root_path base_url
config_get listen_port "config" "listen_port" "8989" config_get listen_port "config" "listen_port" "8989"
config_get root_path "config" "root_path" "/" config_get root_path "config" "root_path" "/"
config_get base_url "config" "base_url" "/"
echo "Starting filebrowser..." echo "Starting filebrowser..."
sed -e "s/ port:.*/ port: $listen_port/" \
-e "s, - path:.*, - path: \"$root_path\"," \
-i "$CONF_PATH"
start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH" start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH"
} }
@@ -49,17 +49,24 @@ return view.extend({
s = m.section(form.TypedSection); s = m.section(form.TypedSection);
s.anonymous = true; s.anonymous = true;
s.render = function() { s.render = function() {
poll.add(function() { poll.add(function() {
return Promise.resolve().then(function() { return fs.stat('/var/run/filebrowser.pid').then(function(stat) {
let view = document.getElementById('service_status'); let view = document.getElementById('service_status');
view.innerHTML = renderStatus(data.isRunning, webport); if (view) {
}); view.innerHTML = renderStatus(stat, webport);
}); }
}).catch(function() {
let view = document.getElementById('service_status');
if (view) {
view.innerHTML = renderStatus(null, webport);
}
});
});
return E('div', { class: 'cbi-section', id: 'status_bar' }, [ return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data...')) E('p', { id: 'service_status' }, _('Collecting data...'))
]); ]);
} };
s = m.section(form.NamedSection, 'config', 'filebrowser'); s = m.section(form.NamedSection, 'config', 'filebrowser');
@@ -75,10 +82,6 @@ return view.extend({
o = s.option(form.Value, 'root_path', _('Root directory')); o = s.option(form.Value, 'root_path', _('Root directory'));
o.default = '/'; o.default = '/';
o.rmempty = false; o.rmempty = false;
o = s.option(form.Value, 'base_url', _('Base url'));
o.default = '/';
o.rmempty = false;
return m.render(); return m.render();
} }
-5
View File
@@ -8,11 +8,6 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI page for IPK upload LUCI_TITLE:=LuCI page for IPK upload
LUCI_DEPENDS:=+luci-lib-fs LUCI_DEPENDS:=+luci-lib-fs
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-filetransfer
PKG_VERSION:=1
PKG_RELEASE:=1
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
@@ -9,5 +9,7 @@ module("luci.controller.filetransfer", package.seeall)
function index() function index()
entry({"admin", "system", "filetransfer"}, form("filetransfer"), _("FileTransfer"), 89).dependent = true local page = entry({"admin", "system", "filetransfer"}, form("filetransfer"), _("FileTransfer"), 89)
page.dependent = true
page.acl_depends = { "luci-app-filetransfer" }
end end
@@ -91,16 +91,6 @@ elseif luci.http.formvalue("download") then
Download() Download()
end end
local function getSizeStr(size)
local i = 0
local byteUnits = {' kB', ' MB', ' GB', ' TB'}
repeat
size = size / 1024
i = i + 1
until(size <= 1024)
return string.format("%.1f", size) .. byteUnits[i]
end
local inits, attr = {} local inits, attr = {}
for i, f in ipairs(fs.glob("/tmp/upload/*")) do for i, f in ipairs(fs.glob("/tmp/upload/*")) do
attr = fs.stat(f) attr = fs.stat(f)
@@ -109,7 +99,7 @@ for i, f in ipairs(fs.glob("/tmp/upload/*")) do
inits[i].name = fs.basename(f) inits[i].name = fs.basename(f)
inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime) inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime)
inits[i].modestr = attr.modestr inits[i].modestr = attr.modestr
inits[i].size = getSizeStr(attr.size) inits[i].size = tostring(attr.size)
inits[i].remove = 0 inits[i].remove = 0
inits[i].install = false inits[i].install = false
end end
@@ -122,9 +112,8 @@ form.submit = false
tb = form:section(Table, inits) tb = form:section(Table, inits)
nm = tb:option(DummyValue, "name", translate("File name")) nm = tb:option(DummyValue, "name", translate("File name"))
mt = tb:option(DummyValue, "mtime", translate("Modify time")) mt = tb:option(DummyValue, "mtime", translate("Modify time"))
ms = tb:option(DummyValue, "modestr", translate("Attributes")) ms = tb:option(DummyValue, "modestr", translate("Mode string"))
sz = tb:option(DummyValue, "size", translate("Size")) sz = tb:option(DummyValue, "size", translate("Size"))
btnrm = tb:option(Button, "remove", translate("Remove")) btnrm = tb:option(Button, "remove", translate("Remove"))
btnrm.render = function(self, section, scope) btnrm.render = function(self, section, scope)
self.inputstyle = "remove" self.inputstyle = "remove"
+1
View File
@@ -0,0 +1 @@
zh_Hans
-1
View File
@@ -1 +0,0 @@
zh-cn
@@ -1,15 +1,5 @@
msgid "" msgid ""
msgstr "" msgstr "Content-Type: text/plain; charset=UTF-8"
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: dingpengyu <dingpengyu06@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"X-Generator: Poedit 2.3.1\n"
msgid "Choose local file:" msgid "Choose local file:"
msgstr "选择本地文件:" msgstr "选择本地文件:"
@@ -38,17 +28,17 @@ msgstr "文件传输"
msgid "Install" msgid "Install"
msgstr "安装" msgstr "安装"
msgid "Attributes" msgid "Mode string"
msgstr "属性" msgstr "模式字符串"
msgid "Modify time" msgid "Modify time"
msgstr "修改时间" msgstr "修改时间"
msgid "No specify upload file." msgid "No specify upload file."
msgstr "未指定上传文件" msgstr "未指定上传文件"
msgid "Path on Route:" msgid "Path on Route:"
msgstr "路由目录" msgstr "路由目录"
msgid "Remove" msgid "Remove"
msgstr "移除" msgstr "移除"
@@ -1,6 +0,0 @@
#!/bin/sh
sed -i 's/cbi.submit\"] = true/cbi.submit\"] = \"1\"/g' /usr/lib/lua/luci/dispatcher.lua
rm -f /tmp/luci-indexcache
exit 0
@@ -0,0 +1,11 @@
{
"luci-app-filetransfer": {
"description": "Grant UCI access for luci-app-filetransfer",
"read": {
"uci": [ "filetransfer" ]
},
"write": {
"uci": [ "filetransfer" ]
}
}
}
+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:=56 PKG_RELEASE:=59
endif endif
LUCI_TITLE:=Footstrap Theme LUCI_TITLE:=Footstrap Theme
@@ -296,7 +296,14 @@ function build() {
const node = w.render(); const node = w.render();
node.setAttribute('aria-label', label); node.setAttribute('aria-label', label);
const push = () => apply(parseInt(w.getValue(), 10)); const push = () => apply(parseInt(w.getValue(), 10));
node.addEventListener('widget-update', push); /* `live: false` for an axis whose value moves THIS CONTROL. Content width widens the column
* the Appearance page is drawn in, so applying mid-drag slides the handle out from under
* the pointer, the pointer catches up, and one small drag runs the value to the maximum —
* reported from a router, and not a thing any other slider here can do: rounding and the
* tint sliders repaint around a control that stays where it is. On release the page still
* follows, and a keyboard user is unaffected either way: an arrow key on a range input
* fires `change` as well as `input`, so the value applies at once. */
if (o.live !== false) node.addEventListener('widget-update', push);
node.addEventListener('widget-change', push); node.addEventListener('widget-change', push);
return node; return node;
}; };
@@ -343,7 +350,10 @@ function build() {
* so it is a proper noun and stays untranslated, like the two above it */ * so it is a proper noun and stays untranslated, like the two above it */
bootstrap: 'Bootstrap', bootstrap: 'Bootstrap',
/* names the OTHER package again, luci-theme-openwrt-2020, whose colourway this is */ /* names the OTHER package again, luci-theme-openwrt-2020, whose colourway this is */
'2020': 'OpenWrt 2020' '2020': 'OpenWrt 2020',
/* names the OpenWrt forum (forum.openwrt.org), whose Discourse colourway this is —
* a proper noun like the three above it, not the English common noun "forum" */
forum: 'Forum'
}, bump(repaint(axes.applyPalette)), label)), }, bump(repaint(axes.applyPalette)), label)),
group(_('Density', 'footstrap'), (label) => selectCtl(prefs.currentDensity(), { group(_('Density', 'footstrap'), (label) => selectCtl(prefs.currentDensity(), {
@@ -352,6 +362,16 @@ function build() {
large: _('Large', 'footstrap') large: _('Large', 'footstrap')
}, bump(prefs.applyDensity), label)), }, bump(prefs.applyDensity), label)),
/* issue #44: the content column's own cap, separate from Density (which moves type and
* air, not the column's ceiling). The slider STARTS at the 1280px the theme has always
* shipped and only ever widens — there is no reason to offer a column narrower than the
* one every page was designed against, and keeping 1280 as the left end also keeps
* --fs-content-min (500px, what the sidebar-to-bar fold is measured against) out of reach
* by construction rather than by a rule someone has to remember. */
group(_('Content width', 'footstrap'),
(label) => sliderCtl(axes.currentContentWidth(), 1280, 3840,
bump(axes.applyContentWidth), label, { step: 40, live: false })),
group(_('Rounding', 'footstrap'), group(_('Rounding', 'footstrap'),
(label) => sliderCtl(axes.currentRadius(), 0, 20, bump(axes.applyRadius), label)), (label) => sliderCtl(axes.currentRadius(), 0, 20, bump(axes.applyRadius), label)),
@@ -575,7 +595,13 @@ function build() {
* the scoped uci ACL). It does not change this browser — localStorage keeps overriding — so the * the scoped uci ACL). It does not change this browser — localStorage keeps overriding — so the
* saved default only shows on a fresh browser. The two Reset buttons below are the escape * saved default only shows on a fresh browser. The two Reset buttons below are the escape
* hatches, and they do not land in the same place. */ * hatches, and they do not land in the same place. */
const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ _('Save to router', 'footstrap') ]); /* the button/status label said three times below, inside this one build() call — hoisting the
* RESULT, not the msgid, so update-po.sh still sees the literal `_()` argument elsewhere
* (measured: 22 B x3 -> 27 B, 39 B saved) */
const SAVE_TO_ROUTER = _('Save to router', 'footstrap');
/* said twice below, same build() call (measured: 24 B x2 -> 30 B, 18 B saved) */
const RESET_TO_ROUTER = _('Reset to router', 'footstrap');
const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ SAVE_TO_ROUTER ]);
/* Two resets, because two things sit underneath a browser's tweaks (fs-prefs.js): "Reset to /* Two resets, because two things sit underneath a browser's tweaks (fs-prefs.js): "Reset to
* router" clears them and lets every axis fall back to whatever the router holds, while "Reset * router" clears them and lets every axis fall back to whatever the router holds, while "Reset
* to built-in" writes the theme's built-ins explicitly — the only way to say "as the theme * to built-in" writes the theme's built-ins explicitly — the only way to say "as the theme
@@ -585,7 +611,7 @@ function build() {
* "Save as default" and the theme's in "Reset to default", while the router's look also * "Save as default" and the theme's in "Reset to default", while the router's look also
* answered to "saved" — three names for two states, asked about on the forum (topic 251930, * answered to "saved" — three names for two states, asked about on the forum (topic 251930,
* post 92). The row now reads as one save and two resets, over `router` and `built-in`. */ * post 92). The row now reads as one save and two resets, over `router` and `built-in`. */
const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ _('Reset to router', 'footstrap') ]); const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ RESET_TO_ROUTER ]);
/* the stock destructive class, so the button discarding every local tweak is the red one /* the stock destructive class, so the button discarding every local tweak is the red one
* (theme/55-buttons.css). "Reset to router" stays neutral: it steps back to the shared state * (theme/55-buttons.css). "Reset to router" stays neutral: it steps back to the shared state
* rather than discarding. */ * rather than discarding. */
@@ -607,14 +633,14 @@ function build() {
function refreshSave() { function refreshSave() {
if (prefs.storageBroken()) { if (prefs.storageBroken()) {
saveBtn.disabled = false; saveBtn.disabled = false;
saveBtn.textContent = _('Save to router', 'footstrap'); saveBtn.textContent = SAVE_TO_ROUTER;
saveErr.textContent = _('This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser.', 'footstrap'); saveErr.textContent = _('This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser.', 'footstrap');
saveErr.hidden = false; saveErr.hidden = false;
return; return;
} }
const saved = axes.matchesSavedDefault(); const saved = axes.matchesSavedDefault();
saveBtn.disabled = saved; saveBtn.disabled = saved;
saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : _('Save to router', 'footstrap'); saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : SAVE_TO_ROUTER;
} }
saveBtn.addEventListener('click', () => { saveBtn.addEventListener('click', () => {
saveBtn.disabled = true; saveBtn.disabled = true;
@@ -655,7 +681,7 @@ function build() {
location.reload(); location.reload();
}); });
} }
twoClick(resetSavedBtn, _('Reset to router', 'footstrap'), axes.resetToSaved); twoClick(resetSavedBtn, RESET_TO_ROUTER, axes.resetToSaved);
twoClick(resetBtn, _('Reset to built-in', 'footstrap'), axes.resetToBuiltin); twoClick(resetBtn, _('Reset to built-in', 'footstrap'), axes.resetToBuiltin);
refreshSave(); /* correct label and enabled state before the first paint */ refreshSave(); /* correct label and enabled state before the first paint */
@@ -43,17 +43,22 @@ const _uciCommit = rpc.declare({ object: 'uci', method: 'commit', params: [ 'con
* Router-side, like the login photo and for the same reason — a file cannot live in localStorage, * Router-side, like the login photo and for the same reason — a file cannot live in localStorage,
* and a pattern is something a router wears. The path is a fixed server-side constant matched * and a pattern is something a router wears. The path is a fixed server-side constant matched
* exactly by the rpcd ACL, so nothing user-controlled reaches a path. It lives under /etc so a * exactly by the rpcd ACL, so nothing user-controlled reaches a path. It lives under /etc so a
* package upgrade cannot delete it (keep.d carries it across a sysupgrade), and the served name * package upgrade cannot delete it (keep.d carries it across a sysupgrade). It keeps the .svg name
* ends in .svg because uhttpd types a file by extension. * for its own sake — the CGI handler that now serves it (root/www/cgi-bin) sets Content-Type
* itself and does not rely on uhttpd's by-extension typing the way the login photo's /www symlink
* still does.
* *
* How it is made to fit is 15-wallpaper.css's mask, not anything done to the bytes: the file * How it is made to fit is 15-wallpaper.css's mask, not anything done to the bytes: the file
* supplies the alpha and the theme the colour, so one upload reads correctly in both modes and * supplies the alpha and the theme the colour, so one upload reads correctly in both modes and
* under every palette. * under every palette.
* *
* What is refused: an SVG is a document, not a picture, and while a masked or background image * What is refused: an SVG is a document, not a picture, and while a masked or background image
* never executes script, the same file fetched from its own URL would. Uploading already needs an * never executes script, the same file fetched from its own URL would (OpenWrt forum thread
* authenticated admin session with uci write rights, so this is defence in depth — but the check is * 251930) — which is what the server-side handler (root/www/cgi-bin) now closes with response
* cheap and the failure mode is somebody else's browser. */ * headers a browser will not execute. THIS check runs in the uploading admin's OWN browser and is
* not a defence at all: a `curl` POST to the same cgi-io path skips it entirely, and the rpcd ACL
* authorises that POST regardless of what wrote it. It exists only so an admin who uploads a bad
* file by mistake is told so before it lands, and the cost of running it is cheap. */
const PAT_PATH = '/etc/footstrap/pattern.svg'; /* cgi-upload target; the ACL grants exactly this */ const PAT_PATH = '/etc/footstrap/pattern.svg'; /* cgi-upload target; the ACL grants exactly this */
const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash and then every page load */ const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash and then every page load */
/* What makes an uploaded SVG unacceptable, decided on the PARSED document and not on its text: a /* What makes an uploaded SVG unacceptable, decided on the PARSED document and not on its text: a
@@ -65,19 +70,30 @@ const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash
* no subresource is fetched, no handler is bound. So the questions are exact ones about nodes: * no subresource is fetched, no handler is bound. So the questions are exact ones about nodes:
* *
* - is it an SVG at all (a parsererror, or a root that is not <svg>, is not an image) * - is it an SVG at all (a parsererror, or a root that is not <svg>, is not an image)
* - does it carry an element that executes or embeds (script, foreignObject, iframe, …) * - does it carry an element that executes or embeds (script, foreignObject, iframe, style, …)
* - does it carry a real event-handler attribute — `^on[a-z]+$` * - does it carry a real event-handler attribute — `^on[a-z]+$`
* - does any value start a `javascript:` url * - does any value start a `javascript:` url
* - does any href point off this router; `#fragment` and `data:` stay allowed, being how a tile * - does any href point off this router — a leading `//`, or a `data:`/`blob:`/`about:` payload
* refers to its own <defs> and embeds a bitmap * - does any element carry a `style` attribute
* *
* The check is for the way the file can be reached that a mask does not cover: its own URL, opened * The check is for the way the file can be reached that a mask does not cover: its own URL, opened
* directly, same-origin with the session. * directly, same-origin with the session.
* *
* `animate`/`set` are listed for a second reason as well: they can retarget an attribute at run * `animate`/`set` are listed for a second reason as well: they can retarget an attribute at run
* time (`<set attributename="href" to="javascript:…">`), and a tile that animates repaints a * time (`<set attributename="href" to="javascript:…">`), and a tile that animates repaints a
* full-viewport layer behind every page. */ * full-viewport layer behind every page.
const PAT_BAD_TAGS = [ 'script', 'foreignobject', 'iframe', 'embed', 'object', 'audio', 'video', 'animate', 'set' ]; *
* `style` is refused both as an ELEMENT and as an ATTRIBUTE: a `<style>@import
* url("//attacker/x.css")</style>` reaches off-router with no script and no href at all, and a
* `style="…url(…)…"` attribute does the same per-element. `data:`/`blob:`/`about:` were allowed
* through `href`/`xlink:href` before this pass, on the theory that a tile only ever uses `data:` to
* embed its own bitmap — closed anyway, because the same scheme admits `data:text/html` and
* `data:image/svg+xml`, and telling those apart from a `data:image/png` bitmap means parsing the
* URI's own MIME token, which is exactly the kind of grammar-guessing this file avoids everywhere
* else. The cost is real: an admin embedding a bitmap inside the tile now needs a second uploaded
* asset referenced by a same-origin URL instead of a `data:` href — the tile itself is unaffected,
* since 15-wallpaper.css never used one. */
const PAT_BAD_TAGS = [ 'script', 'foreignobject', 'iframe', 'embed', 'object', 'audio', 'video', 'animate', 'set', 'style' ];
const SVG_NS = 'http://www.w3.org/2000/svg'; const SVG_NS = 'http://www.w3.org/2000/svg';
/* null if the parsed document is fine, otherwise the sentence to show. */ /* null if the parsed document is fine, otherwise the sentence to show. */
@@ -118,8 +134,17 @@ function _svgObjection(text) {
* refuse files that do nothing. */ * refuse files that do nothing. */
if ((/^on[a-z]+$/).test(n)) return refused; if ((/^on[a-z]+$/).test(n)) return refused;
if ((/^javascript:/i).test(v)) return refused; if ((/^javascript:/i).test(v)) return refused;
/* off-router reference. A leading `//` is protocol-relative and just as external. */ /* a `style=""` attribute reaches the same `url()` grammar as a `<style>` element,
if ((/(?:^|:)href$/).test(n) && (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i).test(v)) return refused; * per-element instead of per-document — refused for the same reason that tag is. */
if (n === 'style') return refused;
if ((/(?:^|:)href$/).test(n)) {
/* off-router reference. A leading `//` is protocol-relative and just as external. */
if ((/^(?:[a-z][a-z0-9+.-]*:)?\/\//i).test(v)) return refused;
/* `data:`/`blob:`/`about:` can carry a second document (text/html, image/svg+xml)
* behind an href that looks like a same-document reference — see the comment above
* PAT_BAD_TAGS for why this is a blanket refusal and not a MIME-token check. */
if ((/^(?:data|blob|about):/i).test(v)) return refused;
}
} }
} }
return null; return null;
@@ -2,6 +2,7 @@
'require baseclass'; 'require baseclass';
'require rpc'; 'require rpc';
'require fs-prefs as prefs'; 'require fs-prefs as prefs';
'require fs-fit as fit';
/* fs-axes — the nineteen Appearance axes and the Save-as-default machinery. /* fs-axes — the nineteen Appearance axes and the Save-as-default machinery.
* *
@@ -79,8 +80,14 @@ function colorAxis(key, attr, hueProp, colorProp) {
* in how the number formats onto the property, which is the one varying argument. The prefs.sd() field * in how the number formats onto the property, which is the one varying argument. The prefs.sd() field
* name is passed explicitly because one instance needs a rename rather than a spelling * name is passed explicitly because one instance needs a rename rather than a spelling
* ('fs-radius' -> rounding), and a factory right for four keys out of five is the trap enumAxis and * ('fs-radius' -> rounding), and a factory right for four keys out of five is the trap enumAxis and
* colorAxis name above. */ * colorAxis name above.
function propAxis(key, sdKey, prop, min, max, dfl, fmt) { *
* `after`, like listAxis's, is optional and runs once the property is set: every propAxis instance
* but Content width only repaints a rule that reads the token, so none of the other five pass it.
* Content width moves --fs-content-max, which shellGeometry() (fs-chrome.js) reads to decide
* data-narrow, so its applier must re-run the fit or the chrome goes on deciding against the width
* the column had a moment ago. */
function propAxis(key, sdKey, prop, min, max, dfl, fmt, after) {
const inRange = (n) => (typeof n === 'number' && n >= min && n <= max); const inRange = (n) => (typeof n === 'number' && n >= min && n <= max);
const def = () => { const d = prefs.sd(sdKey); return inRange(d) ? d : dfl; }; const def = () => { const d = prefs.sd(sdKey); return inRange(d) ? d : dfl; };
return { return {
@@ -96,6 +103,7 @@ function propAxis(key, sdKey, prop, min, max, dfl, fmt) {
prefs.lsSet(key, String(v)); prefs.lsSet(key, String(v));
if (v === dfl) root.style.removeProperty(prop); if (v === dfl) root.style.removeProperty(prop);
else root.style.setProperty(prop, fmt(v)); else root.style.setProperty(prop, fmt(v));
if (after) after();
} }
}; };
} }
@@ -111,7 +119,7 @@ function propAxis(key, sdKey, prop, min, max, dfl, fmt) {
* Legacy names ('rvht'/'roman'/'github') are migrated by head.ut before paint, so they never reach * Legacy names ('rvht'/'roman'/'github') are migrated by head.ut before paint, so they never reach
* currentPalette() on a loaded page; the stray fallthrough covers them anyway. */ * currentPalette() on a loaded page; the stray fallthrough covers them anyway. */
const PALETTES = [ 'hicontrast', 'bootstrap', '2020' ]; /* the non-default values; 'footstrap' = bare :root */ const PALETTES = [ 'hicontrast', 'bootstrap', '2020', 'forum' ]; /* the non-default values; 'footstrap' = bare :root */
const PALETTE = prefs.listAxis('fs-palette', 'data-palette', PALETTES, 'footstrap'); const PALETTE = prefs.listAxis('fs-palette', 'data-palette', PALETTES, 'footstrap');
const currentPalette = PALETTE.current, applyPalette = PALETTE.apply; const currentPalette = PALETTE.current, applyPalette = PALETTE.apply;
@@ -209,6 +217,19 @@ const currentDanger = DANGER.current, applyDanger = DANGER.apply;
const RADIUS = propAxis('fs-radius', 'rounding', '--fs-radius-base', 0, 20, FS_RADIUS_DEFAULT, (v) => (v + 'px')); const RADIUS = propAxis('fs-radius', 'rounding', '--fs-radius-base', 0, 20, FS_RADIUS_DEFAULT, (v) => (v + 'px'));
const currentRadius = RADIUS.current, applyRadius = RADIUS.apply, radiusDefault = RADIUS.def; const currentRadius = RADIUS.current, applyRadius = RADIUS.apply, radiusDefault = RADIUS.def;
/* Content width (issue #44): how far the reader lets the column grow past --fs-content-max's own
* 1280px. A propAxis like Rounding, pointed at that one token — the range comment lives in
* 02-tokens.css, next to the token it bounds, and is not restated here.
*
* The one propAxis instance that moves chrome geometry: shellGeometry() (fs-chrome.js) reads
* --fs-content-max on every fit, so dragging the slider must re-run it through the SAME
* fit.schedule() every other geometry-affecting axis calls, or the sidebar goes on folding at the
* width the column had before the drag. */
const FS_CWIDTH_DEFAULT = 1280;
const CWIDTH = propAxis('fs-content-width', 'content_width', '--fs-content-max', 1280, 3840,
FS_CWIDTH_DEFAULT, (v) => (v + 'px'), () => fit.schedule());
const currentContentWidth = CWIDTH.current, applyContentWidth = CWIDTH.apply, contentWidthDefault = CWIDTH.def;
/* Layout axis: horizontal top bar (the default) vs vertical sidebar. One template, one renderer — /* Layout axis: horizontal top bar (the default) vs vertical sidebar. One template, one renderer —
* CSS morphs the chrome off :root[data-layout] and toggling re-renders nothing; menu-footstrap.js * CSS morphs the chrome off :root[data-layout] and toggling re-renders nothing; menu-footstrap.js
* observes the attribute and folds the accordion into dropdowns or restores it. * observes the attribute and folds the accordion into dropdowns or restores it.
@@ -222,7 +243,7 @@ const AXIS_KEYS = [
'fs-accent', 'fs-good', 'fs-warn', 'fs-danger', 'fs-card', 'fs-control', 'fs-accent', 'fs-good', 'fs-warn', 'fs-danger', 'fs-card', 'fs-control',
'fs-bar', 'fs-line', 'fs-radius', 'fs-menu-autocollapse', 'fs-tint-strength', 'fs-bar', 'fs-line', 'fs-radius', 'fs-menu-autocollapse', 'fs-tint-strength',
'fs-density', 'fs-photo-dim', 'fs-pattern-size', 'fs-pattern-strength', 'fs-density', 'fs-photo-dim', 'fs-pattern-size', 'fs-pattern-strength',
'fs-pattern-ink' 'fs-pattern-ink', 'fs-content-width'
]; ];
/* Tint strength: a multiplier on the tint chroma (03-palettes.css), 100% being the designed /* Tint strength: a multiplier on the tint chroma (03-palettes.css), 100% being the designed
* strength and 200% the cap. 0 is not quite "no tint" — the relative colour that applies the tint * strength and 200% the cap. 0 is not quite "no tint" — the relative colour that applies the tint
@@ -291,7 +312,8 @@ function snapshotAxes() {
photo_dim: String(currentPhotoDim()), photo_dim: String(currentPhotoDim()),
pattern_size: String(currentPatternSize()), pattern_size: String(currentPatternSize()),
pattern_strength: String(currentPatternStrength()), pattern_strength: String(currentPatternStrength()),
pattern_ink: currentPatternInk() pattern_ink: currentPatternInk(),
content_width: String(currentContentWidth())
}; };
} }
/* The resolved router default (the uci value if set, else the built-in) in snapshotAxes() string /* The resolved router default (the uci value if set, else the built-in) in snapshotAxes() string
@@ -326,7 +348,8 @@ function _resolvedDefault() {
photo_dim: String(photoDimDefault()), photo_dim: String(photoDimDefault()),
pattern_size: String(patternSizeDefault()), pattern_size: String(patternSizeDefault()),
pattern_strength: String(patternStrengthDefault()), pattern_strength: String(patternStrengthDefault()),
pattern_ink: PINK.def() pattern_ink: PINK.def(),
content_width: String(contentWidthDefault())
}; };
} }
let _savedDefault = _resolvedDefault(); let _savedDefault = _resolvedDefault();
@@ -401,6 +424,7 @@ function resetToBuiltin() {
applyPatternSize(FS_PSIZE_DEFAULT); applyPatternSize(FS_PSIZE_DEFAULT);
applyPatternStrength(FS_PSTR_DEFAULT); applyPatternStrength(FS_PSTR_DEFAULT);
applyPatternInk('theme'); applyPatternInk('theme');
applyContentWidth(FS_CWIDTH_DEFAULT);
} }
/* ---- the two uploaded wallpapers, browser side ---- /* ---- the two uploaded wallpapers, browser side ----
@@ -414,7 +438,12 @@ function resetToBuiltin() {
* layer — one admin uploads once and every device sees it, pre-login included. So they are absent * layer — one admin uploads once and every device sees it, pre-login included. So they are absent
* from AXIS_KEYS, snapshotAxes() and matchesSavedDefault(), and must not move the Save button. */ * from AXIS_KEYS, snapshotAxes() and matchesSavedDefault(), and must not move the Save button. */
const PAT_SERVE = '/luci-static/footstrap/pattern.svg'; /* the uhttpd symlink the uci-default makes */ /* NOT a /www symlink, unlike BG_SERVE below: an SVG served as a plain static file is a same-origin
* document that runs script opened directly (OpenWrt forum thread 251930). This CGI handler reads
* the fixed /etc/footstrap/pattern.svg path itself and answers with CSP 'none' + sandbox and
* nosniff, headers uhttpd cannot attach to a static file — see the uci-default and the handler for
* the rest of the reasoning. Painting stays exactly as before: a URL is a URL to a mask-image. */
const PAT_SERVE = '/cgi-bin/luci-theme-footstrap-pattern';
function currentPattern() { function currentPattern() {
const t = prefs.sd('pattern'); const t = prefs.sd('pattern');
return (typeof t === 'string' && BG_TOKEN_RE.test(t)) ? t : ''; return (typeof t === 'string' && BG_TOKEN_RE.test(t)) ? t : '';
@@ -430,7 +459,10 @@ function applyPattern(tok) {
prefs.setSD('pattern', tok || ''); prefs.setSD('pattern', tok || '');
} }
const BG_SERVE = '/luci-static/footstrap/bg'; /* the uhttpd symlink the uci-default makes */ /* Still the plain uhttpd symlink the uci-default makes, unlike PAT_SERVE above: fs-assets.js
* re-encodes every login-bg upload to a JPEG on a canvas before it is sent, so the stored bytes
* are pixels only — there is no script grammar left in a raster for a direct hit to run. */
const BG_SERVE = '/luci-static/footstrap/bg';
/* the cache-bust token charset, an md5/sha hex string. One copy here; head.ut's ucode sanitiser /* the cache-bust token charset, an md5/sha hex string. One copy here; head.ut's ucode sanitiser
* and the pre-paint inline script keep their own identical copies unavoidably, running before this * and the pre-paint inline script keep their own identical copies unavoidably, running before this
* module — see the axes contract in head.ut. */ * module — see the axes contract in head.ut. */
@@ -473,6 +505,7 @@ return baseclass.extend({
currentPatternSize, applyPatternSize, currentPatternSize, applyPatternSize,
currentPatternStrength, applyPatternStrength, currentPatternStrength, applyPatternStrength,
currentPatternInk, applyPatternInk, currentPatternInk, applyPatternInk,
currentContentWidth, applyContentWidth, contentWidthDefault,
currentPattern, patternUrl, applyPattern, currentPattern, patternUrl, applyPattern,
currentLoginBg, loginBgUrl, applyLoginBg, currentLoginBg, loginBgUrl, applyLoginBg,
@@ -134,13 +134,22 @@ function resolveLen(token, dflt) {
* The defaults are stated once so the fallbacks and the sanity net below cannot restate the * The defaults are stated once so the fallbacks and the sanity net below cannot restate the
* stylesheet's widths in two places. Reaching for them means the measurement failed. */ * stylesheet's widths in two places. Reaching for them means the measurement failed. */
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56, contentMax: 1280 }; const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56, contentMax: 1280 };
/* the class name toggled below by fitShell's own escalation and by fitCluster's (measured: 16 B x3
* -> 25 B, 23 B saved) */
const CLASS_IND_COMPACT = 'fs-ind-compact';
let _geom = null, _geomDensity = null, _geomWarned = false; let _geom = null, _geomDensity = null, _geomWarned = false;
function shellGeometry() { function shellGeometry() {
const density = document.documentElement.getAttribute('data-density') || ''; const density = document.documentElement.getAttribute('data-density') || '';
/* contentMax moves with data-content-width (02-tokens.css), the other three widths do not, but
* they are one memo and the axis's own applier calls fit.schedule() same as Density's — folded
* into the same key rather than a second one, or a content-width change alone would hit the
* density-only key and go on answering the previous contentMax until density also changed */
const cwidth = document.documentElement.getAttribute('data-content-width') || '';
const key = density + '|' + cwidth;
/* the gutter is re-asked even on a memo hit: it moves with the width, not the density */ /* the gutter is re-asked even on a memo hit: it moves with the width, not the density */
if (_geom && _geomDensity === density) return _geom; if (_geom && _geomDensity === key) return _geom;
_geomDensity = density; _geomDensity = key;
const px = (name, dflt) => resolveLen(name, dflt); const px = (name, dflt) => resolveLen(name, dflt);
const g = { const g = {
contentMin: px('--fs-content-min', GEOM_DFLT.contentMin), contentMin: px('--fs-content-min', GEOM_DFLT.contentMin),
@@ -277,7 +286,7 @@ function fitChrome() {
const hadMinH = bar ? bar.style.minHeight : ''; const hadMinH = bar ? bar.style.minHeight : '';
if (pinned > 0) bar.style.minHeight = pinned + 'px'; if (pinned > 0) bar.style.minHeight = pinned + 'px';
if (bar) bar.classList.remove('fs-bar-stack', 'fs-ind-compact', 'fs-bar-actrow'); if (bar) bar.classList.remove('fs-bar-stack', CLASS_IND_COMPACT, 'fs-bar-actrow');
fitTabStrips(); fitTabStrips();
/* ---- does the main menu fit on the brand's row? ---- /* ---- does the main menu fit on the brand's row? ----
* It depends on how many sections THIS router has (stock 5, a loaded box 11), not on the * It depends on how many sections THIS router has (stock 5, a loaded box 11), not on the
@@ -294,7 +303,7 @@ function fitChrome() {
/* first step before stacking: collapse the poll pill (~90px) to an icon square and /* first step before stacking: collapse the poll pill (~90px) to an icon square and
* re-measure — often enough to keep the menu on the brand's row * re-measure — often enough to keep the menu on the brand's row
* (theme/50-toplayout.css) */ * (theme/50-toplayout.css) */
bar.classList.add('fs-ind-compact'); bar.classList.add(CLASS_IND_COMPACT);
fitTabStrips(); fitTabStrips();
if (!stripFitsOneRow(menu)) { if (!stripFitsOneRow(menu)) {
bar.classList.add('fs-bar-stack'); bar.classList.add('fs-bar-stack');
@@ -345,7 +354,7 @@ function fitCluster(bar, menu) {
if (clusterFitsBrandRow(bar, menu)) if (clusterFitsBrandRow(bar, menu))
return; return;
bar.classList.add('fs-ind-compact'); bar.classList.add(CLASS_IND_COMPACT);
if (clusterFitsBrandRow(bar, menu)) if (clusterFitsBrandRow(bar, menu))
return; return;
@@ -20,6 +20,8 @@
/* section title -> grid role. _() with no msgctxt on purpose: these must resolve to exactly what /* section title -> grid role. _() with no msgctxt on purpose: these must resolve to exactly what
* luci-mod-status resolves to, or the titles stop matching. Built once, not per poll tick. */ * luci-mod-status resolves to, or the titles stop matching. Built once, not per poll tick. */
const ROLES = { [_('System')]: 'sys', [_('Memory')]: 'mem', [_('Storage')]: 'sto' }; const ROLES = { [_('System')]: 'sys', [_('Memory')]: 'mem', [_('Storage')]: 'sto' };
/* the data-page value four call sites compare against; a string literal is not mangled, so a
* repeat is paid in full on flash every time (measured: 24 B x4 -> 37 B, 59 B saved) */
function sectionTitle(sec) { function sectionTitle(sec) {
/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits /* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
@@ -63,7 +65,9 @@ function arrange() {
/* an SPA nav can leave the observer wired while another page renders into #view: detach as soon /* an SPA nav can leave the observer wired while another page renders into #view: detach as soon
* as the route stops being the overview. body[data-page] carries the DISPATCH path from both * as the route stops being the overview. body[data-page] carries the DISPATCH path from both
* the server template and the router, so /admin/status (firstchild -> overview) matches. */ * the server template and the router, so /admin/status (firstchild -> overview) matches. */
if ((document.body.getAttribute('data-page') || '') !== 'admin-status-overview') { if ((document.body.getAttribute('data-page') || '') !== /* spelled out, not hoisted: tools/page-modules.mjs reads this value out of the module's
* SOURCE to check it against the map in menu-footstrap-common.js */
'admin-status-overview') {
stopWatch(); stopWatch();
return; return;
} }
@@ -80,7 +80,13 @@ function currentMode() {
* `data-darkmode` is the name the theme's own CSS keys off. The other two are outbound * `data-darkmode` is the name the theme's own CSS keys off. The other two are outbound
* compatibility, like the `--*-color-*` export tier: nothing in `styles/` may read them, and * compatibility, like the `--*-color-*` export tier: nothing in `styles/` may read them, and
* tools/axes.mjs fails the build if it does. */ * tools/axes.mjs fails the build if it does. */
/* the attribute name reused below by the writer, the guard's reader and both MutationObserver
* filters (measured: 15 B x4 -> 28 B, 32 B saved) */
function stampDark(root, dark) { function stampDark(root, dark) {
/* the literal stays spelled out HERE: tools/axes.mjs reads the attribute names out of
* this function's SOURCE, so a hoisted const reads as no attribute at all and the gate
* reports the pre-paint and the live applier as drifted. The 45 B a const would save are
* not worth teaching a gate to resolve them. */
root.setAttribute('data-darkmode', dark ? 'true' : 'false'); root.setAttribute('data-darkmode', dark ? 'true' : 'false');
root.setAttribute('data-theme', dark ? 'dark' : 'light'); root.setAttribute('data-theme', dark ? 'dark' : 'light');
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light'); root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
@@ -137,7 +143,7 @@ function guardDarkStamp() {
check(); check();
new MutationObserver(check).observe(root, { new MutationObserver(check).observe(root, {
attributes: true, attributes: true,
attributeFilter: ['data-darkmode', 'data-theme', 'data-bs-theme'] attributeFilter: [ 'data-darkmode', 'data-theme', 'data-bs-theme' ]
}); });
} }
/* ---- the browser's own chrome follows the page ---- /* ---- the browser's own chrome follows the page ----
@@ -192,10 +198,10 @@ _mqDark.addEventListener('change', () => {
/* ---- the four axis shapes, each written once ---- /* ---- the four axis shapes, each written once ----
* *
* Fifteen axes are four shapes, so the shape lives in a factory and each instance is one line: * Sixteen axes are four shapes, so the shape lives in a factory and each instance is one line:
* enumAxis (pattern ink), colorAxis (tint, accent, good, warn, danger), surfaceAxis (cards, * enumAxis (pattern ink), colorAxis (tint, accent, good, warn, danger), surfaceAxis (cards,
* controls, bar, borders), propAxis (rounding, tint strength, photo dim, pattern size, pattern * controls, bar, borders), propAxis (rounding, tint strength, photo dim, pattern size, pattern
* strength). Same contract throughout: `current()` is localStorage ?? def(), `def()` is the router * strength, content width). Same contract throughout: `current()` is localStorage ?? def(), `def()` is the router
* default alone, `apply()` stores the choice explicitly. None use `this` — every export is a * default alone, `apply()` stores the choice explicitly. None use `this` — every export is a
* detached method reference, so a `this` here would throw on the first call. * detached method reference, so a `this` here would throw on the first call.
* *
@@ -272,6 +278,11 @@ const DENSITIES = [ 'compact', 'large' ]; /* the two non-default values; 'normal
const DENSITY = listAxis('fs-density', 'data-density', DENSITIES, 'normal', () => fit.schedule()); const DENSITY = listAxis('fs-density', 'data-density', DENSITIES, 'normal', () => fit.schedule());
const currentDensity = DENSITY.current, applyDensity = DENSITY.apply, const currentDensity = DENSITY.current, applyDensity = DENSITY.apply,
densityDefault = DENSITY.def; densityDefault = DENSITY.def;
/* Content width lives entirely in fs-axes.js now (a propAxis like Rounding, issue #44): unlike
* Density it sets no attribute, so nothing here or in fs-chrome.js needs to read one, and
* currentContentWidth()/applyContentWidth() have no caller outside the Appearance tab — the one
* thing that kept Density's shape in this cold-path file. */
/* Background-tint axis: the canvas the cards float on (--fs-bg), so a whole install reads as one /* Background-tint axis: the canvas the cards float on (--fs-bg), so a whole install reads as one
* colour and a tab or a screenshot says which router it belongs to. Cards, chrome and the status * colour and a tab or a screenshot says which router it belongs to. Cards, chrome and the status
* colours keep the palette's values — the cue colours the paper, not the UI. On a hue it is mixed * colours keep the palette's values — the cue colours the paper, not the UI. On a hue it is mixed
@@ -219,12 +219,18 @@ function build() {
const btn = document.getElementById('fs-search-btn'); const btn = document.getElementById('fs-search-btn');
if (!btn) return null; if (!btn) return null;
/* the list's id, said three times below as an id, a class and aria-controls (measured: 17 B x3
* -> 26 B, 25 B saved) */
const ID_SEARCH_LIST = 'fs-search-list';
/* the attribute name toggled below and stated once more as its own starting value (measured:
* 15 B x3 -> 27 B, 18 B saved) */
const ATTR_EXPANDED = 'aria-expanded';
const input = E('input', { const input = E('input', {
'type': 'text', 'type': 'text',
'class': 'fs-search-input', 'class': 'fs-search-input',
'role': 'combobox', 'role': 'combobox',
'aria-controls': 'fs-search-list', 'aria-controls': ID_SEARCH_LIST,
'aria-expanded': 'true', [ATTR_EXPANDED]: 'true',
'aria-autocomplete': 'list', 'aria-autocomplete': 'list',
'aria-label': _('Search pages', 'footstrap'), 'aria-label': _('Search pages', 'footstrap'),
'placeholder': _('Search pages…', 'footstrap'), 'placeholder': _('Search pages…', 'footstrap'),
@@ -232,7 +238,7 @@ function build() {
'autocapitalize': 'off', 'autocapitalize': 'off',
'spellcheck': 'false' 'spellcheck': 'false'
}); });
const list = E('div', { 'id': 'fs-search-list', 'class': 'fs-search-list', 'role': 'listbox', 'aria-label': _('Pages', 'footstrap') }); const list = E('div', { 'id': ID_SEARCH_LIST, 'class': ID_SEARCH_LIST, 'role': 'listbox', 'aria-label': _('Pages', 'footstrap') });
const note = E('div', { 'class': 'fs-search-note' }); const note = E('div', { 'class': 'fs-search-note' });
const ico = E('span', { 'class': 'fs-search-ico' }); const ico = E('span', { 'class': 'fs-search-ico' });
ico.innerHTML = widgets.svgIcon('<circle cx="11" cy="11" r="7"/><path d="M16.5 16.5 21 21"/>'); ico.innerHTML = widgets.svgIcon('<circle cx="11" cy="11" r="7"/><path d="M16.5 16.5 21 21"/>');
@@ -329,7 +335,7 @@ function build() {
function open() { function open() {
if (!ov.hidden) return; if (!ov.hidden) return;
ov.hidden = false; ov.hidden = false;
btn.setAttribute('aria-expanded', 'true'); btn.setAttribute(ATTR_EXPANDED, 'true');
input.value = ''; input.value = '';
render(''); render('');
input.focus(); input.focus();
@@ -338,7 +344,7 @@ function build() {
function close(returnFocus = true) { function close(returnFocus = true) {
if (ov.hidden) return; if (ov.hidden) return;
ov.hidden = true; ov.hidden = true;
btn.setAttribute('aria-expanded', 'false'); btn.setAttribute(ATTR_EXPANDED, 'false');
if (returnFocus) btn.focus(); if (returnFocus) btn.focus();
} }
+81 -68
View File
@@ -11,12 +11,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.9\n" "X-Generator: Poedit 3.9\n"
#: htdocs/luci-static/resources/fs-appearance.js:401 #: htdocs/luci-static/resources/fs-appearance.js:414
msgctxt "footstrap" msgctxt "footstrap"
msgid "Accent" msgid "Accent"
msgstr "Acento" msgstr "Acento"
#: htdocs/luci-static/resources/fs-appearance.js:481 #: htdocs/luci-static/resources/fs-appearance.js:494
msgctxt "footstrap" msgctxt "footstrap"
msgid "As in file" msgid "As in file"
msgstr "Como en el archivo" msgstr "Como en el archivo"
@@ -30,12 +30,12 @@ msgctxt "footstrap"
msgid "Auto" msgid "Auto"
msgstr "Auto" msgstr "Auto"
#: htdocs/luci-static/resources/fs-appearance.js:365 #: htdocs/luci-static/resources/fs-appearance.js:378
msgctxt "footstrap" msgctxt "footstrap"
msgid "Auto-collapse" msgid "Auto-collapse"
msgstr "Colapso automático" msgstr "Colapso automático"
#: htdocs/luci-static/resources/fs-appearance.js:806 #: htdocs/luci-static/resources/fs-appearance.js:819
msgctxt "footstrap" msgctxt "footstrap"
msgid "Background" msgid "Background"
msgstr "Fondo" msgstr "Fondo"
@@ -45,22 +45,22 @@ msgctxt "footstrap"
msgid "Barely visible %s" msgid "Barely visible %s"
msgstr "Apenas visible %s" msgstr "Apenas visible %s"
#: htdocs/luci-static/resources/fs-appearance.js:424 #: htdocs/luci-static/resources/fs-appearance.js:437
msgctxt "footstrap" msgctxt "footstrap"
msgid "Borders" msgid "Borders"
msgstr "Bordes" msgstr "Bordes"
#: htdocs/luci-static/resources/fs-appearance.js:421 #: htdocs/luci-static/resources/fs-appearance.js:434
msgctxt "footstrap" msgctxt "footstrap"
msgid "Cards" msgid "Cards"
msgstr "Tarjetas" msgstr "Tarjetas"
#: htdocs/luci-static/resources/fs-appearance.js:454 #: htdocs/luci-static/resources/fs-appearance.js:467
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose SVG" msgid "Choose SVG"
msgstr "Elegir SVG" msgstr "Elegir SVG"
#: htdocs/luci-static/resources/fs-appearance.js:447 #: htdocs/luci-static/resources/fs-appearance.js:460
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose image" msgid "Choose image"
msgstr "Elegir imagen" msgstr "Elegir imagen"
@@ -70,33 +70,38 @@ msgctxt "footstrap"
msgid "Clearly visible %s" msgid "Clearly visible %s"
msgstr "Bien visible %s" msgstr "Bien visible %s"
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
#: ucode/template/themes/footstrap/header.ut:138 #: ucode/template/themes/footstrap/header.ut:138
msgid "Collapse menu" msgid "Collapse menu"
msgstr "Contraer menú" msgstr "Contraer menú"
#: htdocs/luci-static/resources/fs-appearance.js:465 #: htdocs/luci-static/resources/fs-appearance.js:478
#: htdocs/luci-static/resources/fs-appearance.js:803 #: htdocs/luci-static/resources/fs-appearance.js:816
msgctxt "footstrap" msgctxt "footstrap"
msgid "Colours" msgid "Colours"
msgstr "Colores" msgstr "Colores"
#: htdocs/luci-static/resources/fs-appearance.js:350 #: htdocs/luci-static/resources/fs-appearance.js:353
msgctxt "footstrap" msgctxt "footstrap"
msgid "Compact" msgid "Compact"
msgstr "Compacto" msgstr "Compacto"
#: htdocs/luci-static/resources/fs-appearance.js:648 #: htdocs/luci-static/resources/fs-appearance.js:661
msgctxt "footstrap" msgctxt "footstrap"
msgid "Confirm reset" msgid "Confirm reset"
msgstr "Confirmar restablecimiento" msgstr "Confirmar restablecimiento"
#: htdocs/luci-static/resources/fs-appearance.js:364
msgctxt "footstrap"
msgid "Content width"
msgstr "Ancho del contenido"
#: htdocs/luci-static/resources/fs-appearance.js:193 #: htdocs/luci-static/resources/fs-appearance.js:193
msgctxt "footstrap" msgctxt "footstrap"
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)" msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
msgstr "Contraste %s:1 (WCAG AA exige aquí %s:1)" msgstr "Contraste %s:1 (WCAG AA exige aquí %s:1)"
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "Controls" msgid "Controls"
msgstr "Controles" msgstr "Controles"
@@ -106,14 +111,14 @@ msgctxt "footstrap"
msgid "Could not process the image." msgid "Could not process the image."
msgstr "No se pudo procesar la imagen." msgstr "No se pudo procesar la imagen."
#: htdocs/luci-static/resources/fs-appearance.js:628 #: htdocs/luci-static/resources/fs-appearance.js:641
msgctxt "footstrap" msgctxt "footstrap"
msgid "Could not save the default. Reload the page and try again." msgid "Could not save the default. Reload the page and try again."
msgstr "" msgstr ""
"No se pudo guardar la configuración predeterminada. Recargue la página e " "No se pudo guardar la configuración predeterminada. Recargue la página e "
"inténtelo de nuevo." "inténtelo de nuevo."
#: htdocs/luci-static/resources/fs-appearance.js:404 #: htdocs/luci-static/resources/fs-appearance.js:417
msgctxt "footstrap" msgctxt "footstrap"
msgid "Danger" msgid "Danger"
msgstr "Peligro" msgstr "Peligro"
@@ -123,12 +128,12 @@ msgctxt "footstrap"
msgid "Dark" msgid "Dark"
msgstr "Oscuro" msgstr "Oscuro"
#: htdocs/luci-static/resources/fs-appearance.js:349 #: htdocs/luci-static/resources/fs-appearance.js:352
msgctxt "footstrap" msgctxt "footstrap"
msgid "Density" msgid "Density"
msgstr "Densidad" msgstr "Densidad"
#: htdocs/luci-static/resources/fs-appearance.js:462 #: htdocs/luci-static/resources/fs-appearance.js:475
msgctxt "footstrap" msgctxt "footstrap"
msgid "Dim" msgid "Dim"
msgstr "Oscurecimiento" msgstr "Oscurecimiento"
@@ -138,12 +143,12 @@ msgctxt "footstrap"
msgid "Easy to read %s" msgid "Easy to read %s"
msgstr "Se lee bien %s" msgstr "Se lee bien %s"
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
msgid "Expand menu" msgid "Expand menu"
msgstr "Expandir menú" msgstr "Expandir menú"
#: htdocs/luci-static/resources/fs-appearance.js:486 #: htdocs/luci-static/resources/fs-appearance.js:499
#: htdocs/luci-static/resources/fs-appearance.js:564 #: htdocs/luci-static/resources/fs-appearance.js:577
msgctxt "footstrap" msgctxt "footstrap"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
@@ -153,10 +158,10 @@ msgid ""
"Footstrap Appearance router-wide look (Save to router) and the login " "Footstrap Appearance router-wide look (Save to router) and the login "
"background and pattern uploads" "background and pattern uploads"
msgstr "" msgstr ""
"Aspecto de Footstrap para todo el router («Guardar en el router») y subida de " "Aspecto de Footstrap para todo el router («Guardar en el router») y subida "
"la imagen de fondo y del patrón" "de la imagen de fondo y del patrón"
#: htdocs/luci-static/resources/fs-appearance.js:794 #: htdocs/luci-static/resources/fs-appearance.js:807
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Footstrap theme settings apply at once and are stored permanently in this " "Footstrap theme settings apply at once and are stored permanently in this "
@@ -173,7 +178,7 @@ msgstr "Ir a la actualización del firmware..."
msgid "Go to password configuration..." msgid "Go to password configuration..."
msgstr "Ir a la configuración de contraseñas..." msgstr "Ir a la configuración de contraseñas..."
#: htdocs/luci-static/resources/fs-appearance.js:402 #: htdocs/luci-static/resources/fs-appearance.js:415
msgctxt "footstrap" msgctxt "footstrap"
msgid "Good" msgid "Good"
msgstr "Correcto" msgstr "Correcto"
@@ -183,7 +188,7 @@ msgctxt "footstrap"
msgid "Hard to read %s — large text only" msgid "Hard to read %s — large text only"
msgstr "Se lee mal %s: solo texto grande" msgstr "Se lee mal %s: solo texto grande"
#: htdocs/luci-static/resources/fs-appearance.js:802 #: htdocs/luci-static/resources/fs-appearance.js:815
msgctxt "footstrap" msgctxt "footstrap"
msgid "Interface" msgid "Interface"
msgstr "Interfaz" msgstr "Interfaz"
@@ -196,18 +201,18 @@ msgstr "¡Nombre de usuario y/o contraseña incorrectos! Inténtelo de nuevo."
msgid "JavaScript required!" msgid "JavaScript required!"
msgstr "¡Se requiere JavaScript!" msgstr "¡Se requiere JavaScript!"
#: htdocs/luci-static/resources/fs-appearance.js:364 #: htdocs/luci-static/resources/fs-appearance.js:377
msgctxt "footstrap" msgctxt "footstrap"
msgid "Keep open" msgid "Keep open"
msgstr "Mantener abierto" msgstr "Mantener abierto"
#: htdocs/luci-static/resources/fs-appearance.js:352 #: htdocs/luci-static/resources/fs-appearance.js:355
msgctxt "footstrap" msgctxt "footstrap"
msgid "Large" msgid "Large"
msgstr "Grande" msgstr "Grande"
#: htdocs/luci-static/resources/fs-appearance.js:328 #: htdocs/luci-static/resources/fs-appearance.js:328
#: htdocs/luci-static/resources/fs-appearance.js:723 #: htdocs/luci-static/resources/fs-appearance.js:736
msgctxt "footstrap" msgctxt "footstrap"
msgid "Layout" msgid "Layout"
msgstr "Diseño" msgstr "Diseño"
@@ -257,12 +262,12 @@ msgstr "No se encontraron páginas"
msgid "No password set!" msgid "No password set!"
msgstr "¡No se ha establecido ninguna contraseña!" msgstr "¡No se ha establecido ninguna contraseña!"
#: htdocs/luci-static/resources/fs-appearance.js:351 #: htdocs/luci-static/resources/fs-appearance.js:354
msgctxt "footstrap" msgctxt "footstrap"
msgid "Normal" msgid "Normal"
msgstr "Normal" msgstr "Normal"
#: htdocs/luci-static/resources/fs-appearance.js:562 #: htdocs/luci-static/resources/fs-appearance.js:575
msgctxt "footstrap" msgctxt "footstrap"
msgid "Off" msgid "Off"
msgstr "Apagado" msgstr "Apagado"
@@ -282,8 +287,8 @@ msgstr "Paleta"
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
#: htdocs/luci-static/resources/fs-appearance.js:472 #: htdocs/luci-static/resources/fs-appearance.js:485
#: htdocs/luci-static/resources/fs-appearance.js:563 #: htdocs/luci-static/resources/fs-appearance.js:576
msgctxt "footstrap" msgctxt "footstrap"
msgid "Pattern" msgid "Pattern"
msgstr "Patrón" msgstr "Patrón"
@@ -307,44 +312,44 @@ msgctxt "footstrap"
msgid "Recently visited" msgid "Recently visited"
msgstr "Visitado recientemente" msgstr "Visitado recientemente"
#: htdocs/luci-static/resources/fs-appearance.js:449 #: htdocs/luci-static/resources/fs-appearance.js:462
#: htdocs/luci-static/resources/fs-appearance.js:456 #: htdocs/luci-static/resources/fs-appearance.js:469
msgctxt "footstrap" msgctxt "footstrap"
msgid "Remove" msgid "Remove"
msgstr "Eliminar" msgstr "Eliminar"
#: htdocs/luci-static/resources/fs-appearance.js:592 #: htdocs/luci-static/resources/fs-appearance.js:605
#: htdocs/luci-static/resources/fs-appearance.js:659 #: htdocs/luci-static/resources/fs-appearance.js:672
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in" msgid "Reset to built-in"
msgstr "Restablecer al integrado" msgstr "Restablecer al integrado"
#: htdocs/luci-static/resources/fs-appearance.js:687 #: htdocs/luci-static/resources/fs-appearance.js:700
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in — go back to the look the theme ships with." msgid "Reset to built-in — go back to the look the theme ships with."
msgstr "" msgstr ""
"«Restablecer al integrado»: vuelve al aspecto con el que se distribuye el " "«Restablecer al integrado»: vuelve al aspecto con el que se distribuye el "
"tema." "tema."
#: htdocs/luci-static/resources/fs-appearance.js:588 #: htdocs/luci-static/resources/fs-appearance.js:601
#: htdocs/luci-static/resources/fs-appearance.js:658 #: htdocs/luci-static/resources/fs-appearance.js:671
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router" msgid "Reset to router"
msgstr "Restablecer al del router" msgstr "Restablecer al del router"
#: htdocs/luci-static/resources/fs-appearance.js:685 #: htdocs/luci-static/resources/fs-appearance.js:698
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router — drop this browser's changes and follow the router." msgid "Reset to router — drop this browser's changes and follow the router."
msgstr "" msgstr ""
"«Restablecer al del router»: descarta los cambios de este navegador y sigue " "«Restablecer al del router»: descarta los cambios de este navegador y sigue "
"al router." "al router."
#: htdocs/luci-static/resources/fs-appearance.js:355 #: htdocs/luci-static/resources/fs-appearance.js:368
msgctxt "footstrap" msgctxt "footstrap"
msgid "Rounding" msgid "Rounding"
msgstr "Redondeo" msgstr "Redondeo"
#: htdocs/luci-static/resources/fs-appearance.js:795 #: htdocs/luci-static/resources/fs-appearance.js:808
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save & Apply at the foot of the page belongs to the System form and does not " "Save & Apply at the foot of the page belongs to the System form and does not "
@@ -353,38 +358,38 @@ msgstr ""
"El botón «Guardar y aplicar» al pie de la página pertenece al formulario del " "El botón «Guardar y aplicar» al pie de la página pertenece al formulario del "
"Sistema y no los guarda." "Sistema y no los guarda."
#: htdocs/luci-static/resources/fs-appearance.js:578 #: htdocs/luci-static/resources/fs-appearance.js:591
#: htdocs/luci-static/resources/fs-appearance.js:610 #: htdocs/luci-static/resources/fs-appearance.js:623
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Save to router" msgid "Save to router"
msgstr "Guardar en el router" msgstr "Guardar en el router"
#: htdocs/luci-static/resources/fs-appearance.js:683 #: htdocs/luci-static/resources/fs-appearance.js:696
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save to router — store this look on the router. Browsers with a look of " "Save to router — store this look on the router. Browsers with a look of "
"their own keep it." "their own keep it."
msgstr "" msgstr ""
"«Guardar en el router»: guarda este aspecto en el router. Los navegadores con " "«Guardar en el router»: guarda este aspecto en el router. Los navegadores "
"un aspecto propio lo conservan." "con un aspecto propio lo conservan."
#: htdocs/luci-static/resources/fs-appearance.js:693 #: htdocs/luci-static/resources/fs-appearance.js:706
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved look" msgid "Saved look"
msgstr "Aspecto guardado" msgstr "Aspecto guardado"
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved to router" msgid "Saved to router"
msgstr "Guardado en el router" msgstr "Guardado en el router"
#: htdocs/luci-static/resources/fs-appearance.js:807 #: htdocs/luci-static/resources/fs-appearance.js:820
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saving" msgid "Saving"
msgstr "Guardado" msgstr "Guardado"
#: htdocs/luci-static/resources/fs-appearance.js:463 #: htdocs/luci-static/resources/fs-appearance.js:476
msgctxt "footstrap" msgctxt "footstrap"
msgid "Scale" msgid "Scale"
msgstr "Escala" msgstr "Escala"
@@ -410,7 +415,7 @@ msgctxt "footstrap"
msgid "Sidebar" msgid "Sidebar"
msgstr "Barra lateral" msgstr "Barra lateral"
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "Sidebar and bar" msgid "Sidebar and bar"
msgstr "Barra lateral y superior" msgstr "Barra lateral y superior"
@@ -428,17 +433,17 @@ msgstr "Empiece a escribir para encontrar una página"
msgid "Storage" msgid "Storage"
msgstr "Almacenamiento" msgstr "Almacenamiento"
#: htdocs/luci-static/resources/fs-appearance.js:464 #: htdocs/luci-static/resources/fs-appearance.js:477
msgctxt "footstrap" msgctxt "footstrap"
msgid "Strength" msgid "Strength"
msgstr "Intensidad" msgstr "Intensidad"
#: htdocs/luci-static/resources/fs-appearance.js:362 #: htdocs/luci-static/resources/fs-appearance.js:375
msgctxt "footstrap" msgctxt "footstrap"
msgid "Submenus" msgid "Submenus"
msgstr "Submenús" msgstr "Submenús"
#: htdocs/luci-static/resources/fs-appearance.js:804 #: htdocs/luci-static/resources/fs-appearance.js:817
msgctxt "footstrap" msgctxt "footstrap"
msgid "Surfaces" msgid "Surfaces"
msgstr "Superficies" msgstr "Superficies"
@@ -494,7 +499,7 @@ msgid "The router refused to delete the file (ubus status %d)."
msgstr "El router se negó a eliminar el archivo (estado ubus %d)." msgstr "El router se negó a eliminar el archivo (estado ubus %d)."
#: htdocs/luci-static/resources/fs-appearance.js:333 #: htdocs/luci-static/resources/fs-appearance.js:333
#: htdocs/luci-static/resources/fs-appearance.js:480 #: htdocs/luci-static/resources/fs-appearance.js:493
msgctxt "footstrap" msgctxt "footstrap"
msgid "Theme" msgid "Theme"
msgstr "Tema" msgstr "Tema"
@@ -507,7 +512,7 @@ msgstr ""
"Este enrutador no tiene contraseña. Configure una contraseña de " "Este enrutador no tiene contraseña. Configure una contraseña de "
"administrador para proteger la interfaz web." "administrador para proteger la interfaz web."
#: htdocs/luci-static/resources/fs-appearance.js:611 #: htdocs/luci-static/resources/fs-appearance.js:624
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"This browser is not storing preferences (site data is blocked), so a change " "This browser is not storing preferences (site data is blocked), so a change "
@@ -518,17 +523,17 @@ msgstr ""
"bloqueados), así que un cambio aquí dura hasta que recargues. Guardar como " "bloqueados), así que un cambio aquí dura hasta que recargues. Guardar como "
"predeterminado sigue funcionando y se aplica a todos los navegadores." "predeterminado sigue funcionando y se aplica a todos los navegadores."
#: htdocs/luci-static/resources/fs-appearance.js:725 #: htdocs/luci-static/resources/fs-appearance.js:738
msgctxt "footstrap" msgctxt "footstrap"
msgid "This theme is not translated on this router yet." msgid "This theme is not translated on this router yet."
msgstr "Este tema aún no está traducido en este router." msgstr "Este tema aún no está traducido en este router."
#: htdocs/luci-static/resources/fs-appearance.js:374 #: htdocs/luci-static/resources/fs-appearance.js:387
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint (router identification)" msgid "Tint (router identification)"
msgstr "Tinte (identificación del enrutador)" msgstr "Tinte (identificación del enrutador)"
#: htdocs/luci-static/resources/fs-appearance.js:387 #: htdocs/luci-static/resources/fs-appearance.js:400
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint strength" msgid "Tint strength"
msgstr "Intensidad del tinte" msgstr "Intensidad del tinte"
@@ -548,7 +553,7 @@ msgctxt "footstrap"
msgid "Upload failed." msgid "Upload failed."
msgstr "Error al cargar el archivo." msgstr "Error al cargar el archivo."
#: htdocs/luci-static/resources/fs-appearance.js:531 #: htdocs/luci-static/resources/fs-appearance.js:544
msgctxt "footstrap" msgctxt "footstrap"
msgid "Uploading…" msgid "Uploading…"
msgstr "Cargando…" msgstr "Cargando…"
@@ -557,12 +562,12 @@ msgstr "Cargando…"
msgid "Username" msgid "Username"
msgstr "Nombre de usuario" msgstr "Nombre de usuario"
#: htdocs/luci-static/resources/fs-appearance.js:560 #: htdocs/luci-static/resources/fs-appearance.js:573
msgctxt "footstrap" msgctxt "footstrap"
msgid "Wallpaper" msgid "Wallpaper"
msgstr "Fondo de pantalla" msgstr "Fondo de pantalla"
#: htdocs/luci-static/resources/fs-appearance.js:403 #: htdocs/luci-static/resources/fs-appearance.js:416
msgctxt "footstrap" msgctxt "footstrap"
msgid "Warning" msgid "Warning"
msgstr "Advertencia" msgstr "Advertencia"
@@ -573,7 +578,7 @@ msgid ""
msgstr "" msgstr ""
"Debe activar JavaScript en su navegador o LuCI no funcionará correctamente." "Debe activar JavaScript en su navegador o LuCI no funcionará correctamente."
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "in the sidebar" msgid "in the sidebar"
msgstr "en la barra lateral" msgstr "en la barra lateral"
@@ -583,12 +588,12 @@ msgctxt "footstrap"
msgid "on a card" msgid "on a card"
msgstr "en una tarjeta" msgstr "en una tarjeta"
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "on a control" msgid "on a control"
msgstr "en un control" msgstr "en un control"
#: htdocs/luci-static/resources/fs-appearance.js:379 #: htdocs/luci-static/resources/fs-appearance.js:392
msgctxt "footstrap" msgctxt "footstrap"
msgid "on the canvas" msgid "on the canvas"
msgstr "en el lienzo" msgstr "en el lienzo"
@@ -599,6 +604,14 @@ msgid ""
"the uploaded file could not be removed either; it is still on the router." "the uploaded file could not be removed either; it is still on the router."
msgstr "tampoco se pudo eliminar el archivo subido; sigue en el router." msgstr "tampoco se pudo eliminar el archivo subido; sigue en el router."
#~ msgctxt "footstrap"
#~ msgid "Full"
#~ msgstr "Completa"
#~ msgctxt "footstrap"
#~ msgid "Wide"
#~ msgstr "Ancha"
#~ msgctxt "footstrap" #~ msgctxt "footstrap"
#~ msgid "Defaults" #~ msgid "Defaults"
#~ msgstr "Predeterminados" #~ msgstr "Predeterminados"
+78 -66
View File
@@ -7,12 +7,12 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: htdocs/luci-static/resources/fs-appearance.js:401 #: htdocs/luci-static/resources/fs-appearance.js:414
msgctxt "footstrap" msgctxt "footstrap"
msgid "Accent" msgid "Accent"
msgstr "Акцент" msgstr "Акцент"
#: htdocs/luci-static/resources/fs-appearance.js:481 #: htdocs/luci-static/resources/fs-appearance.js:494
msgctxt "footstrap" msgctxt "footstrap"
msgid "As in file" msgid "As in file"
msgstr "Как в файле" msgstr "Как в файле"
@@ -26,12 +26,12 @@ msgctxt "footstrap"
msgid "Auto" msgid "Auto"
msgstr "Авто" msgstr "Авто"
#: htdocs/luci-static/resources/fs-appearance.js:365 #: htdocs/luci-static/resources/fs-appearance.js:378
msgctxt "footstrap" msgctxt "footstrap"
msgid "Auto-collapse" msgid "Auto-collapse"
msgstr "Сворачивать" msgstr "Сворачивать"
#: htdocs/luci-static/resources/fs-appearance.js:806 #: htdocs/luci-static/resources/fs-appearance.js:819
msgctxt "footstrap" msgctxt "footstrap"
msgid "Background" msgid "Background"
msgstr "Фон" msgstr "Фон"
@@ -41,22 +41,22 @@ msgctxt "footstrap"
msgid "Barely visible %s" msgid "Barely visible %s"
msgstr "Едва заметна %s" msgstr "Едва заметна %s"
#: htdocs/luci-static/resources/fs-appearance.js:424 #: htdocs/luci-static/resources/fs-appearance.js:437
msgctxt "footstrap" msgctxt "footstrap"
msgid "Borders" msgid "Borders"
msgstr "Границы" msgstr "Границы"
#: htdocs/luci-static/resources/fs-appearance.js:421 #: htdocs/luci-static/resources/fs-appearance.js:434
msgctxt "footstrap" msgctxt "footstrap"
msgid "Cards" msgid "Cards"
msgstr "Карточки" msgstr "Карточки"
#: htdocs/luci-static/resources/fs-appearance.js:454 #: htdocs/luci-static/resources/fs-appearance.js:467
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose SVG" msgid "Choose SVG"
msgstr "Выбрать SVG" msgstr "Выбрать SVG"
#: htdocs/luci-static/resources/fs-appearance.js:447 #: htdocs/luci-static/resources/fs-appearance.js:460
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose image" msgid "Choose image"
msgstr "Выбрать изображение" msgstr "Выбрать изображение"
@@ -66,33 +66,38 @@ msgctxt "footstrap"
msgid "Clearly visible %s" msgid "Clearly visible %s"
msgstr "Хорошо видна %s" msgstr "Хорошо видна %s"
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
#: ucode/template/themes/footstrap/header.ut:138 #: ucode/template/themes/footstrap/header.ut:138
msgid "Collapse menu" msgid "Collapse menu"
msgstr "Свернуть меню" msgstr "Свернуть меню"
#: htdocs/luci-static/resources/fs-appearance.js:465 #: htdocs/luci-static/resources/fs-appearance.js:478
#: htdocs/luci-static/resources/fs-appearance.js:803 #: htdocs/luci-static/resources/fs-appearance.js:816
msgctxt "footstrap" msgctxt "footstrap"
msgid "Colours" msgid "Colours"
msgstr "Цвета" msgstr "Цвета"
#: htdocs/luci-static/resources/fs-appearance.js:350 #: htdocs/luci-static/resources/fs-appearance.js:353
msgctxt "footstrap" msgctxt "footstrap"
msgid "Compact" msgid "Compact"
msgstr "Компактно" msgstr "Компактно"
#: htdocs/luci-static/resources/fs-appearance.js:648 #: htdocs/luci-static/resources/fs-appearance.js:661
msgctxt "footstrap" msgctxt "footstrap"
msgid "Confirm reset" msgid "Confirm reset"
msgstr "Подтвердить сброс" msgstr "Подтвердить сброс"
#: htdocs/luci-static/resources/fs-appearance.js:364
msgctxt "footstrap"
msgid "Content width"
msgstr "Ширина контента"
#: htdocs/luci-static/resources/fs-appearance.js:193 #: htdocs/luci-static/resources/fs-appearance.js:193
msgctxt "footstrap" msgctxt "footstrap"
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)" msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
msgstr "Контраст %s:1 (WCAG AA требует здесь %s:1)" msgstr "Контраст %s:1 (WCAG AA требует здесь %s:1)"
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "Controls" msgid "Controls"
msgstr "Элементы форм" msgstr "Элементы форм"
@@ -102,12 +107,12 @@ msgctxt "footstrap"
msgid "Could not process the image." msgid "Could not process the image."
msgstr "Не удалось обработать изображение." msgstr "Не удалось обработать изображение."
#: htdocs/luci-static/resources/fs-appearance.js:628 #: htdocs/luci-static/resources/fs-appearance.js:641
msgctxt "footstrap" msgctxt "footstrap"
msgid "Could not save the default. Reload the page and try again." msgid "Could not save the default. Reload the page and try again."
msgstr "Не удалось сохранить умолчание. Обновите страницу и повторите." msgstr "Не удалось сохранить умолчание. Обновите страницу и повторите."
#: htdocs/luci-static/resources/fs-appearance.js:404 #: htdocs/luci-static/resources/fs-appearance.js:417
msgctxt "footstrap" msgctxt "footstrap"
msgid "Danger" msgid "Danger"
msgstr "Опасность" msgstr "Опасность"
@@ -117,12 +122,12 @@ msgctxt "footstrap"
msgid "Dark" msgid "Dark"
msgstr "Тёмная" msgstr "Тёмная"
#: htdocs/luci-static/resources/fs-appearance.js:349 #: htdocs/luci-static/resources/fs-appearance.js:352
msgctxt "footstrap" msgctxt "footstrap"
msgid "Density" msgid "Density"
msgstr "Плотность" msgstr "Плотность"
#: htdocs/luci-static/resources/fs-appearance.js:462 #: htdocs/luci-static/resources/fs-appearance.js:475
msgctxt "footstrap" msgctxt "footstrap"
msgid "Dim" msgid "Dim"
msgstr "Затемнение" msgstr "Затемнение"
@@ -132,12 +137,12 @@ msgctxt "footstrap"
msgid "Easy to read %s" msgid "Easy to read %s"
msgstr "Хорошо читается %s" msgstr "Хорошо читается %s"
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
msgid "Expand menu" msgid "Expand menu"
msgstr "Развернуть меню" msgstr "Развернуть меню"
#: htdocs/luci-static/resources/fs-appearance.js:486 #: htdocs/luci-static/resources/fs-appearance.js:499
#: htdocs/luci-static/resources/fs-appearance.js:564 #: htdocs/luci-static/resources/fs-appearance.js:577
msgctxt "footstrap" msgctxt "footstrap"
msgid "File" msgid "File"
msgstr "Файл" msgstr "Файл"
@@ -150,7 +155,7 @@ msgstr ""
"Общий для роутера вид Footstrap («Сохранить на роутер»), а также загрузка " "Общий для роутера вид Footstrap («Сохранить на роутер»), а также загрузка "
"фонового изображения и узора" "фонового изображения и узора"
#: htdocs/luci-static/resources/fs-appearance.js:794 #: htdocs/luci-static/resources/fs-appearance.js:807
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Footstrap theme settings apply at once and are stored permanently in this " "Footstrap theme settings apply at once and are stored permanently in this "
@@ -167,7 +172,7 @@ msgstr "Перейти к обновлению прошивки..."
msgid "Go to password configuration..." msgid "Go to password configuration..."
msgstr "Перейти к настройке пароля..." msgstr "Перейти к настройке пароля..."
#: htdocs/luci-static/resources/fs-appearance.js:402 #: htdocs/luci-static/resources/fs-appearance.js:415
msgctxt "footstrap" msgctxt "footstrap"
msgid "Good" msgid "Good"
msgstr "Норма" msgstr "Норма"
@@ -177,7 +182,7 @@ msgctxt "footstrap"
msgid "Hard to read %s — large text only" msgid "Hard to read %s — large text only"
msgstr "Плохо читается %s — только крупный текст" msgstr "Плохо читается %s — только крупный текст"
#: htdocs/luci-static/resources/fs-appearance.js:802 #: htdocs/luci-static/resources/fs-appearance.js:815
msgctxt "footstrap" msgctxt "footstrap"
msgid "Interface" msgid "Interface"
msgstr "Интерфейс" msgstr "Интерфейс"
@@ -190,18 +195,18 @@ msgstr "Неверное имя пользователя и/или пароль!
msgid "JavaScript required!" msgid "JavaScript required!"
msgstr "Требуется JavaScript!" msgstr "Требуется JavaScript!"
#: htdocs/luci-static/resources/fs-appearance.js:364 #: htdocs/luci-static/resources/fs-appearance.js:377
msgctxt "footstrap" msgctxt "footstrap"
msgid "Keep open" msgid "Keep open"
msgstr "Не сворачивать" msgstr "Не сворачивать"
#: htdocs/luci-static/resources/fs-appearance.js:352 #: htdocs/luci-static/resources/fs-appearance.js:355
msgctxt "footstrap" msgctxt "footstrap"
msgid "Large" msgid "Large"
msgstr "Крупно" msgstr "Крупно"
#: htdocs/luci-static/resources/fs-appearance.js:328 #: htdocs/luci-static/resources/fs-appearance.js:328
#: htdocs/luci-static/resources/fs-appearance.js:723 #: htdocs/luci-static/resources/fs-appearance.js:736
msgctxt "footstrap" msgctxt "footstrap"
msgid "Layout" msgid "Layout"
msgstr "Раскладка" msgstr "Раскладка"
@@ -250,12 +255,12 @@ msgstr "Страницы не найдены"
msgid "No password set!" msgid "No password set!"
msgstr "Пароль не установлен!" msgstr "Пароль не установлен!"
#: htdocs/luci-static/resources/fs-appearance.js:351 #: htdocs/luci-static/resources/fs-appearance.js:354
msgctxt "footstrap" msgctxt "footstrap"
msgid "Normal" msgid "Normal"
msgstr "Обычно" msgstr "Обычно"
#: htdocs/luci-static/resources/fs-appearance.js:562 #: htdocs/luci-static/resources/fs-appearance.js:575
msgctxt "footstrap" msgctxt "footstrap"
msgid "Off" msgid "Off"
msgstr "Выкл." msgstr "Выкл."
@@ -275,8 +280,8 @@ msgstr "Палитра"
msgid "Password" msgid "Password"
msgstr "Пароль" msgstr "Пароль"
#: htdocs/luci-static/resources/fs-appearance.js:472 #: htdocs/luci-static/resources/fs-appearance.js:485
#: htdocs/luci-static/resources/fs-appearance.js:563 #: htdocs/luci-static/resources/fs-appearance.js:576
msgctxt "footstrap" msgctxt "footstrap"
msgid "Pattern" msgid "Pattern"
msgstr "Узор" msgstr "Узор"
@@ -300,43 +305,42 @@ msgctxt "footstrap"
msgid "Recently visited" msgid "Recently visited"
msgstr "Недавние" msgstr "Недавние"
#: htdocs/luci-static/resources/fs-appearance.js:449 #: htdocs/luci-static/resources/fs-appearance.js:462
#: htdocs/luci-static/resources/fs-appearance.js:456 #: htdocs/luci-static/resources/fs-appearance.js:469
msgctxt "footstrap" msgctxt "footstrap"
msgid "Remove" msgid "Remove"
msgstr "Убрать" msgstr "Убрать"
#: htdocs/luci-static/resources/fs-appearance.js:592 #: htdocs/luci-static/resources/fs-appearance.js:605
#: htdocs/luci-static/resources/fs-appearance.js:659 #: htdocs/luci-static/resources/fs-appearance.js:672
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in" msgid "Reset to built-in"
msgstr "Сбросить к встроенному" msgstr "Сбросить к встроенному"
#: htdocs/luci-static/resources/fs-appearance.js:687 #: htdocs/luci-static/resources/fs-appearance.js:700
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in — go back to the look the theme ships with." msgid "Reset to built-in — go back to the look the theme ships with."
msgstr "" msgstr "«Сбросить к встроенному» — вернуть вид, с которым поставляется тема."
"«Сбросить к встроенному» — вернуть вид, с которым поставляется тема."
#: htdocs/luci-static/resources/fs-appearance.js:588 #: htdocs/luci-static/resources/fs-appearance.js:601
#: htdocs/luci-static/resources/fs-appearance.js:658 #: htdocs/luci-static/resources/fs-appearance.js:671
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router" msgid "Reset to router"
msgstr "Сбросить к виду роутера" msgstr "Сбросить к виду роутера"
#: htdocs/luci-static/resources/fs-appearance.js:685 #: htdocs/luci-static/resources/fs-appearance.js:698
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router — drop this browser's changes and follow the router." msgid "Reset to router — drop this browser's changes and follow the router."
msgstr "" msgstr ""
"«Сбросить к виду роутера» — отменить правки этого браузера и следовать " "«Сбросить к виду роутера» — отменить правки этого браузера и следовать "
"роутеру." "роутеру."
#: htdocs/luci-static/resources/fs-appearance.js:355 #: htdocs/luci-static/resources/fs-appearance.js:368
msgctxt "footstrap" msgctxt "footstrap"
msgid "Rounding" msgid "Rounding"
msgstr "Скругление" msgstr "Скругление"
#: htdocs/luci-static/resources/fs-appearance.js:795 #: htdocs/luci-static/resources/fs-appearance.js:808
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save & Apply at the foot of the page belongs to the System form and does not " "Save & Apply at the foot of the page belongs to the System form and does not "
@@ -345,14 +349,14 @@ msgstr ""
"Кнопка «Сохранить и применить» внизу страницы принадлежит форме «Система» и " "Кнопка «Сохранить и применить» внизу страницы принадлежит форме «Система» и "
"их не сохраняет." "их не сохраняет."
#: htdocs/luci-static/resources/fs-appearance.js:578 #: htdocs/luci-static/resources/fs-appearance.js:591
#: htdocs/luci-static/resources/fs-appearance.js:610 #: htdocs/luci-static/resources/fs-appearance.js:623
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Save to router" msgid "Save to router"
msgstr "Сохранить на роутер" msgstr "Сохранить на роутер"
#: htdocs/luci-static/resources/fs-appearance.js:683 #: htdocs/luci-static/resources/fs-appearance.js:696
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save to router — store this look on the router. Browsers with a look of " "Save to router — store this look on the router. Browsers with a look of "
@@ -361,22 +365,22 @@ msgstr ""
"«Сохранить на роутер» — записать этот вид на роутер. Браузеры со своим видом " "«Сохранить на роутер» — записать этот вид на роутер. Браузеры со своим видом "
"его сохранят." "его сохранят."
#: htdocs/luci-static/resources/fs-appearance.js:693 #: htdocs/luci-static/resources/fs-appearance.js:706
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved look" msgid "Saved look"
msgstr "Сохранённый вид" msgstr "Сохранённый вид"
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved to router" msgid "Saved to router"
msgstr "Сохранено на роутере" msgstr "Сохранено на роутере"
#: htdocs/luci-static/resources/fs-appearance.js:807 #: htdocs/luci-static/resources/fs-appearance.js:820
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saving" msgid "Saving"
msgstr "Сохранение" msgstr "Сохранение"
#: htdocs/luci-static/resources/fs-appearance.js:463 #: htdocs/luci-static/resources/fs-appearance.js:476
msgctxt "footstrap" msgctxt "footstrap"
msgid "Scale" msgid "Scale"
msgstr "Масштаб" msgstr "Масштаб"
@@ -402,7 +406,7 @@ msgctxt "footstrap"
msgid "Sidebar" msgid "Sidebar"
msgstr "Сбоку" msgstr "Сбоку"
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "Sidebar and bar" msgid "Sidebar and bar"
msgstr "Боковая панель и шапка" msgstr "Боковая панель и шапка"
@@ -420,17 +424,17 @@ msgstr "Начните вводить название страницы"
msgid "Storage" msgid "Storage"
msgstr "Накопитель" msgstr "Накопитель"
#: htdocs/luci-static/resources/fs-appearance.js:464 #: htdocs/luci-static/resources/fs-appearance.js:477
msgctxt "footstrap" msgctxt "footstrap"
msgid "Strength" msgid "Strength"
msgstr "Насыщенность" msgstr "Насыщенность"
#: htdocs/luci-static/resources/fs-appearance.js:362 #: htdocs/luci-static/resources/fs-appearance.js:375
msgctxt "footstrap" msgctxt "footstrap"
msgid "Submenus" msgid "Submenus"
msgstr "Подменю" msgstr "Подменю"
#: htdocs/luci-static/resources/fs-appearance.js:804 #: htdocs/luci-static/resources/fs-appearance.js:817
msgctxt "footstrap" msgctxt "footstrap"
msgid "Surfaces" msgid "Surfaces"
msgstr "Поверхности" msgstr "Поверхности"
@@ -486,7 +490,7 @@ msgid "The router refused to delete the file (ubus status %d)."
msgstr "Роутер отказался удалить файл (статус ubus %d)." msgstr "Роутер отказался удалить файл (статус ubus %d)."
#: htdocs/luci-static/resources/fs-appearance.js:333 #: htdocs/luci-static/resources/fs-appearance.js:333
#: htdocs/luci-static/resources/fs-appearance.js:480 #: htdocs/luci-static/resources/fs-appearance.js:493
msgctxt "footstrap" msgctxt "footstrap"
msgid "Theme" msgid "Theme"
msgstr "Тема" msgstr "Тема"
@@ -499,7 +503,7 @@ msgstr ""
"На этом роутере не задан пароль. Установите пароль root, чтобы защитить веб-" "На этом роутере не задан пароль. Установите пароль root, чтобы защитить веб-"
"интерфейс." "интерфейс."
#: htdocs/luci-static/resources/fs-appearance.js:611 #: htdocs/luci-static/resources/fs-appearance.js:624
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"This browser is not storing preferences (site data is blocked), so a change " "This browser is not storing preferences (site data is blocked), so a change "
@@ -510,17 +514,17 @@ msgstr ""
"изменение здесь действует до перезагрузки страницы. «Сохранить как " "изменение здесь действует до перезагрузки страницы. «Сохранить как "
"умолчание» по-прежнему работает и применяется ко всем браузерам." "умолчание» по-прежнему работает и применяется ко всем браузерам."
#: htdocs/luci-static/resources/fs-appearance.js:725 #: htdocs/luci-static/resources/fs-appearance.js:738
msgctxt "footstrap" msgctxt "footstrap"
msgid "This theme is not translated on this router yet." msgid "This theme is not translated on this router yet."
msgstr "Тема ещё не переведена на этом роутере." msgstr "Тема ещё не переведена на этом роутере."
#: htdocs/luci-static/resources/fs-appearance.js:374 #: htdocs/luci-static/resources/fs-appearance.js:387
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint (router identification)" msgid "Tint (router identification)"
msgstr "Оттенок (опознание роутера)" msgstr "Оттенок (опознание роутера)"
#: htdocs/luci-static/resources/fs-appearance.js:387 #: htdocs/luci-static/resources/fs-appearance.js:400
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint strength" msgid "Tint strength"
msgstr "Насыщенность оттенка" msgstr "Насыщенность оттенка"
@@ -540,7 +544,7 @@ msgctxt "footstrap"
msgid "Upload failed." msgid "Upload failed."
msgstr "Не удалось загрузить." msgstr "Не удалось загрузить."
#: htdocs/luci-static/resources/fs-appearance.js:531 #: htdocs/luci-static/resources/fs-appearance.js:544
msgctxt "footstrap" msgctxt "footstrap"
msgid "Uploading…" msgid "Uploading…"
msgstr "Загрузка…" msgstr "Загрузка…"
@@ -549,12 +553,12 @@ msgstr "Загрузка…"
msgid "Username" msgid "Username"
msgstr "Имя пользователя" msgstr "Имя пользователя"
#: htdocs/luci-static/resources/fs-appearance.js:560 #: htdocs/luci-static/resources/fs-appearance.js:573
msgctxt "footstrap" msgctxt "footstrap"
msgid "Wallpaper" msgid "Wallpaper"
msgstr "Обои" msgstr "Обои"
#: htdocs/luci-static/resources/fs-appearance.js:403 #: htdocs/luci-static/resources/fs-appearance.js:416
msgctxt "footstrap" msgctxt "footstrap"
msgid "Warning" msgid "Warning"
msgstr "Предупреждение" msgstr "Предупреждение"
@@ -565,7 +569,7 @@ msgid ""
msgstr "" msgstr ""
"Включите JavaScript в браузере, иначе LuCI не будет работать корректно." "Включите JavaScript в браузере, иначе LuCI не будет работать корректно."
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "in the sidebar" msgid "in the sidebar"
msgstr "в боковой панели" msgstr "в боковой панели"
@@ -575,12 +579,12 @@ msgctxt "footstrap"
msgid "on a card" msgid "on a card"
msgstr "на карточке" msgstr "на карточке"
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "on a control" msgid "on a control"
msgstr "на элементе формы" msgstr "на элементе формы"
#: htdocs/luci-static/resources/fs-appearance.js:379 #: htdocs/luci-static/resources/fs-appearance.js:392
msgctxt "footstrap" msgctxt "footstrap"
msgid "on the canvas" msgid "on the canvas"
msgstr "на холсте" msgstr "на холсте"
@@ -591,6 +595,14 @@ msgid ""
"the uploaded file could not be removed either; it is still on the router." "the uploaded file could not be removed either; it is still on the router."
msgstr "загруженный файл тоже не удалось удалить — он остался на роутере." msgstr "загруженный файл тоже не удалось удалить — он остался на роутере."
#~ msgctxt "footstrap"
#~ msgid "Full"
#~ msgstr "Во всю ширину"
#~ msgctxt "footstrap"
#~ msgid "Wide"
#~ msgstr "Широкая"
#~ msgctxt "footstrap" #~ msgctxt "footstrap"
#~ msgid "Defaults" #~ msgid "Defaults"
#~ msgstr "Умолчания" #~ msgstr "Умолчания"
+69 -64
View File
@@ -1,12 +1,12 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8" msgstr "Content-Type: text/plain; charset=UTF-8"
#: htdocs/luci-static/resources/fs-appearance.js:401 #: htdocs/luci-static/resources/fs-appearance.js:414
msgctxt "footstrap" msgctxt "footstrap"
msgid "Accent" msgid "Accent"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:481 #: htdocs/luci-static/resources/fs-appearance.js:494
msgctxt "footstrap" msgctxt "footstrap"
msgid "As in file" msgid "As in file"
msgstr "" msgstr ""
@@ -20,12 +20,12 @@ msgctxt "footstrap"
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:365 #: htdocs/luci-static/resources/fs-appearance.js:378
msgctxt "footstrap" msgctxt "footstrap"
msgid "Auto-collapse" msgid "Auto-collapse"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:806 #: htdocs/luci-static/resources/fs-appearance.js:819
msgctxt "footstrap" msgctxt "footstrap"
msgid "Background" msgid "Background"
msgstr "" msgstr ""
@@ -35,22 +35,22 @@ msgctxt "footstrap"
msgid "Barely visible %s" msgid "Barely visible %s"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:424 #: htdocs/luci-static/resources/fs-appearance.js:437
msgctxt "footstrap" msgctxt "footstrap"
msgid "Borders" msgid "Borders"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:421 #: htdocs/luci-static/resources/fs-appearance.js:434
msgctxt "footstrap" msgctxt "footstrap"
msgid "Cards" msgid "Cards"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:454 #: htdocs/luci-static/resources/fs-appearance.js:467
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose SVG" msgid "Choose SVG"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:447 #: htdocs/luci-static/resources/fs-appearance.js:460
msgctxt "footstrap" msgctxt "footstrap"
msgid "Choose image" msgid "Choose image"
msgstr "" msgstr ""
@@ -60,33 +60,38 @@ msgctxt "footstrap"
msgid "Clearly visible %s" msgid "Clearly visible %s"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
#: ucode/template/themes/footstrap/header.ut:138 #: ucode/template/themes/footstrap/header.ut:138
msgid "Collapse menu" msgid "Collapse menu"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:465 #: htdocs/luci-static/resources/fs-appearance.js:478
#: htdocs/luci-static/resources/fs-appearance.js:803 #: htdocs/luci-static/resources/fs-appearance.js:816
msgctxt "footstrap" msgctxt "footstrap"
msgid "Colours" msgid "Colours"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:350 #: htdocs/luci-static/resources/fs-appearance.js:353
msgctxt "footstrap" msgctxt "footstrap"
msgid "Compact" msgid "Compact"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:648 #: htdocs/luci-static/resources/fs-appearance.js:661
msgctxt "footstrap" msgctxt "footstrap"
msgid "Confirm reset" msgid "Confirm reset"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:364
msgctxt "footstrap"
msgid "Content width"
msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:193 #: htdocs/luci-static/resources/fs-appearance.js:193
msgctxt "footstrap" msgctxt "footstrap"
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)" msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "Controls" msgid "Controls"
msgstr "" msgstr ""
@@ -96,12 +101,12 @@ msgctxt "footstrap"
msgid "Could not process the image." msgid "Could not process the image."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:628 #: htdocs/luci-static/resources/fs-appearance.js:641
msgctxt "footstrap" msgctxt "footstrap"
msgid "Could not save the default. Reload the page and try again." msgid "Could not save the default. Reload the page and try again."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:404 #: htdocs/luci-static/resources/fs-appearance.js:417
msgctxt "footstrap" msgctxt "footstrap"
msgid "Danger" msgid "Danger"
msgstr "" msgstr ""
@@ -111,12 +116,12 @@ msgctxt "footstrap"
msgid "Dark" msgid "Dark"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:349 #: htdocs/luci-static/resources/fs-appearance.js:352
msgctxt "footstrap" msgctxt "footstrap"
msgid "Density" msgid "Density"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:462 #: htdocs/luci-static/resources/fs-appearance.js:475
msgctxt "footstrap" msgctxt "footstrap"
msgid "Dim" msgid "Dim"
msgstr "" msgstr ""
@@ -126,12 +131,12 @@ msgctxt "footstrap"
msgid "Easy to read %s" msgid "Easy to read %s"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-chrome.js:447 #: htdocs/luci-static/resources/fs-chrome.js:453
msgid "Expand menu" msgid "Expand menu"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:486 #: htdocs/luci-static/resources/fs-appearance.js:499
#: htdocs/luci-static/resources/fs-appearance.js:564 #: htdocs/luci-static/resources/fs-appearance.js:577
msgctxt "footstrap" msgctxt "footstrap"
msgid "File" msgid "File"
msgstr "" msgstr ""
@@ -142,7 +147,7 @@ msgid ""
"background and pattern uploads" "background and pattern uploads"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:794 #: htdocs/luci-static/resources/fs-appearance.js:807
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Footstrap theme settings apply at once and are stored permanently in this " "Footstrap theme settings apply at once and are stored permanently in this "
@@ -157,7 +162,7 @@ msgstr ""
msgid "Go to password configuration..." msgid "Go to password configuration..."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:402 #: htdocs/luci-static/resources/fs-appearance.js:415
msgctxt "footstrap" msgctxt "footstrap"
msgid "Good" msgid "Good"
msgstr "" msgstr ""
@@ -167,7 +172,7 @@ msgctxt "footstrap"
msgid "Hard to read %s — large text only" msgid "Hard to read %s — large text only"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:802 #: htdocs/luci-static/resources/fs-appearance.js:815
msgctxt "footstrap" msgctxt "footstrap"
msgid "Interface" msgid "Interface"
msgstr "" msgstr ""
@@ -180,18 +185,18 @@ msgstr ""
msgid "JavaScript required!" msgid "JavaScript required!"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:364 #: htdocs/luci-static/resources/fs-appearance.js:377
msgctxt "footstrap" msgctxt "footstrap"
msgid "Keep open" msgid "Keep open"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:352 #: htdocs/luci-static/resources/fs-appearance.js:355
msgctxt "footstrap" msgctxt "footstrap"
msgid "Large" msgid "Large"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:328 #: htdocs/luci-static/resources/fs-appearance.js:328
#: htdocs/luci-static/resources/fs-appearance.js:723 #: htdocs/luci-static/resources/fs-appearance.js:736
msgctxt "footstrap" msgctxt "footstrap"
msgid "Layout" msgid "Layout"
msgstr "" msgstr ""
@@ -238,12 +243,12 @@ msgstr ""
msgid "No password set!" msgid "No password set!"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:351 #: htdocs/luci-static/resources/fs-appearance.js:354
msgctxt "footstrap" msgctxt "footstrap"
msgid "Normal" msgid "Normal"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:562 #: htdocs/luci-static/resources/fs-appearance.js:575
msgctxt "footstrap" msgctxt "footstrap"
msgid "Off" msgid "Off"
msgstr "" msgstr ""
@@ -263,8 +268,8 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:472 #: htdocs/luci-static/resources/fs-appearance.js:485
#: htdocs/luci-static/resources/fs-appearance.js:563 #: htdocs/luci-static/resources/fs-appearance.js:576
msgctxt "footstrap" msgctxt "footstrap"
msgid "Pattern" msgid "Pattern"
msgstr "" msgstr ""
@@ -288,76 +293,76 @@ msgctxt "footstrap"
msgid "Recently visited" msgid "Recently visited"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:449 #: htdocs/luci-static/resources/fs-appearance.js:462
#: htdocs/luci-static/resources/fs-appearance.js:456 #: htdocs/luci-static/resources/fs-appearance.js:469
msgctxt "footstrap" msgctxt "footstrap"
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:592 #: htdocs/luci-static/resources/fs-appearance.js:605
#: htdocs/luci-static/resources/fs-appearance.js:659 #: htdocs/luci-static/resources/fs-appearance.js:672
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in" msgid "Reset to built-in"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:687 #: htdocs/luci-static/resources/fs-appearance.js:700
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to built-in — go back to the look the theme ships with." msgid "Reset to built-in — go back to the look the theme ships with."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:588 #: htdocs/luci-static/resources/fs-appearance.js:601
#: htdocs/luci-static/resources/fs-appearance.js:658 #: htdocs/luci-static/resources/fs-appearance.js:671
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router" msgid "Reset to router"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:685 #: htdocs/luci-static/resources/fs-appearance.js:698
msgctxt "footstrap" msgctxt "footstrap"
msgid "Reset to router — drop this browser's changes and follow the router." msgid "Reset to router — drop this browser's changes and follow the router."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:355 #: htdocs/luci-static/resources/fs-appearance.js:368
msgctxt "footstrap" msgctxt "footstrap"
msgid "Rounding" msgid "Rounding"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:795 #: htdocs/luci-static/resources/fs-appearance.js:808
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save & Apply at the foot of the page belongs to the System form and does not " "Save & Apply at the foot of the page belongs to the System form and does not "
"store them." "store them."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:578 #: htdocs/luci-static/resources/fs-appearance.js:591
#: htdocs/luci-static/resources/fs-appearance.js:610 #: htdocs/luci-static/resources/fs-appearance.js:623
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Save to router" msgid "Save to router"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:683 #: htdocs/luci-static/resources/fs-appearance.js:696
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"Save to router — store this look on the router. Browsers with a look of " "Save to router — store this look on the router. Browsers with a look of "
"their own keep it." "their own keep it."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:693 #: htdocs/luci-static/resources/fs-appearance.js:706
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved look" msgid "Saved look"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:617 #: htdocs/luci-static/resources/fs-appearance.js:630
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saved to router" msgid "Saved to router"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:807 #: htdocs/luci-static/resources/fs-appearance.js:820
msgctxt "footstrap" msgctxt "footstrap"
msgid "Saving" msgid "Saving"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:463 #: htdocs/luci-static/resources/fs-appearance.js:476
msgctxt "footstrap" msgctxt "footstrap"
msgid "Scale" msgid "Scale"
msgstr "" msgstr ""
@@ -383,7 +388,7 @@ msgctxt "footstrap"
msgid "Sidebar" msgid "Sidebar"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "Sidebar and bar" msgid "Sidebar and bar"
msgstr "" msgstr ""
@@ -401,17 +406,17 @@ msgstr ""
msgid "Storage" msgid "Storage"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:464 #: htdocs/luci-static/resources/fs-appearance.js:477
msgctxt "footstrap" msgctxt "footstrap"
msgid "Strength" msgid "Strength"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:362 #: htdocs/luci-static/resources/fs-appearance.js:375
msgctxt "footstrap" msgctxt "footstrap"
msgid "Submenus" msgid "Submenus"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:804 #: htdocs/luci-static/resources/fs-appearance.js:817
msgctxt "footstrap" msgctxt "footstrap"
msgid "Surfaces" msgid "Surfaces"
msgstr "" msgstr ""
@@ -466,7 +471,7 @@ msgid "The router refused to delete the file (ubus status %d)."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:333 #: htdocs/luci-static/resources/fs-appearance.js:333
#: htdocs/luci-static/resources/fs-appearance.js:480 #: htdocs/luci-static/resources/fs-appearance.js:493
msgctxt "footstrap" msgctxt "footstrap"
msgid "Theme" msgid "Theme"
msgstr "" msgstr ""
@@ -477,7 +482,7 @@ msgid ""
"protect the web interface." "protect the web interface."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:611 #: htdocs/luci-static/resources/fs-appearance.js:624
msgctxt "footstrap" msgctxt "footstrap"
msgid "" msgid ""
"This browser is not storing preferences (site data is blocked), so a change " "This browser is not storing preferences (site data is blocked), so a change "
@@ -485,17 +490,17 @@ msgid ""
"every browser." "every browser."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:725 #: htdocs/luci-static/resources/fs-appearance.js:738
msgctxt "footstrap" msgctxt "footstrap"
msgid "This theme is not translated on this router yet." msgid "This theme is not translated on this router yet."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:374 #: htdocs/luci-static/resources/fs-appearance.js:387
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint (router identification)" msgid "Tint (router identification)"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:387 #: htdocs/luci-static/resources/fs-appearance.js:400
msgctxt "footstrap" msgctxt "footstrap"
msgid "Tint strength" msgid "Tint strength"
msgstr "" msgstr ""
@@ -515,7 +520,7 @@ msgctxt "footstrap"
msgid "Upload failed." msgid "Upload failed."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:531 #: htdocs/luci-static/resources/fs-appearance.js:544
msgctxt "footstrap" msgctxt "footstrap"
msgid "Uploading…" msgid "Uploading…"
msgstr "" msgstr ""
@@ -524,12 +529,12 @@ msgstr ""
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:560 #: htdocs/luci-static/resources/fs-appearance.js:573
msgctxt "footstrap" msgctxt "footstrap"
msgid "Wallpaper" msgid "Wallpaper"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:403 #: htdocs/luci-static/resources/fs-appearance.js:416
msgctxt "footstrap" msgctxt "footstrap"
msgid "Warning" msgid "Warning"
msgstr "" msgstr ""
@@ -539,7 +544,7 @@ msgid ""
"You must enable JavaScript in your browser or LuCI will not work properly." "You must enable JavaScript in your browser or LuCI will not work properly."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:423 #: htdocs/luci-static/resources/fs-appearance.js:436
msgctxt "footstrap" msgctxt "footstrap"
msgid "in the sidebar" msgid "in the sidebar"
msgstr "" msgstr ""
@@ -549,12 +554,12 @@ msgctxt "footstrap"
msgid "on a card" msgid "on a card"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:422 #: htdocs/luci-static/resources/fs-appearance.js:435
msgctxt "footstrap" msgctxt "footstrap"
msgid "on a control" msgid "on a control"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/fs-appearance.js:379 #: htdocs/luci-static/resources/fs-appearance.js:392
msgctxt "footstrap" msgctxt "footstrap"
msgid "on the canvas" msgid "on the canvas"
msgstr "" msgstr ""
@@ -1,7 +1,9 @@
#!/bin/sh #!/bin/sh
# Registration, plus the three symlinks that expose what the admin uploads. Nothing here migrates a # Registration, plus the two symlinks that expose what the admin uploads (a third, the pattern, is
# router off an older footstrap: OpenWrt expects a sysupgrade rather than a package upgrade. # deliberately NOT one — see below). Nothing here migrates a router off an older footstrap: OpenWrt
# expects a sysupgrade rather than a package upgrade — the one exception is the stale pattern
# symlink itself, removed below because leaving it would keep a reported security hole open.
changed=0 changed=0
@@ -23,13 +25,22 @@ fi
[ "$changed" = 1 ] && uci commit luci [ "$changed" = 1 ] && uci commit luci
# uhttpd serves /www only, so the admin's uploads live in /etc — where a sysupgrade keeps them # uhttpd serves /www only, so the admin's uploads live in /etc — where a sysupgrade keeps them
# (lib/upgrade/keep.d) — and are reached through symlinks. The pattern keeps its .svg name: uhttpd # (lib/upgrade/keep.d) — and are reached through symlinks, EXCEPT the pattern (below).
# types a response by extension, and an SVG sent as octet-stream paints nowhere. #
# The pattern is deliberately NOT symlinked into /www: an SVG is a document, and served as one
# through a plain same-origin URL it runs as a script with the admin's session (OpenWrt forum
# thread 251930). It is instead read by cgi-bin/luci-theme-footstrap-pattern, which answers with
# CSP 'none' + sandbox and nosniff — headers uhttpd cannot attach to a static file. The login
# background stays a plain symlink: fs-assets.js re-encodes every upload to a JPEG on a canvas
# first, so there is no script grammar left in the bytes for a direct hit to run.
mkdir -p /etc/footstrap mkdir -p /etc/footstrap
if [ -d /www/luci-static/footstrap ]; then if [ -d /www/luci-static/footstrap ]; then
ln -sf /etc/footstrap/login-bg /www/luci-static/footstrap/bg ln -sf /etc/footstrap/login-bg /www/luci-static/footstrap/bg
ln -sf /etc/footstrap/pattern.svg /www/luci-static/footstrap/pattern.svg
ln -sfn /etc/footstrap/fonts /www/luci-static/footstrap/fonts ln -sfn /etc/footstrap/fonts /www/luci-static/footstrap/fonts
fi fi
# Clean up a pre-0.15 install's stale pattern symlink — see the header comment above for why this
# is the one migration this script makes. `-L` only: never remove a real file some other install
# step could have left at that path.
[ -L /www/luci-static/footstrap/pattern.svg ] && rm -f /www/luci-static/footstrap/pattern.svg
exit 0 exit 0
@@ -0,0 +1,43 @@
#!/bin/sh
# Serves the admin-uploaded wallpaper pattern with headers a browser will not execute: opened
# directly, the /www symlink this replaces was a same-origin document with an authenticated LuCI
# session reachable from it (OpenWrt forum thread 251930, confirmed on a stand — a marker SVG set
# document.title, read location.origin as the router's, and reached document.cookie, pre-login
# too, since /www needs no session). The theme's own rendering never hits this: 15-wallpaper.css
# paints the same file through mask-image or a tiled background-image, and neither ever runs
# script inside it — the exposure was only the direct URL, which is what CSP 'none' + sandbox and
# nosniff below close, without touching how the mask/background paints it.
#
# Deliberately unauthenticated, on purpose and not an oversight: the pattern paints before login
# too (head.ut's pre-paint, from window.__fsSD). That is safe here ONLY because this script takes
# NO input at all — FILE below is a shell constant, never $QUERY_STRING, $PATH_INFO or anything
# else request-supplied — so there is nothing to traverse and nothing else this handler can be
# made to read.
#
# The login background (/etc/footstrap/login-bg) stays a plain /www symlink and does NOT get this
# treatment: fs-assets.js re-encodes every upload to a JPEG on a canvas before it is sent, so the
# stored bytes are pixels only and there is no script grammar left in the file for a direct hit to
# run — see docs/package.md.
FILE=/etc/footstrap/pattern.svg
if [ ! -r "$FILE" ]; then
printf 'Status: 404 Not Found\r\n'
printf 'Content-Type: text/plain\r\n'
printf '\r\n'
exit 0
fi
printf 'Status: 200 OK\r\n'
printf 'Content-Type: image/svg+xml\r\n'
# Cacheable forever, because the URL is not stable: fs-axes.js appends `?v=<token>` and the token
# changes with every upload, so a new file is a new URL. Without this the /www symlink's ETag and
# Last-Modified were gone and the tile was re-fetched on EVERY page load - a background image on a
# router that serves it off flash with no gzip, which is the cost this handler must not add.
printf 'Cache-Control: public, max-age=31536000, immutable\r\n'
printf 'X-Content-Type-Options: nosniff\r\n'
printf "Content-Security-Policy: default-src 'none'; sandbox\r\n"
printf '\r\n'
cat "$FILE"
exit 0
+49
View File
@@ -169,6 +169,29 @@
--fs-bar-bg: color-mix(in srgb, var(--fs-panel) 88%, transparent); --fs-bar-bg: color-mix(in srgb, var(--fs-panel) 88%, transparent);
/* height of that bar, and the scroll-margin every anchor target needs to clear it /* height of that bar, and the scroll-margin every anchor target needs to clear it
* (base/10-reset.css): if the two drift, an in-page jump lands under the bar */ * (base/10-reset.css): if the two drift, an in-page jump lands under the bar */
/* The chrome's own controls — the square buttons (search, appearance, log out) and the poll
* pill — ride the bar rather than cutting panel-coloured holes in it: the bar is one surface,
* and a control on it is carried by its hairline, not by a second fill. `transparent`, not a
* copy of --fs-bar-bg, so the strip's own translucency is not applied twice.
*
* This gives up what `.label`'s comment in theme/35-alerts.css leans on — that a coloured
* variant sits on --fs-panel2, where the palette guarantees >=4.5:1 by construction. On the
* bar the backdrop is now whatever the strip lands on, so the ratio is a MEASURED fact per
* palette rather than a guaranteed one; the numbers live in the changelog entry. A palette that
* needs the old behaviour sets this back to var(--fs-panel2). */
--fs-bar-ctl-bg: transparent;
/* The select's chevron, drawn rather than fetched. It used to be an inline SVG data-URI, and
* a URI cannot read var() — the stroke colour had to be written INTO the string, so the
* declaration was repeated once per palette and once per mode: ten copies of one glyph, ~2.4 KB
* of the shipped sheet, and ten chances for a palette to be recoloured while its chevron was
* not. Two gradient bands cost one declaration and take their colour from the palette's own
* --fs-dim at paint time, so a new palette gets a correct chevron for free. The 45% / 55% stops
* are the band's thickness; the two layers are positioned by the rules that use this, since
* only they know the field's inset. What is given up is the round line cap the SVG had, which
* at 12px is not visible — compared side by side before the swap. */
--fs-select-chevron:
linear-gradient(-45deg, transparent 45%, var(--fs-dim) 45% 55%, transparent 55%),
linear-gradient(45deg, transparent 45%, var(--fs-dim) 45% 55%, transparent 55%);
--fs-bar-h: calc(46px * var(--fs-density-box)); --fs-bar-h: calc(46px * var(--fs-density-box));
/* the square icon button in the bar and rail: the logo box, the .fs-themerow controls, /* the square icon button in the bar and rail: the logo box, the .fs-themerow controls,
* Log out and the collapsed poll pill are one size, so the right cluster reads as a row * Log out and the collapsed poll pill are one size, so the right cluster reads as a row
@@ -509,6 +532,32 @@
--fs-density-space: 1; --fs-density-space: 1;
} }
/* ---- the content-width axis ----
*
* No :root[data-content-width] blocks here (issue #44 moved this from a three-step picker to a
* slider): the value is a real px length written straight onto --fs-content-max as an inline
* style by fs-axes.js's propAxis, the same shape Rounding uses, and read back exactly as before
* by `.fs-content`, the modal width calc and the top layout's centring padding
* (theme/20-shell.css, theme/50-toplayout.css) — deliberately including the modal, which follows
* the column on purpose rather than keeping its own separate cap the reader would have to
* reconcile against.
*
* The range is 1280-3840, and neither end is derived from this file: 1280 (--fs-content-max
* above) is the value the theme has always drawn, so the slider's left end IS today's look and
* the axis can never go narrower than the default — a difference from every other numeric axis,
* none of which has a floor that also has to be the default. --fs-content-min (500px) was the
* rule this axis had to stay clear of; with 1280 as the floor it is now a property of the range
* instead, 780px below the nearest point the slider can reach. 3840 stands in for "uncapped": no
* CSS viewport width in ordinary use reaches it, since even a physical 4K panel reports a scaled
* CSS width well under it — and `columnWidth()`'s `Math.min(state.outerW - cut, g.contentMax)`
* needs a real px length either way, `none`/`100%` failing both the `calc()` in
* 50-toplayout.css and `px()`'s getComputedStyle probe in fs-chrome.js.
*
* Step 128px: 2,560px of travel at 20 stops, matching Rounding/Photo dim/Pattern strength's stop
* count rather than Pattern scale's near-pixel drag — a stop here should move a table's column
* count, not shave a pixel off a margin, and 128 is the largest round number that divides the
* range into stops that few. */
:root[data-darkmode="true"] { :root[data-darkmode="true"] {
/* palette colours are in 03-palettes.css; only mode-level, palette-independent /* palette colours are in 03-palettes.css; only mode-level, palette-independent
* tokens live here */ * tokens live here */
+90 -10
View File
@@ -2,8 +2,9 @@
/* ---- colourways (palettes) ---- /* ---- colourways (palettes) ----
* *
* Every colour is a variable and every palette is one self-contained block per mode, so a new * Every colour is a variable and every palette is one self-contained block per mode, so a new
* colourway is copy-one-pair-and-recolour. Four ship: footstrap (GitHub Primer, the default), * colourway is copy-one-pair-and-recolour. Five ship: footstrap (GitHub Primer, the default),
* hicontrast, bootstrap (the stock LuCI theme's colours) and 2020. * hicontrast, bootstrap (the stock LuCI theme's colours), 2020 (the OpenWrt 2020 theme's
* colourway) and forum (the OpenWrt forum's Discourse colourway).
* *
* TO ADD A COLOURWAY "sunset": * TO ADD A COLOURWAY "sunset":
* 1. Copy the two hicontrast blocks below, rename data-palette to "sunset". * 1. Copy the two hicontrast blocks below, rename data-palette to "sunset".
@@ -262,7 +263,6 @@
* label. */ * label. */
--fs-dim: #33383d; --fs-dim: #33383d;
--fs-faint: #3d4348; --fs-faint: #3d4348;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%2333383d" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
--fs-accent-base: #0969da; --fs-accent-base: #0969da;
/* a tint OF --fs-accent, mixed FROM it: a restated literal goes stale the moment the accent /* a tint OF --fs-accent, mixed FROM it: a restated literal goes stale the moment the accent
* is recoloured or the accent-hue axis rotates it */ * is recoloured or the accent-hue axis rotates it */
@@ -287,7 +287,6 @@
--fs-text: #adbac7; --fs-text: #adbac7;
--fs-dim: #8f9cab; --fs-dim: #8f9cab;
--fs-faint: #959ca5; --fs-faint: #959ca5;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%238f9cab" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
--fs-accent-base: #569df5; --fs-accent-base: #569df5;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
--fs-track: #373e47; --fs-track: #373e47;
@@ -320,7 +319,6 @@
* 12.0:1 against the default palette's 11.8 and 10.0 */ * 12.0:1 against the default palette's 11.8 and 10.0 */
--fs-dim: #272f3d; --fs-dim: #272f3d;
--fs-faint: #2e3745; --fs-faint: #2e3745;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%23272f3d" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
--fs-accent-base: #0b6fbd; --fs-accent-base: #0b6fbd;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eaeef4; --fs-track: #eaeef4;
@@ -342,7 +340,6 @@
--fs-text: #e8edf5; --fs-text: #e8edf5;
--fs-dim: #9ba6b8; --fs-dim: #9ba6b8;
--fs-faint: #8f9aad; --fs-faint: #8f9aad;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%239ba6b8" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
--fs-accent-base: #3ba7ff; --fs-accent-base: #3ba7ff;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 14%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 14%, transparent);
--fs-track: #1b2230; --fs-track: #1b2230;
@@ -396,7 +393,6 @@
--fs-text: #232323; --fs-text: #232323;
--fs-dim: #383838; --fs-dim: #383838;
--fs-faint: #424242; --fs-faint: #424242;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%23383838" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
/* --primary-color-medium; -high (#1976d2) measured 4.22:1 on --fs-panel2 */ /* --primary-color-medium; -high (#1976d2) measured 4.22:1 on --fs-panel2 */
--fs-accent-base: #1564c0; --fs-accent-base: #1564c0;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
@@ -430,7 +426,6 @@
* than collapsing --text-color-medium and -low onto one colour. */ * than collapsing --text-color-medium and -low onto one colour. */
--fs-dim: #a6a6a6; --fs-dim: #a6a6a6;
--fs-faint: #9a9a9a; --fs-faint: #9a9a9a;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%23a6a6a6" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
--fs-accent-base: #53a4c2; --fs-accent-base: #53a4c2;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
--fs-track: #333333; --fs-track: #333333;
@@ -470,7 +465,6 @@
--fs-text: #10202b; --fs-text: #10202b;
--fs-dim: #2c4150; --fs-dim: #2c4150;
--fs-faint: #3d5566; --fs-faint: #3d5566;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%232c4150" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
/* #00B5E2 held at hue, taken down until it reads: 5.89:1 on white against its own 2.09:1 */ /* #00B5E2 held at hue, taken down until it reads: 5.89:1 on white against its own 2.09:1 */
--fs-accent-base: #036c92; --fs-accent-base: #036c92;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
@@ -493,7 +487,6 @@
--fs-text: #e6f1f7; --fs-text: #e6f1f7;
--fs-dim: #b8cfdd; --fs-dim: #b8cfdd;
--fs-faint: #9dbccd; --fs-faint: #9dbccd;
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%23b8cfdd" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
/* the CI cyan, one step up so it clears AA on the lightest of the three surfaces too */ /* the CI cyan, one step up so it clears AA on the lightest of the three surfaces too */
--fs-accent-base: #35c6ea; --fs-accent-base: #35c6ea;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
@@ -507,4 +500,91 @@
--fs-on-warn: #002b49; --fs-on-warn: #002b49;
--fs-on-danger: #002b49; --fs-on-danger: #002b49;
} }
/* ---- forum — the OpenWrt forum's Discourse colourway (opt-in) ----
*
* Sampled from forum.openwrt.org's own `getComputedStyle(document.documentElement)`, one read
* per colour scheme: LIGHT --primary #222 (ink) / --secondary #fff (page) / --tertiary #0088cc
* (accent) / --quaternary and --danger both #e45735 / --success #009900 / --highlight #ffff4d;
* DARK --primary #ddd / --secondary #222 / --tertiary #0f82af / --danger #e45735 (unchanged
* from light) / --success #1ca551 / --highlight #a87137. Discourse names quaternary and
* danger as one colour in light and two in dark (#c14924); footstrap has no fifth semantic
* slot, so the dark quaternary is not carried — --fs-danger reads the site's own --danger.
*
* The header bar's navy (rgb(0,43,73) = #002b49) is not here: it is the forum's masthead, not
* its dark canvas — --secondary is #222 in BOTH the forum's own light and dark schemes (ink
* and page swap places, the page itself never turns navy). 2020's dark canvas is navy because
* that theme has no dark scheme of its own to sample; forum does, and it says #222. The chrome
* reads --fs-panel regardless (2020's own note, above), so the bar could not carry it anyway.
*
* None of the six sampled colours clears 4.5:1 as TEXT on the surface its own scheme pairs it
* with — Discourse checks contrast for its own use (a filled pill, an icon), not for the
* export tier every app prints text in — so every one of them is moved the way 2020's cyan
* was: hue and saturation held, lightness walked until the ratio clears. --fs-highlight has no
* footstrap analogue (it marks a search hit, not a status) and the sampled #ffff4d is a
* background wash — 1.07:1 on white — so it fills the one status slot forum's own scheme has
* no colour for, --fs-warn, exactly as its LIGHTNESS says: the brightest of the four before the
* move, still the one furthest from AA after it. */
:root[data-palette="forum"] {
/* LIGHT — the forum's own page, exactly: white canvas, near-black ink */
--fs-bg-base: #ffffff;
--fs-panel-base: #ffffff;
--fs-panel2-base: #f0f0f0;
--fs-border-base: #dddddd;
--fs-text: #222222;
--fs-dim: #3d3d3d;
--fs-faint: #4a4a4a;
/* --tertiary #0088cc measures 3.89:1 on white; #0074ad held the hue at 5.12:1 there but
* only 4.49:1 on --fs-panel2, under the 4.5:1 floor the export tier holds every surface to;
* #0073ac holds it at 5.19:1 on white and 4.55:1 on --fs-panel2 */
--fs-accent-base: #0073ac;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 10%, transparent);
--fs-track: #eeeeee;
/* --success #009900 measured 3.78:1, #008000 5.14:1; --quaternary/--danger #e45735
* measured 3.68:1, #c63c1b 5.17:1; --highlight #ffff4d measured 1.07:1 as text, #717100
* 5.17:1 — see the block comment above for why highlight fills warn */
--fs-good-base: #008000;
--fs-warn-base: #717100;
--fs-danger-base: #c63c1b;
/* white on the fills above: 5.19/5.14/5.17/5.17 */
--fs-on-accent: #fff;
--fs-on-good: #fff;
--fs-on-warn: #fff;
--fs-on-danger: #fff;
}
:root[data-palette="forum"][data-darkmode="true"] {
/* DARK — the forum's own dark scheme: #222 canvas, #ddd ink, both exactly as sampled */
--fs-bg-base: #222222;
--fs-panel-base: #262626;
--fs-panel2-base: #2e2e2e;
--fs-border-base: #3a3a3a;
--fs-text: #dddddd;
--fs-dim: #a3a3a3;
--fs-faint: #969696;
/* --tertiary #0f82af measured 3.66:1 on --fs-bg and 3.12:1 on --fs-panel2, both under the
* 4.5:1 the export tier wants for a colour printed as text; #13a8e3 holds the hue at
* 5.86:1 on --fs-bg and 5.00:1 on --fs-panel2 */
--fs-accent-base: #13a8e3;
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
--fs-track: #333333;
/* --success #1ca551 measured 4.24:1 on --fs-panel2, #1fb65a 5.11:1; --danger #e45735
* measured 3.69:1, #eb8066 5.05:1; --highlight #a87137 measured 3.29:1, #c99259 5.00:1 —
* same reasoning as the light block for why highlight fills warn */
--fs-good-base: #1fb65a;
--fs-warn-base: #c99259;
--fs-danger-base: #eb8066;
/* dark ink: these fills are light, and #222222 (= --fs-bg-base) gives 5.86/5.98/5.86/5.92 */
--fs-on-accent: #222222;
--fs-on-good: #222222;
--fs-on-warn: #222222;
--fs-on-danger: #222222;
/* The masthead, and the one place the forum's navy belongs. rgb(0,43,73) is what
forum.openwrt.org paints its header with in its DARK scheme — measured live; its light
scheme has a white header (--header_background #ffffff, ink #333333), which is what this
palette's light half already gives, so the navy is declared here and only here.
color-mix, not a flat fill: --fs-bar-bg is 88% of the panel over the page on purpose (a
bar is a thin strip the page ghosts through, 02-tokens.css), and a flat colour would drop
that. Over this palette's #222 canvas the strip lands near #06294a, where the bar's own
ink (#dddddd) measures 9.9:1 and its dimmed ink (#a3a3a3) 5.4:1. */
--fs-bar-bg: color-mix(in srgb, #002b49 88%, transparent);
}
} }
@@ -190,6 +190,7 @@
display: flex; flex-wrap: wrap; gap: var(--fs-space-1-5); display: flex; flex-wrap: wrap; gap: var(--fs-space-1-5);
order: 2; margin-inline-start: auto; padding: 0; order: 2; margin-inline-start: auto; padding: 0;
} }
.fs-themerow { .fs-themerow {
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
flex: 0 0 auto; order: 2; margin-inline-start: auto; flex: 0 0 auto; order: 2; margin-inline-start: auto;
@@ -197,7 +198,7 @@
* 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: var(--fs-hairline); 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-bar-ctl-bg); 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;
transition: background var(--fs-dur), border-color var(--fs-dur), color var(--fs-dur); transition: background var(--fs-dur), border-color var(--fs-dur), color var(--fs-dur);
@@ -262,7 +263,7 @@
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: var(--fs-hairline); border-radius: var(--fs-radius); border: var(--fs-hairline); border-radius: var(--fs-radius);
background: var(--fs-panel2); background: var(--fs-bar-ctl-bg);
} }
/* 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
* shared hover/active groups in 10-chrome.css */ * shared hover/active groups in 10-chrome.css */
@@ -769,7 +770,11 @@
min-height: var(--fs-btn-size); min-height: var(--fs-btn-size);
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; /* the bar's control surface, not the panel: this pill sits in the same row as the square
* buttons and wears what they wear. Verified on the live DOM rather than by reading —
* ui.js emits the indicator as a bare `<span data-indicator=…>` with NO class, so a rule
* written against `.label` (as one here briefly was) matches nothing at all. */
background: var(--fs-bar-ctl-bg); background-image: none;
border: var(--fs-hairline); 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;
+52 -46
View File
@@ -313,17 +313,9 @@
} }
/* keyboard/click focus rings on the DRAWN box — the theme's one ring token, replacing /* keyboard/click focus rings on the DRAWN box — the theme's one ring token, replacing
* base's old generic glow; :focus (not :focus-visible) keeps the pre-absorption behaviour * base's old generic glow; :focus (not :focus-visible) keeps the pre-absorption behaviour
* of ringing a clicked box */ * of ringing a clicked box. Both states (plain and invalid) are declared once, further
input[type="checkbox"]:focus::before, * down this file, in the selector lists every other same-body ring joins — see the two
input[type="radio"]:focus::before { * `--fs-focus-ring`/`--fs-focus-ring-invalid` rules below the text-input group. */
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
}
input[type="checkbox"].cbi-input-invalid:focus::before,
input[type="radio"].cbi-input-invalid:focus::before {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
input[type="checkbox"]::after, input[type="checkbox"]::after,
input[type="radio"]::after { input[type="radio"]::after {
/* the mark is inset from the DRAWING, and the drawing is centred in the target, so the mark /* the mark is inset from the DRAWING, and the drawing is centred in the target, so the mark
@@ -401,7 +393,10 @@
background-image: var(--fs-select-chevron); background-image: var(--fs-select-chevron);
/* right inset from the scale, not a literal 12 — the .cbi-select shell below states the same /* right inset from the scale, not a literal 12 — the .cbi-select shell below states the same
* 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; /* two 6px bands forming the V, the far one a band-width further in; --fs-space-3 stays the
* single spelling of the inset, as the note above says */
background-repeat: no-repeat; background-size: 6px 6px;
background-position: right calc(var(--fs-space-3) - 1px) center, right calc(var(--fs-space-3) + 5px) center;
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: var(--fs-hairline); 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);
@@ -420,7 +415,7 @@
* listbox in dark mode. `height: inherit` absorbed from base with the width above — without * listbox in dark mode. `height: inherit` absorbed from base with the width above — without
* it the fixed box would cap a multi-row listbox at one line. */ * it the fixed box would cap a multi-row listbox at one line. */
select[multiple] { background-color: var(--fs-panel2); color: var(--fs-text); height: inherit; } select[multiple] { background-color: var(--fs-panel2); color: var(--fs-text); height: inherit; }
select:focus { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); outline: none; } /* :focus joins the shared accent ring in the text-input group below. */
/* absorbed from base's reset: 100% (= inherit) keeps the option on its select's size, and /* absorbed from base's reset: 100% (= inherit) keeps the option on its select's size, and
* only <option> needs the 2em leading — the other controls carry line-height: normal above */ * only <option> needs the 2em leading — the other controls carry line-height: normal above */
option { font-size: 100%; line-height: 2em; } option { font-size: 100%; line-height: 2em; }
@@ -491,8 +486,8 @@
background-color: var(--fs-panel2); background-color: var(--fs-panel2);
background-image: var(--fs-select-chevron); background-image: var(--fs-select-chevron);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right var(--fs-space-3) center; background-size: 6px 6px;
background-size: 12px; background-position: right calc(var(--fs-space-3) - 1px) center, right calc(var(--fs-space-3) + 5px) center;
transition: var(--fs-field-transition); 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
@@ -502,15 +497,8 @@
opacity: var(--fs-disabled-opacity); opacity: var(--fs-disabled-opacity);
pointer-events: none; pointer-events: none;
} }
.cbi-select.focus { /* .focus and .cbi-input-invalid.focus join the shared accent/danger ring rules further down
border-color: var(--fs-accent); * this file — same two bodies as every other :focus/:focus-within ring on this token. */
box-shadow: var(--fs-focus-ring);
}
/* the alarm must survive focus: .focus above would repaint an invalid shell accent */
.cbi-select.cbi-input-invalid.focus {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
/* the native <select> inside the shell: invisible, filling it, clearing the ▾ plate on the /* the native <select> inside the shell: invisible, filling it, clearing the ▾ plate on the
* right. The inset is a scale step; the right margin is the plate's width, so it stays * right. The inset is a scale step; the right margin is the plate's width, so it stays
* em-relative to the glyph it clears. Every themed-select declaration above (chevron, * em-relative to the glyph it clears. Every themed-select declaration above (chevron,
@@ -621,26 +609,57 @@
/* bare `input:focus` absorbed from base's generic glow onto the same ring: it is the fallback /* bare `input:focus` absorbed from base's generic glow onto the same ring: it is the fallback
* for the types the list does not name and for a typeless <input>; on checkbox/radio it lands * for the types the list does not name and for a typeless <input>; on checkbox/radio it lands
* on the invisible element box UNDER the drawn ::before ring above — coincident boxes, one * on the invisible element box UNDER the drawn ::before ring above — coincident boxes, one
* visible ring, as the old glow behaved */ * visible ring, as the old glow behaved. `select:focus` and the keyboard-removable dynlist
* chip (ui.js gives it a tabindex) join here rather than repeating the same three
* declarations under their own selector: one shared body instead of three, 149 B off
* cascade.css. */
.cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus, .cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus,
.cbi-section-create-name:focus, .cbi-section-create-name:focus,
input:focus, input:focus,
input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus, input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus,
input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus,
input[type="tel"]:focus, input[type="date"]:focus, input[type="time"]:focus, input[type="tel"]:focus, input[type="date"]:focus, input[type="time"]:focus,
textarea:focus { textarea:focus,
select:focus,
.cbi-dynlist > .item:focus {
border-color: var(--fs-accent); border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring); box-shadow: var(--fs-focus-ring);
outline: none; outline: none;
} }
/* The plain accent ring above, restated for four components whose selector cannot join that
* list without changing what it matches (a pseudo-element, a legacy shell class, two
* `:focus-within` containers) — same two declarations, one shared body instead of four, 186 B
* off cascade.css. `:is()` around the `:has()` compound keeps the list forgiving: unwrapped, a
* `:has()` compound sharing a selector list with plain ones fails the WHOLE rule below the
* floor (Firefox < 121; docs/css.md "Two shapes fail it"), instead of just losing that one
* selector. */
input[type="checkbox"]:focus::before, input[type="radio"]:focus::before,
.cbi-select.focus,
.cbi-dynlist > .add-item:focus-within,
.control-group:is(:has(> .cbi-input-password)):focus-within {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
}
/* Invalid state keeps the danger cue. `.cbi-value-error input` is the Lua-CBI path and belongs /* Invalid state keeps the danger cue. `.cbi-value-error input` is the Lua-CBI path and belongs
* HERE, not in base: base does declare a red border-color for it, but the rule above sets the * HERE, not in base: base does declare a red border-color for it, but the rule above sets the
* `border` SHORTHAND, and a shorthand in a later layer wipes the longhand out regardless of * `border` SHORTHAND, and a shorthand in a later layer wipes the longhand out regardless of
* specificity — so every third-party app on luci-compat rendered the field in --fs-border grey * specificity — so every third-party app on luci-compat rendered the field in --fs-border grey
* and had lost its field-error cue while the modern .cbi-input-invalid path worked. */ * and had lost its field-error cue while the modern .cbi-input-invalid path worked.
*
* `.cbi-input-invalid:focus` is not redundant with the rule it sits beside: the generic
* `input:focus` above is (0,1,1) and would repaint a focused invalid field accent, and
* `.cbi-input-invalid:focus` at (0,2,0) is what stops it — the TYPED `:focus` rules above are
* (0,2,1) and still outrank this, so a typed invalid field focuses accent, exactly as before
* the absorption. The checkbox/radio `::before` ring and the legacy `.cbi-select` shell need
* the identical danger body for the same reason on their own elements — one shared rule
* instead of four, 186 B off cascade.css. */
input[type="checkbox"].cbi-input-invalid:focus::before,
input[type="radio"].cbi-input-invalid:focus::before,
.cbi-select.cbi-input-invalid.focus,
.cbi-input-invalid, input.cbi-input-invalid, textarea.cbi-input-invalid, .cbi-input-invalid, input.cbi-input-invalid, textarea.cbi-input-invalid,
.cbi-value-error input, .cbi-value-error textarea { .cbi-value-error input, .cbi-value-error textarea,
.cbi-input-invalid:focus {
border-color: var(--fs-danger); border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid); box-shadow: var(--fs-focus-ring-invalid);
} }
@@ -649,13 +668,6 @@
* the input, so half of one state lived in a layer the theme cannot reach. */ * the input, so half of one state lived in a layer the theme cannot reach. */
.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button), .cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),
.cbi-input-invalid.cbi-dropdown:not([open]) > ul > li { border-color: var(--fs-danger); } .cbi-input-invalid.cbi-dropdown:not([open]) > ul > li { border-color: var(--fs-danger); }
/* the alarm must survive focus: the generic `input:focus` above is (0,1,1) and would repaint
* a focused invalid field accent; (0,2,0) outranks it. The TYPED :focus rules (0,2,1) still
* outrank this — a typed invalid field focuses accent, exactly as before the absorption. */
.cbi-input-invalid:focus {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
/* readonly keeps its faded border — moved from base with the generic box (its border-color /* readonly keeps its faded border — moved from base with the generic box (its border-color
* longhand would lose to the box's `border` shorthand by layer). The doubled [readonly] is * longhand would lose to the box's `border` shorthand by layer). The doubled [readonly] is
@@ -692,12 +704,8 @@
margin: 0; margin: 0;
transition: var(--fs-field-transition); 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): :focus joins the shared accent
.cbi-dynlist > .item:focus { * ring in the text-input group above. */
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
outline: none;
}
/* The chip's × plate is a BUTTON in base's paint group (`.item::after, .btn, .cbi-button`) — /* The chip's × plate is a BUTTON in base's paint group (`.item::after, .btn, .cbi-button`) —
* absorbed with it, bare as base had it (coverage: an .item outside a dynlist keeps its * absorbed with it, bare as base had it (coverage: an .item outside a dynlist keeps its
* plate). The machinery — content/position/edges/align — stays in base with the widget. */ * plate). The machinery — content/position/edges/align — stays in base with the widget. */
@@ -730,7 +738,7 @@
border-radius: var(--fs-radius-sm); overflow: hidden; border-radius: var(--fs-radius-sm); overflow: hidden;
transition: var(--fs-field-transition); transition: var(--fs-field-transition);
} }
.cbi-dynlist > .add-item:focus-within { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); } /* :focus-within joins the shared accent ring in the text-input group above. */
.cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > input,
/* `width: auto` keeps the flex basis in charge: this input sits INSIDE .cbi-value-field, so the /* `width: auto` keeps the flex basis in charge: this input sits INSIDE .cbi-value-field, so the
* elastic width above would otherwise hand it 100% of the field on top of `flex: 1 1 auto`. */ * elastic width above would otherwise hand it 100% of the field on top of `flex: 1 1 auto`. */
@@ -756,10 +764,8 @@
background: transparent; box-shadow: none; background: transparent; box-shadow: none;
min-height: var(--fs-ctl-h); min-height: var(--fs-ctl-h);
} }
.control-group:has(> .cbi-input-password):focus-within { /* :focus-within (wrapped in :is() for the shared list) joins the accent ring in the
border-color: var(--fs-accent); * text-input group above. */
box-shadow: var(--fs-focus-ring);
}
/* base pulls .cbi-section-create up 3px to hug a table border, which glues the "Add" button /* base pulls .cbi-section-create up 3px to hug a table border, which glues the "Add" button
* to the last row — give it a real gap instead. */ * to the last row — give it a real gap instead. */
.cbi-section-create, .cbi-section-create.cbi-tblsection-create { margin-top: var(--fs-space-3); } .cbi-section-create, .cbi-section-create.cbi-tblsection-create { margin-top: var(--fs-space-3); }
@@ -45,7 +45,7 @@
'tint', 'accent', 'good', 'warn', 'danger', 'tint', 'accent', 'good', 'warn', 'danger',
'card', 'control', 'bar', 'line', 'card', 'control', 'bar', 'line',
'rounding', 'autocollapse', 'tint_strength', 'photo_dim', 'rounding', 'autocollapse', 'tint_strength', 'photo_dim',
'pattern_size', 'pattern_strength', 'pattern_ink', 'pattern_size', 'pattern_strength', 'pattern_ink', 'content_width',
/* the two uploaded images' cache-bust tokens (head.ut whitelists both to hex); the files /* the two uploaded images' cache-bust tokens (head.ut whitelists both to hex); the files
themselves are served. Not axes — they have no browser layer — but they reach the client themselves are served. Not axes — they have no browser layer — but they reach the client
the same way. */ the same way. */
@@ -16,11 +16,15 @@
before the include, so a `?? {}` here would only hide that contract from a second caller. */ before the include, so a `?? {}` here would only hide that contract from a second caller. */
let _sd_lay = index([ 'sidebar', 'top' ], fs_defaults.layout) >= 0 ? fs_defaults.layout : ''; let _sd_lay = index([ 'sidebar', 'top' ], fs_defaults.layout) >= 0 ? fs_defaults.layout : '';
let _sd_dark = index([ 'auto', 'light', 'dark' ], fs_defaults.darkmode) >= 0 ? fs_defaults.darkmode : ''; let _sd_dark = index([ 'auto', 'light', 'dark' ], fs_defaults.darkmode) >= 0 ? fs_defaults.darkmode : '';
let _sd_pal = index([ 'footstrap', 'hicontrast', 'bootstrap', '2020' ], fs_defaults.palette) >= 0 ? fs_defaults.palette : ''; let _sd_pal = index([ 'footstrap', 'hicontrast', 'bootstrap', '2020', 'forum' ], fs_defaults.palette) >= 0 ? fs_defaults.palette : '';
let _sd_wall = index([ 'off', 'pattern', 'file' ], fs_defaults.wallpaper) >= 0 ? fs_defaults.wallpaper : ''; let _sd_wall = index([ 'off', 'pattern', 'file' ], fs_defaults.wallpaper) >= 0 ? fs_defaults.wallpaper : '';
let _sd_pink = index([ 'theme', 'original' ], fs_defaults.pattern_ink) >= 0 ? fs_defaults.pattern_ink : ''; let _sd_pink = index([ 'theme', 'original' ], fs_defaults.pattern_ink) >= 0 ? fs_defaults.pattern_ink : '';
let _sd_ac = index([ 'on', 'off' ], fs_defaults.autocollapse) >= 0 ? fs_defaults.autocollapse : ''; let _sd_ac = index([ 'on', 'off' ], fs_defaults.autocollapse) >= 0 ? fs_defaults.autocollapse : '';
let _sd_dens = index([ 'compact', 'normal', 'large' ], fs_defaults.density) >= 0 ? fs_defaults.density : ''; let _sd_dens = index([ 'compact', 'normal', 'large' ], fs_defaults.density) >= 0 ? fs_defaults.density : '';
/* issue #44: how far the content column may grow past --fs-content-max's 1280px default.
header.ut's FS_AXES carries 'content_width', so a saved router default reaches this. */
let _cw = int(fs_defaults.content_width);
let _sd_cwidth = (fs_defaults.content_width != '' && _cw >= 1280 && _cw <= 3840) ? _cw : -1;
/* The five colour axes each hold one of three things: '' or '0' for off, '1'..'360' for a hue, /* The five colour axes each hold one of three things: '' or '0' for off, '1'..'360' for a hue,
or '#rrggbb'. Both live forms reach the client as a string, and the pre-paint re-reads which or '#rrggbb'. Both live forms reach the client as a string, and the pre-paint re-reads which
it is the way fs-prefs.js's normColor() does, so this passes through exactly those two shapes it is the way fs-prefs.js's normColor() does, so this passes through exactly those two shapes
@@ -147,7 +151,7 @@
current*(), so the Appearance controls show the effective default when this browser has no current*(), so the Appearance controls show the effective default when this browser has no
localStorage. See the sanitiser above for the unset encodings. #} localStorage. See the sanitiser above for the unset encodings. #}
<script>window.__fsPlugins={{ sprintf('%J', fs_plugins) }};</script> <script>window.__fsPlugins={{ sprintf('%J', fs_plugins) }};</script>
<script>window.__fsSD={layout:"{{ _sd_lay }}",darkmode:"{{ _sd_dark }}",palette:"{{ _sd_pal }}",wallpaper:"{{ _sd_wall }}",tint:"{{ _sd_tint }}",accent:"{{ _sd_acc }}",good:"{{ _sd_good }}",warn:"{{ _sd_warn }}",danger:"{{ _sd_dang }}",card:"{{ _sd_card }}",control:"{{ _sd_ctrl }}",bar:"{{ _sd_bar }}",line:"{{ _sd_line }}",rounding:{{ _sd_round }},autocollapse:"{{ _sd_ac }}",login_bg:"{{ _sd_lbg }}",tint_strength:{{ _sd_tstr }},photo_dim:{{ _sd_pdim }},density:"{{ _sd_dens }}",pattern:"{{ _sd_pat }}",pattern_size:{{ _sd_psize }},pattern_strength:{{ _sd_pstr }},pattern_ink:"{{ _sd_pink }}"};</script> <script>window.__fsSD={layout:"{{ _sd_lay }}",darkmode:"{{ _sd_dark }}",palette:"{{ _sd_pal }}",wallpaper:"{{ _sd_wall }}",tint:"{{ _sd_tint }}",accent:"{{ _sd_acc }}",good:"{{ _sd_good }}",warn:"{{ _sd_warn }}",danger:"{{ _sd_dang }}",card:"{{ _sd_card }}",control:"{{ _sd_ctrl }}",bar:"{{ _sd_bar }}",line:"{{ _sd_line }}",rounding:{{ _sd_round }},autocollapse:"{{ _sd_ac }}",login_bg:"{{ _sd_lbg }}",tint_strength:{{ _sd_tstr }},photo_dim:{{ _sd_pdim }},density:"{{ _sd_dens }}",pattern:"{{ _sd_pat }}",pattern_size:{{ _sd_psize }},pattern_strength:{{ _sd_pstr }},pattern_ink:"{{ _sd_pink }}",content_width:{{ _sd_cwidth }}};</script>
<script> <script>
/* Each IIFE below pre-paints one Appearance axis before the first frame, duplicating a /* Each IIFE below pre-paints one Appearance axis before the first frame, duplicating a
live applier in fs-prefs.js. They cannot share code — this runs before the module live applier in fs-prefs.js. They cannot share code — this runs before the module
@@ -218,7 +222,7 @@
is the same one fs-prefs.js validates against — a name in one and not the is the same one fs-prefs.js validates against — a name in one and not the
other paints here and is taken away by the first live change. */ other paints here and is taken away by the first live change. */
if (p === null) p = sd.palette || null; if (p === null) p = sd.palette || null;
if (p === 'hicontrast' || p === 'bootstrap' || p === '2020') root.setAttribute('data-palette', p); if (p === 'hicontrast' || p === 'bootstrap' || p === '2020' || p === 'forum') root.setAttribute('data-palette', p);
else root.removeAttribute('data-palette'); else root.removeAttribute('data-palette');
/* Wallpaper: off | pattern (the uploaded SVG, tiled and recoloured) | file (the /* Wallpaper: off | pattern (the uploaded SVG, tiled and recoloured) | file (the
@@ -227,7 +231,11 @@
sd.pattern is whitelisted to hex above, so nothing user-controlled reaches sd.pattern is whitelisted to hex above, so nothing user-controlled reaches
the url(). */ the url(). */
const pu = sd.pattern; const pu = sd.pattern;
if (pu) root.style.setProperty('--fs-pattern-url', 'url("/luci-static/footstrap/pattern.svg?v=' + pu + '")'); /* /cgi-bin, not a /www symlink: a plain static SVG is a same-origin document
that runs script opened directly (OpenWrt forum thread 251930); this path
is the CGI handler that answers with CSP 'none' + sandbox instead — see
fs-axes.js's PAT_SERVE, which this string must keep matching. */
if (pu) root.style.setProperty('--fs-pattern-url', 'url("/cgi-bin/luci-theme-footstrap-pattern?v=' + pu + '")');
const psRaw = lsGet('fs-pattern-size'); const psRaw = lsGet('fs-pattern-size');
const ps = (psRaw !== null) ? parseInt(psRaw, 10) : sd.pattern_size; const ps = (psRaw !== null) ? parseInt(psRaw, 10) : sd.pattern_size;
if (ps >= 40 && ps <= 1600 && ps !== 440) root.style.setProperty('--fs-pattern-size', ps + 'px'); if (ps >= 40 && ps <= 1600 && ps !== 440) root.style.setProperty('--fs-pattern-size', ps + 'px');
@@ -252,6 +260,17 @@
if (dn === 'compact' || dn === 'large') root.setAttribute('data-density', dn); if (dn === 'compact' || dn === 'large') root.setAttribute('data-density', dn);
else root.removeAttribute('data-density'); else root.removeAttribute('data-density');
/* Content width (issue #44): how far the column may grow past the 1280px
default, as a px length rather than a step name — the slider is continuous.
Written only when it differs from the default, so an untouched theme paints
from the bare :root exactly as it did before this axis existed.
sd.content_width is -1 when unset. */
const cwRaw = lsGet('fs-content-width');
let cw = parseInt(cwRaw, 10);
if (cwRaw === null) cw = (sd.content_width >= 1280 ? sd.content_width : 1280);
if (cw >= 1280 && cw <= 3840 && cw !== 1280)
root.style.setProperty('--fs-content-max', cw + 'px');
/* Rounding 0-20px (02-tokens derives the control and chip radii from it). The /* Rounding 0-20px (02-tokens derives the control and chip radii from it). The
default is a literal, this running before the stylesheet, so axes.mjs is the default is a literal, this running before the stylesheet, so axes.mjs is the
only thing checking the two agree. sd.rounding is -1 when unset. */ only thing checking the two agree. sd.rounding is -1 when unset. */
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow PKG_NAME:=natflow
PKG_VERSION:=20260531 PKG_VERSION:=20260531
PKG_RELEASE:=78 PKG_RELEASE:=79
PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=da0eb608568f2ff10ce80ee3337db87c8e49611b PKG_SOURCE_VERSION:=e059e88e4c1ddc202d122098424d8b9f1d859356
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com> PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0 PKG_LICENSE:=GPL-2.0
+2 -2
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=tcp-brutal PKG_NAME:=tcp-brutal
PKG_VERSION:=1.0.3 PKG_VERSION:=2.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)?
+54 -4
View File
@@ -37,12 +37,36 @@ endif
PKG_NAME:=pgyvpn PKG_NAME:=pgyvpn
PKG_VERSION:=3.1.0 PKG_VERSION:=3.1.0
PKG_RELEASE:=1 PKG_RELEASE:=4
PKG_SOURCE:=pgyvpnsvr PKG_SOURCE:=pgyvpnsvr
PKG_SOURCE_URL:=https://mirrors.oray.com/orayos/packages/$(PKG_NAME)/$(PKG_ARCH_PGYYPN)/$(PKG_VERSION)/bin PKG_SOURCE_URL:=https://mirrors.oray.com/orayos/packages/$(PKG_NAME)/$(PKG_ARCH_PGYYPN)/$(PKG_VERSION)/bin
PKG_HASH:=skip PKG_HASH:=skip
OPENSSL_COMPAT_VERSION:=1.1.1w
OPENSSL_COMPAT_SOURCE:=openssl-$(OPENSSL_COMPAT_VERSION).tar.gz
OPENSSL_COMPAT_HASH:=cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8
OPENSSL_COMPAT_TARGET:=linux-generic32
ifeq ($(ARCH),arm)
OPENSSL_COMPAT_TARGET:=linux-armv4
endif
ifeq ($(ARCH),aarch64)
OPENSSL_COMPAT_TARGET:=linux-aarch64
endif
ifeq ($(ARCH),mips)
OPENSSL_COMPAT_TARGET:=linux-mips32
endif
ifeq ($(ARCH),mipsel)
OPENSSL_COMPAT_TARGET:=linux-mips32
endif
ifeq ($(ARCH),i386)
OPENSSL_COMPAT_TARGET:=linux-x86
endif
ifeq ($(ARCH),x86_64)
OPENSSL_COMPAT_TARGET:=linux-x86_64
endif
PKG_MAINTAINER:=Oray <developer@oray.com> PKG_MAINTAINER:=Oray <developer@oray.com>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
@@ -52,7 +76,7 @@ define Package/pgyvpn
CATEGORY:=Network CATEGORY:=Network
SUBMENU:=VPN SUBMENU:=VPN
DEPENDS:=@(arm||aarch64||mips||mipsel||i386||x86_64) \ DEPENDS:=@(arm||aarch64||mips||mipsel||i386||x86_64) \
+iptables +libc +libopenssl +librt \ +iptables +libc +librt \
+libpthread +libstdcpp +kmod-tun +libpthread +libstdcpp +kmod-tun
TITLE:=PuGongYing VPN, Fast networking TITLE:=PuGongYing VPN, Fast networking
URL:=https://pgy.oray.com/ URL:=https://pgy.oray.com/
@@ -68,16 +92,39 @@ define Download/extra
URL:=$(PKG_SOURCE_URL) URL:=$(PKG_SOURCE_URL)
HASH:=skip HASH:=skip
endef endef
define Download/openssl-compat
FILE:=$(OPENSSL_COMPAT_SOURCE)
URL:=https://www.openssl.org/source/old/1.1.1
HASH:=skip
endef
$(eval $(call Download,extra)) $(eval $(call Download,extra))
$(eval $(call Download,openssl-compat))
define Build/Prepare define Build/Prepare
mkdir -p $(PKG_BUILD_DIR) mkdir -p $(PKG_BUILD_DIR)
$(TAR) -C $(PKG_BUILD_DIR) -xf $(DL_DIR)/$(OPENSSL_COMPAT_SOURCE)
mv $(DL_DIR)/{pgyvpn_oraysl,pgyvpnsvr} $(PKG_BUILD_DIR) mv $(DL_DIR)/{pgyvpn_oraysl,pgyvpnsvr} $(PKG_BUILD_DIR)
chmod +x $(PKG_BUILD_DIR)/{pgyvpn_oraysl,pgyvpnsvr} chmod +x $(PKG_BUILD_DIR)/{pgyvpn_oraysl,pgyvpnsvr}
endef endef
define Build/Configure
( cd $(PKG_BUILD_DIR)/openssl-$(OPENSSL_COMPAT_VERSION) && \
CC="$(TARGET_CC)" \
./Configure $(OPENSSL_COMPAT_TARGET) \
--prefix=/usr \
--libdir=lib \
--cross-compile-prefix="$(TARGET_CROSS)" \
shared no-tests no-engine no-async no-comp no-asm \
$(TARGET_CFLAGS) $(TARGET_CPPFLAGS) $(TARGET_LDFLAGS) \
)
endef
define Build/Compile define Build/Compile
true +$(MAKE) $(PKG_JOBS) -C $(PKG_BUILD_DIR)/openssl-$(OPENSSL_COMPAT_VERSION) \
CC="$(TARGET_CC)" \
SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) \
build_libs
endef endef
define Package/pgyvpn/install define Package/pgyvpn/install
@@ -86,8 +133,11 @@ define Package/pgyvpn/install
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface $(INSTALL_DIR) $(1)/etc/hotplug.d/iface
$(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_DIR) $(1)/usr/share/pgyvpn $(INSTALL_DIR) $(1)/usr/share/pgyvpn
$(INSTALL_DIR) $(1)/usr/lib
$(CP) $(PKG_BUILD_DIR)/pgyvpn_oraysl $(1)/usr/sbin $(CP) $(PKG_BUILD_DIR)/{pgyvpn_oraysl,pgyvpnsvr} $(1)/usr/sbin
$(INSTALL_DATA) $(PKG_BUILD_DIR)/openssl-$(OPENSSL_COMPAT_VERSION)/libssl.so.1.1 $(1)/usr/lib
$(INSTALL_DATA) $(PKG_BUILD_DIR)/openssl-$(OPENSSL_COMPAT_VERSION)/libcrypto.so.1.1 $(1)/usr/lib
$(INSTALL_BIN) ./files/etc/init.d/* $(1)/etc/init.d $(INSTALL_BIN) ./files/etc/init.d/* $(1)/etc/init.d
$(INSTALL_CONF) ./files/etc/config/* $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/* $(1)/etc/config
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/* $(1)/etc/hotplug.d/iface $(INSTALL_BIN) ./files/etc/hotplug.d/iface/* $(1)/etc/hotplug.d/iface