💋 Sync 2026-10-04 04:16:09

This commit is contained in:
github-actions[bot] committed 2026-10-04 04:16:09 +08:00
1 parent f6b2df73ec
commit f62992fd80
745 files changed
+212416 -1173

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
#
# Vantage - a LuCI theme for OpenWrt
# Copyright (C) 2026 anothaDev
#
# This is free software, licensed under the GNU General Public License,
# version 3 or (at your option) any later version.
# Original work; see NOTICE.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage
PKG_VERSION:=1.0.1
PKG_RELEASE:=1
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme
LUCI_DESCRIPTION:=A console-style LuCI theme: icon rail with flyouts, \
breadcrumb, page tabs, Ctrl+K command palette, light and dark modes.
LUCI_DEPENDS:=+luci-base
LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com>
LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage
# The stylesheet uses modern CSS (custom properties, :is(), :has(),
# :where(), color-mix(), inset) that csstidy mangles, so it ships as
# written. JavaScript still goes through LuCI's jsmin when CONFIG_LUCI_JSMIN
# is set (checked: the minified theme scripts parse and keep every regex).
LUCI_MINIFY_CSS:=0
# Language packages (po/<lang>/vantage-theme.po): luci.mk would name them
# after "vantage", like the app's; this keeps them apart
# (luci-i18n-vantage-theme-<lang>, i18n/vantage-theme.<lang>.lmo) and gives
# them the package version instead of one derived from git history.
LUCI_BASENAME:=vantage-theme
PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE)
define Package/luci-theme-vantage/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
# never leave LuCI pointing at a theme that is gone: fall back to
# Bootstrap when it is registered, else to any other registered theme
# (LuCI itself also falls back to the first theme that renders)
if [ "$$(uci -q get luci.main.mediaurlbase)" = "/luci-static/vantage" ]; then
alt="$$(uci -q get luci.themes.Bootstrap)"
[ -n "$$alt" ] || alt="$$(uci -q show luci.themes | sed -n "s|^luci\.themes\.[A-Za-z0-9_]*='\(/luci-static/[A-Za-z0-9_.-]*\)'$$|\1|p" | grep -v '^/luci-static/vantage$$' | head -n 1)"
[ -z "$$alt" ] || uci -q set luci.main.mediaurlbase="$$alt"
fi
uci -q delete luci.themes.Vantage
uci -q commit luci
}
exit 0
endef
# In the LuCI tree (themes/luci-theme-vantage) use ../../luci.mk; built from
# an external feed, use the luci feed installed in the build tree.
include $(TOPDIR)/feeds/luci/luci.mk)
# call BuildPackage - OpenWrt buildroot signature
+32
View File
@@ -0,0 +1,32 @@
Vantage - a LuCI theme for OpenWrt
Copyright (C) 2026 anothaDev
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
SPDX-License-Identifier: GPL-3.0-or-later
Origin
------
This theme is original work. Its stylesheet, templates, scripts, logo and
icons were written for Vantage; no code, markup, styles or artwork were
copied from other LuCI themes (Bootstrap, Material, Argon, OpenWrt 2020,
UniWRT or others). The status icons are drawn by dev/icons/build.js in the
Vantage source tree.
The theme targets LuCI, the OpenWrt web interface (https://github.com/openwrt/luci),
which is licensed under the Apache License 2.0 (compatible with GPL-3.0). It uses LuCI's public
interfaces only - the template scope, luci.js (L, E, ui.menu, rpc), the
luci.themes/luci.main.mediaurlbase configuration and the CSS classes that
LuCI's views emit - and ships no LuCI source. The package is built with
LuCI's luci.mk.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,67 @@
'use strict';
'require baseclass';
/*
* Vantage theme - device naming for the host chip and title.
*
* `system board` model strings are often the SoC vendor's reference-design
* name ("Qualcomm Technologies, Inc. IPQ5332/RDP442/AP-MI01.3") rather than
* the product. In that case (or when the model is missing) the product name
* is derived from board_name ("zyxel,nwa50be" -> "Zyxel NWA50BE"): vendor
* capitalised (or its usual spelling), model upper-cased. Pure functions,
* no DOM, unit tested in tests/host.test.js.
*/
/* SoC vendors, reference boards and placeholders: not a product name.
A bare chip id only counts at the start ("IPQ8074/AP-HK01"); inside a
product name it is part of the model ("GL.iNet GL-MT3000"). */
const REFERENCE = /\b(qualcomm|qca|mediatek|broadcom|realtek|marvell|airoha|econet|ralink|allwinner|rockchip|amlogic|rdp\d*|rfb|reference|ref[\s_-]?board|evb|eval(?:uation)?\s*board|dev(?:elopment)?\s*board|generic|default\s*string|to be filled|unknown)\b|^(ipq|qca|mt|bcm|rtl|en)\d{3,}/i;
/* board_name vendor prefixes whose usual spelling is not simply Capitalised */
const VENDORS = {
'tplink': 'TP-Link', 'tp-link': 'TP-Link', 'dlink': 'D-Link', 'd-link': 'D-Link', 'asus': 'ASUS',
'netgear': 'NETGEAR', 'glinet': 'GL.iNet', 'gl-inet': 'GL.iNet', 'ubnt': 'Ubiquiti', 'ubiquiti': 'Ubiquiti',
'mikrotik': 'MikroTik', 'avm': 'AVM', 'zte': 'ZTE', 'tenda': 'Tenda', 'openwrt': 'OpenWrt', 'bananapi': 'Banana Pi',
'sinovoip': 'Banana Pi', 'raspberrypi': 'Raspberry Pi', 'friendlyarm': 'FriendlyElec', 'friendlyelec': 'FriendlyElec',
'iodata': 'I-O DATA', 'nec': 'NEC', 'wavlink': 'Wavlink', 'cudy': 'Cudy', 'xiaomi': 'Xiaomi', 'linksys': 'Linksys',
'zyxel': 'Zyxel', 'edgecore': 'Edgecore', 'engenius': 'EnGenius', 'aruba': 'Aruba', 'meraki': 'Meraki', 'buffalo': 'Buffalo'
};
function clean(s) {
return (typeof s === 'string') ? s.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim() : '';
}
function looksLikeReference(model) {
const m = clean(model);
return !m || REFERENCE.test(m);
}
function vendorName(v) {
const key = v.toLowerCase();
if (Object.prototype.hasOwnProperty.call(VENDORS, key)) return VENDORS[key];
return key.split(/[-_]+/).filter(Boolean).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}
/* "zyxel,nwa50be" -> "Zyxel NWA50BE"; null when there is no vendor,model pair */
function fromBoardName(boardName) {
const b = clean(boardName);
const i = b.indexOf(',');
if (i <= 0 || i === b.length - 1) return null;
const vendor = b.slice(0, i).trim(), model = b.slice(i + 1).trim();
if (!/^[A-Za-z0-9._-]+$/.test(vendor) || !/^[A-Za-z0-9._+,-]+$/.test(model)) return null;
return vendorName(vendor) + ' ' + model.replace(/,/g, ' ').toUpperCase();
}
/* product name for a `system board` reply, '' when nothing usable */
function productName(board) {
if (!board || typeof board !== 'object') return '';
const model = clean(board.model);
if (model && !looksLikeReference(model)) return model;
return fromBoardName(board.board_name) || clean(board.board_name);
}
return baseclass.extend({
productName,
fromBoardName,
looksLikeReference
});
@@ -0,0 +1,68 @@
/*
* Vantage status icons - GENERATED by dev/icons/build.js, do not edit.
* Replaces luci-base's /luci-static/resources/icons/*.svg in place: LuCI keeps
* its <img> (size, title, alt); the image content comes from this theme.
* ?h= is a content hash so a changed icon is never served from cache.
*/
/* light (default) */
img[src*="/resources/icons/wifi.svg"] { content: url("icons/light/wifi.svg?h=2f84b7d6"); }
img[src*="/resources/icons/wifi_disabled.svg"] { content: url("icons/light/wifi_disabled.svg?h=e62a2fea"); }
img[src*="/resources/icons/ethernet.svg"] { content: url("icons/light/ethernet.svg?h=1694dc92"); }
img[src*="/resources/icons/ethernet_disabled.svg"] { content: url("icons/light/ethernet_disabled.svg?h=56e10306"); }
img[src*="/resources/icons/bridge.svg"] { content: url("icons/light/bridge.svg?h=f75682a4"); }
img[src*="/resources/icons/bridge_disabled.svg"] { content: url("icons/light/bridge_disabled.svg?h=a02590f3"); }
img[src*="/resources/icons/switch.svg"] { content: url("icons/light/switch.svg?h=501f6012"); }
img[src*="/resources/icons/switch_disabled.svg"] { content: url("icons/light/switch_disabled.svg?h=5b30a77e"); }
img[src*="/resources/icons/vlan.svg"] { content: url("icons/light/vlan.svg?h=0f80d5ad"); }
img[src*="/resources/icons/vlan_disabled.svg"] { content: url("icons/light/vlan_disabled.svg?h=c2e85689"); }
img[src*="/resources/icons/vrf.svg"] { content: url("icons/light/vrf.svg?h=cca1a791"); }
img[src*="/resources/icons/vrf_disabled.svg"] { content: url("icons/light/vrf_disabled.svg?h=8be0a928"); }
img[src*="/resources/icons/tunnel.svg"] { content: url("icons/light/tunnel.svg?h=0b457444"); }
img[src*="/resources/icons/tunnel_disabled.svg"] { content: url("icons/light/tunnel_disabled.svg?h=3aad88c8"); }
img[src*="/resources/icons/wireguard.svg"] { content: url("icons/light/wireguard.svg?h=6aea13cf"); }
img[src*="/resources/icons/wireguard_disabled.svg"] { content: url("icons/light/wireguard_disabled.svg?h=073850b0"); }
img[src*="/resources/icons/alias.svg"] { content: url("icons/light/alias.svg?h=388af309"); }
img[src*="/resources/icons/alias_disabled.svg"] { content: url("icons/light/alias_disabled.svg?h=c3b30647"); }
img[src*="/resources/icons/signal-075-100.svg"] { content: url("icons/light/signal-075-100.svg?h=9a9f0e12"); }
img[src*="/resources/icons/signal-050-075.svg"] { content: url("icons/light/signal-050-075.svg?h=9f8ba4f1"); }
img[src*="/resources/icons/signal-025-050.svg"] { content: url("icons/light/signal-025-050.svg?h=24bf1a04"); }
img[src*="/resources/icons/signal-000-025.svg"] { content: url("icons/light/signal-000-025.svg?h=b49c7e88"); }
img[src*="/resources/icons/signal-000-000.svg"] { content: url("icons/light/signal-000-000.svg?h=2aace352"); }
img[src*="/resources/icons/signal-none.svg"] { content: url("icons/light/signal-none.svg?h=afc17393"); }
img[src*="/resources/icons/port_up.svg"] { content: url("icons/light/port_up.svg?h=4eeb8507"); }
img[src*="/resources/icons/port_down.svg"] { content: url("icons/light/port_down.svg?h=7c1dcc4d"); }
img[src*="/resources/icons/port_pse_up.svg"] { content: url("icons/light/port_pse_up.svg?h=c9ef298d"); }
img[src*="/resources/icons/port_pse_down.svg"] { content: url("icons/light/port_pse_down.svg?h=680fe699"); }
img[src*="/resources/icons/loading.svg"] { content: url("icons/light/loading.svg?h=6a5fac64"); }
/* dark */
html[data-darkmode="true"] img[src*="/resources/icons/wifi.svg"] { content: url("icons/dark/wifi.svg?h=32f37eb8"); }
html[data-darkmode="true"] img[src*="/resources/icons/wifi_disabled.svg"] { content: url("icons/dark/wifi_disabled.svg?h=036c48a6"); }
html[data-darkmode="true"] img[src*="/resources/icons/ethernet.svg"] { content: url("icons/dark/ethernet.svg?h=6de09bc1"); }
html[data-darkmode="true"] img[src*="/resources/icons/ethernet_disabled.svg"] { content: url("icons/dark/ethernet_disabled.svg?h=d7ba9f4a"); }
html[data-darkmode="true"] img[src*="/resources/icons/bridge.svg"] { content: url("icons/dark/bridge.svg?h=26c06162"); }
html[data-darkmode="true"] img[src*="/resources/icons/bridge_disabled.svg"] { content: url("icons/dark/bridge_disabled.svg?h=c2e75ebc"); }
html[data-darkmode="true"] img[src*="/resources/icons/switch.svg"] { content: url("icons/dark/switch.svg?h=842b842a"); }
html[data-darkmode="true"] img[src*="/resources/icons/switch_disabled.svg"] { content: url("icons/dark/switch_disabled.svg?h=c944f98d"); }
html[data-darkmode="true"] img[src*="/resources/icons/vlan.svg"] { content: url("icons/dark/vlan.svg?h=760897df"); }
html[data-darkmode="true"] img[src*="/resources/icons/vlan_disabled.svg"] { content: url("icons/dark/vlan_disabled.svg?h=3659cd38"); }
html[data-darkmode="true"] img[src*="/resources/icons/vrf.svg"] { content: url("icons/dark/vrf.svg?h=d6b4fc63"); }
html[data-darkmode="true"] img[src*="/resources/icons/vrf_disabled.svg"] { content: url("icons/dark/vrf_disabled.svg?h=eaca5a7e"); }
html[data-darkmode="true"] img[src*="/resources/icons/tunnel.svg"] { content: url("icons/dark/tunnel.svg?h=f6b093c6"); }
html[data-darkmode="true"] img[src*="/resources/icons/tunnel_disabled.svg"] { content: url("icons/dark/tunnel_disabled.svg?h=af44617f"); }
html[data-darkmode="true"] img[src*="/resources/icons/wireguard.svg"] { content: url("icons/dark/wireguard.svg?h=0d8b6e28"); }
html[data-darkmode="true"] img[src*="/resources/icons/wireguard_disabled.svg"] { content: url("icons/dark/wireguard_disabled.svg?h=3dcfe102"); }
html[data-darkmode="true"] img[src*="/resources/icons/alias.svg"] { content: url("icons/dark/alias.svg?h=efa08bd5"); }
html[data-darkmode="true"] img[src*="/resources/icons/alias_disabled.svg"] { content: url("icons/dark/alias_disabled.svg?h=a7cd9175"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-075-100.svg"] { content: url("icons/dark/signal-075-100.svg?h=8a92ab56"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-050-075.svg"] { content: url("icons/dark/signal-050-075.svg?h=6959aac7"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-025-050.svg"] { content: url("icons/dark/signal-025-050.svg?h=f6c8bda3"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-000-025.svg"] { content: url("icons/dark/signal-000-025.svg?h=1a4d332a"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-000-000.svg"] { content: url("icons/dark/signal-000-000.svg?h=c614ea16"); }
html[data-darkmode="true"] img[src*="/resources/icons/signal-none.svg"] { content: url("icons/dark/signal-none.svg?h=6eb009ad"); }
html[data-darkmode="true"] img[src*="/resources/icons/port_up.svg"] { content: url("icons/dark/port_up.svg?h=f4492ac4"); }
html[data-darkmode="true"] img[src*="/resources/icons/port_down.svg"] { content: url("icons/dark/port_down.svg?h=86d6f109"); }
html[data-darkmode="true"] img[src*="/resources/icons/port_pse_up.svg"] { content: url("icons/dark/port_pse_up.svg?h=30276fcf"); }
html[data-darkmode="true"] img[src*="/resources/icons/port_pse_down.svg"] { content: url("icons/dark/port_pse_down.svg?h=90d4fded"); }
html[data-darkmode="true"] img[src*="/resources/icons/loading.svg"] { content: url("icons/dark/loading.svg?h=7b69fe88"); }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="12" width="12" height="6" rx="3" transform="rotate(-45 9 15)"/><rect x="9" y="6" width="12" height="6" rx="3" transform="rotate(-45 15 9)"/></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><rect x="3" y="12" width="12" height="6" rx="3" transform="rotate(-45 9 15)"/><rect x="9" y="6" width="12" height="6" rx="3" transform="rotate(-45 15 9)"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8h19M6 8v12M18 8v12M6 16.5a6 6 0 0 1 12 0M2.5 20h3.5M18 20h3.5"/></svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M2.5 8h19M6 8v12M18 8v12M6 16.5a6 6 0 0 1 12 0M2.5 20h3.5M18 20h3.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 522 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 675 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><style>.a{transform-box:view-box;transform-origin:50% 50%;animation:r 1.1s linear infinite}@keyframes r{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.a{animation-duration:3.5s}}</style><circle cx="12" cy="12" r="8" stroke="#383b40"/><path class="a" d="M12 4a8 8 0 0 1 8 8" stroke="#a9afb7"/></svg>

After

Width:  |  Height:  |  Size: 504 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M13.3 6.6 9.6 11.7h2.9l-1.2 4.2 3.8-5.2h-2.9z" fill="currentColor" stroke-width="1.1"/></svg>

After

Width:  |  Height:  |  Size: 458 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#2dd4bf" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z" fill="currentColor" fill-opacity=".16" stroke="none"/><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M13.3 6.6 9.6 11.7h2.9l-1.2 4.2 3.8-5.2h-2.9z" fill="currentColor" stroke-width="1.1"/></svg>

After

Width:  |  Height:  |  Size: 667 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#2dd4bf" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z" fill="currentColor" fill-opacity=".16" stroke="none"/><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#383b40" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#383b40" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#383b40" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#383b40" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#ff6b6b" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#383b40" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#383b40" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#383b40" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#f5b84a" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#f5b84a" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#383b40" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#383b40" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#29af9f" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#29af9f" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#29af9f" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#383b40" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#2dd4bf" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#2dd4bf" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#2dd4bf" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#2dd4bf" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#383b40" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#383b40" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#383b40" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#383b40" stroke="none"/><path d="M3.5 4.5l4.5 4.5M8 4.5 3.5 9" stroke="#60646b" stroke-width="1.9"/></svg>

After

Width:  |  Height:  |  Size: 601 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h15.5M16 4.5 19.5 8 16 11.5M20 16H4.5M8 12.5 4.5 16 8 19.5"/></svg>

After

Width:  |  Height:  |  Size: 280 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M4 8h15.5M16 4.5 19.5 8 16 11.5M20 16H4.5M8 12.5 4.5 16 8 19.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 517 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 20v-8a8.5 8.5 0 0 1 17 0v8M8.5 20v-7a3.5 3.5 0 0 1 7 0v7M2 20h20"/></svg>

After

Width:  |  Height:  |  Size: 287 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M3.5 20v-8a8.5 8.5 0 0 1 17 0v8M8.5 20v-7a3.5 3.5 0 0 1 7 0v7M2 20h20"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 524 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 5v6.38a1.5 1.5 0 0 0 .44 1.06l7.62 7.62a1.5 1.5 0 0 0 2.12 0l6.38-6.38a1.5 1.5 0 0 0 0-2.12L12.44 3.94A1.5 1.5 0 0 0 11.38 3.5H5A1.5 1.5 0 0 0 3.5 5z"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M3.5 5v6.38a1.5 1.5 0 0 0 .44 1.06l7.62 7.62a1.5 1.5 0 0 0 2.12 0l6.38-6.38a1.5 1.5 0 0 0 0-2.12L12.44 3.94A1.5 1.5 0 0 0 11.38 3.5H5A1.5 1.5 0 0 0 3.5 5z"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 674 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 20.5 8 12 12.5 3.5 8z"/><path d="m3.5 12 8.5 4.5 8.5-4.5M3.5 16l8.5 4.5 8.5-4.5"/></svg>

After

Width:  |  Height:  |  Size: 305 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M12 3.5 20.5 8 12 12.5 3.5 8z"/><path d="m3.5 12 8.5 4.5 8.5-4.5M3.5 16l8.5 4.5 8.5-4.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 542 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="18.6" r="1.35" fill="currentColor" stroke="none"/><path d="M8.46 15.06a5 5 0 0 1 7.08 0M5.28 11.88a9.5 9.5 0 0 1 13.44 0M2.1 8.7a14 14 0 0 1 19.8 0"/></svg>

After

Width:  |  Height:  |  Size: 376 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><circle cx="12" cy="18.6" r="1.35" fill="currentColor" stroke="none"/><path d="M8.46 15.06a5 5 0 0 1 7.08 0M5.28 11.88a9.5 9.5 0 0 1 13.44 0M2.1 8.7a14 14 0 0 1 19.8 0"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 613 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#a9afb7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 19.5 6v5.5c0 4.6-3.1 8-7.5 9.5-4.4-1.5-7.5-4.9-7.5-9.5V6z"/><path d="M9 16v-3a3 3 0 0 1 6 0v3"/></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#60646b" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M12 3 19.5 6v5.5c0 4.6-3.1 8-7.5 9.5-4.4-1.5-7.5-4.9-7.5-9.5V6z"/><path d="M9 16v-3a3 3 0 0 1 6 0v3"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="12" width="12" height="6" rx="3" transform="rotate(-45 9 15)"/><rect x="9" y="6" width="12" height="6" rx="3" transform="rotate(-45 15 9)"/></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><rect x="3" y="12" width="12" height="6" rx="3" transform="rotate(-45 9 15)"/><rect x="9" y="6" width="12" height="6" rx="3" transform="rotate(-45 15 9)"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8h19M6 8v12M18 8v12M6 16.5a6 6 0 0 1 12 0M2.5 20h3.5M18 20h3.5"/></svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M2.5 8h19M6 8v12M18 8v12M6 16.5a6 6 0 0 1 12 0M2.5 20h3.5M18 20h3.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 522 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 675 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><style>.a{transform-box:view-box;transform-origin:50% 50%;animation:r 1.1s linear infinite}@keyframes r{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.a{animation-duration:3.5s}}</style><circle cx="12" cy="12" r="8" stroke="#d5d7da"/><path class="a" d="M12 4a8 8 0 0 1 8 8" stroke="#4a515a"/></svg>

After

Width:  |  Height:  |  Size: 504 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M13.3 6.6 9.6 11.7h2.9l-1.2 4.2 3.8-5.2h-2.9z" fill="currentColor" stroke-width="1.1"/></svg>

After

Width:  |  Height:  |  Size: 458 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#0b7f6c" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z" fill="currentColor" fill-opacity=".16" stroke="none"/><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M13.3 6.6 9.6 11.7h2.9l-1.2 4.2 3.8-5.2h-2.9z" fill="currentColor" stroke-width="1.1"/></svg>

After

Width:  |  Height:  |  Size: 667 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#0b7f6c" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z" fill="currentColor" fill-opacity=".16" stroke="none"/><path d="M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17h-2.5v2.5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V17H5a1.5 1.5 0 0 1-1.5-1.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8.5 8v2.5M10.83 8v2.5M13.17 8v2.5M15.5 8v2.5" stroke-width="1.5"/></svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#d5d7da" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#c42b2b" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#d5d7da" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#9a5b00" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#9a5b00" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#d5d7da" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#3c9989" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#3c9989" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#3c9989" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#d5d7da" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#0b7f6c" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#0b7f6c" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#0b7f6c" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#0b7f6c" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 525 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="3.5" height="6" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="7.5" y="11" width="3.5" height="10" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="13" y="7" width="3.5" height="14" rx="1.25" fill="#d5d7da" stroke="none"/><rect x="18.5" y="3" width="3.5" height="18" rx="1.25" fill="#d5d7da" stroke="none"/><path d="M3.5 4.5l4.5 4.5M8 4.5 3.5 9" stroke="#9ca0a6" stroke-width="1.9"/></svg>

After

Width:  |  Height:  |  Size: 601 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h15.5M16 4.5 19.5 8 16 11.5M20 16H4.5M8 12.5 4.5 16 8 19.5"/></svg>

After

Width:  |  Height:  |  Size: 280 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M4 8h15.5M16 4.5 19.5 8 16 11.5M20 16H4.5M8 12.5 4.5 16 8 19.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 517 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 20v-8a8.5 8.5 0 0 1 17 0v8M8.5 20v-7a3.5 3.5 0 0 1 7 0v7M2 20h20"/></svg>

After

Width:  |  Height:  |  Size: 287 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M3.5 20v-8a8.5 8.5 0 0 1 17 0v8M8.5 20v-7a3.5 3.5 0 0 1 7 0v7M2 20h20"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 524 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 5v6.38a1.5 1.5 0 0 0 .44 1.06l7.62 7.62a1.5 1.5 0 0 0 2.12 0l6.38-6.38a1.5 1.5 0 0 0 0-2.12L12.44 3.94A1.5 1.5 0 0 0 11.38 3.5H5A1.5 1.5 0 0 0 3.5 5z"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M3.5 5v6.38a1.5 1.5 0 0 0 .44 1.06l7.62 7.62a1.5 1.5 0 0 0 2.12 0l6.38-6.38a1.5 1.5 0 0 0 0-2.12L12.44 3.94A1.5 1.5 0 0 0 11.38 3.5H5A1.5 1.5 0 0 0 3.5 5z"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 674 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 20.5 8 12 12.5 3.5 8z"/><path d="m3.5 12 8.5 4.5 8.5-4.5M3.5 16l8.5 4.5 8.5-4.5"/></svg>

After

Width:  |  Height:  |  Size: 305 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M12 3.5 20.5 8 12 12.5 3.5 8z"/><path d="m3.5 12 8.5 4.5 8.5-4.5M3.5 16l8.5 4.5 8.5-4.5"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 542 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="18.6" r="1.35" fill="currentColor" stroke="none"/><path d="M8.46 15.06a5 5 0 0 1 7.08 0M5.28 11.88a9.5 9.5 0 0 1 13.44 0M2.1 8.7a14 14 0 0 1 19.8 0"/></svg>

After

Width:  |  Height:  |  Size: 376 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><circle cx="12" cy="18.6" r="1.35" fill="currentColor" stroke="none"/><path d="M8.46 15.06a5 5 0 0 1 7.08 0M5.28 11.88a9.5 9.5 0 0 1 13.44 0M2.1 8.7a14 14 0 0 1 19.8 0"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 613 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#4a515a" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 19.5 6v5.5c0 4.6-3.1 8-7.5 9.5-4.4-1.5-7.5-4.9-7.5-9.5V6z"/><path d="M9 16v-3a3 3 0 0 1 6 0v3"/></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" color="#9ca0a6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#fff" stroke="none"/><path d="M4 4 20 20" stroke="#000" stroke-width="5.1"/></mask><g mask="url(#m)"><path d="M12 3 19.5 6v5.5c0 4.6-3.1 8-7.5 9.5-4.4-1.5-7.5-4.9-7.5-9.5V6z"/><path d="M9 16v-3a3 3 0 0 1 6 0v3"/></g><path d="M4 4 20 20"/></svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"><rect x="1" y="1" width="30" height="30" rx="7" fill="#16181b" stroke="#3a3e44"/><path d="M9 10.5 16 22" fill="none" stroke="#e7e9ec" stroke-width="2.6" stroke-linecap="round"/><path d="M16 22 23 10.5" fill="none" stroke="#c8f031" stroke-width="2.6" stroke-linecap="round"/><circle cx="23" cy="10.5" r="1.9" fill="#c8f031"/></svg>

After

Width:  |  Height:  |  Size: 414 B

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,236 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:918
msgid "Actions"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
msgid "Appearance"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:43
msgid "Authorization Required"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
msgid "Auto"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:69
msgid "Breadcrumb"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
msgid "Buffered"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
msgid "Cache"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
msgid "Cached"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:344
msgid "Channel"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
msgid "Collapse navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1093
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81
msgid "Colour theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:934
msgid "Command palette"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
msgid "Dark"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54
msgid "Expand navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
msgid "Follow system theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
msgid "Free"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:48
msgid "Home"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:283
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287
msgid "In use"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48
msgid "Invalid username and/or password! Please try again."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
msgid "Jump to a page or action…"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
msgid "Light"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:23
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:60
msgid "Log in"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
msgid "Log out"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:47
msgid "Main navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:273
msgid "Memory"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1005
msgid "No matching pages"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:90
msgid "Page tabs"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
msgid "Password"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
msgid "Press"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:932
msgid "Results"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74
msgid "Search pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
msgid "Session"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:44
msgid "Skip to content"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:310
msgid "Storage"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:296
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
msgid "Swap"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
msgid "Swap free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
msgid "Total"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
msgid "Total Available"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
msgid "Use dark theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
msgid "Use light theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
msgid "Used"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:53
msgid "Username"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:337
msgid "Wireless"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
msgid "available"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:940
msgid "close"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
msgid "free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1007
msgid "items"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:938
msgid "navigate"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:939
msgid "open"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
msgid "page"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
msgid "pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
msgid "reclaimable"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
msgid "to search all pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
msgid "used"
msgstr ""
@@ -0,0 +1,17 @@
#!/bin/sh
# Register the Vantage theme with LuCI (System -> Language and Style).
# Select it only on a fresh install and only when the entry is new, so an
# upgrade or a reinstall never overrides the theme the administrator chose.
new=0
if [ -z "$(uci -q get luci.themes.Vantage)" ]; then
new=1
fi
uci -q set luci.themes.Vantage=/luci-static/vantage || exit 1
if [ "$PKG_UPGRADE" != 1 ] && [ "$new" = 1 ]; then
uci -q set luci.main.mediaurlbase=/luci-static/vantage
fi
uci -q commit luci
exit 0
@@ -0,0 +1,33 @@
{#
Vantage theme - footer.
Versions are shortened to what a person reads: "OpenWrt 25.12.3 · LuCI 26.124"
(no git describe noise such as "(HEAD detached at …) branch" or "~hash"),
entity-encoded, and only shown to logged-in users: error pages reach
anonymous visitors too. The menu script is only loaded for a session.
(The core header's L.env script still carries the LuCI build for
everyone; see docs/luci-contract.md section 2.)
-#}
{% if (blank_page): %}
</div>
{% else %}
</main>
{%
const dist_ver = match(version.distversion ?? '', /^[0-9A-Za-z][0-9A-Za-z.+-]*/);
const luci_name = match(version.luciname ?? '', /^[A-Za-z][A-Za-z0-9]*/);
const luci_ver = match(version.luciversion ?? '', /[0-9]+\.[0-9]+/);
-%}
<footer class="vt-foot">
{% if (ctx.authsession): %}
<span>{{ entityencode(version.distname ?? 'OpenWrt', true) }}{{ dist_ver ? ' ' + entityencode(dist_ver[0], true) : '' }}</span>
<span class="vt-foot-sep" aria-hidden="true">·</span>
<span>{{ entityencode(luci_name ? luci_name[0] : 'LuCI', true) }}{{ luci_ver ? ' ' + entityencode(luci_ver[0], true) : '' }}</span>
{% endif %}
</footer>
</div>
</div>
{% endif %}
{% if (ctx.authsession && !blank_page): %}
<script>L.require('menu-vantage');</script>
{% endif %}
</body>
</html>
@@ -0,0 +1,92 @@
{#
Vantage theme - header.
Data-free: host details and the menu are fetched by menu-vantage.js over
rpc after login. Everything request-derived is entity-encoded; visitors who
are not logged in (login redirect, 404, CSRF error) get the bare frame with
no menu, no host details and a generic title from this template. LuCI's
core header.ut, which includes this one, then prints its own
L = new LuCI({...}) script (LuCI build, nodespec; on an anonymous 404 the
whole menu tree): upstream, see docs/luci-contract.md section 2.
-#}
{%
const authed = !!ctx.authsession;
const page_title = (authed && dispatched?.title) ? striptags(_(dispatched.title)) : 'LuCI';
const lang = entityencode(dispatcher.lang ?? 'en', true);
-%}
<!DOCTYPE html>
<html lang="{{ lang }}" data-darkmode="true">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<title>{{ entityencode(page_title, true) }} · LuCI</title>
<script>
(function (r) {
var p = 'auto', rail = '';
try { p = localStorage.getItem('vantage.theme') || 'auto'; rail = localStorage.getItem('vantage.rail') || ''; } catch (e) {}
var dark = (p === 'dark') || (p !== 'light' && !!(window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches));
r.setAttribute('data-darkmode', dark ? 'true' : 'false');
r.setAttribute('data-theme', p);
if (rail === 'pinned') r.setAttribute('data-rail', 'pinned');
})(document.documentElement);
</script>
<link rel="stylesheet" href="{{ media }}/vantage.css">
<link rel="stylesheet" href="{{ media }}/icons.css">
<link rel="icon" href="{{ media }}/logo.svg" type="image/svg+xml">
<script src="{{ entityencode(dispatcher.build_url('admin/translations', dispatcher.lang), true) }}"></script>
<script src="{{ resource }}/cbi.js"></script>
</head>
<body class="lang_{{ lang }}{{ authed ? ' vt-auth' : ' vt-anon' }}" data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
{% if (blank_page): %}
<div id="indicators" hidden></div>
<div id="maincontent" class="vt-blank">
{% else %}
<a class="vt-skip" href="#maincontent">{{ entityencode(_('Skip to content'), true) }}</a>
<div class="vt-shell">
{% if (authed): %}
<nav class="vt-rail" id="vt-rail" aria-label="{{ entityencode(_('Main navigation'), true) }}">
<a class="vt-brand" href="{{ dispatcher.build_url('admin') }}" aria-label="{{ entityencode(_('Home'), true) }}">
<svg viewBox="0 0 32 32" width="32" height="32" aria-hidden="true"><rect x="1" y="1" width="30" height="30" rx="7" class="vt-brand-tile"/><path d="M9 10.5 16 22" class="vt-brand-l"/><path d="M16 22 23 10.5" class="vt-brand-r"/><circle cx="23" cy="10.5" r="1.9" class="vt-brand-dot"/></svg>
<span class="vt-brand-word">Vantage</span>
</a>
<ul class="vt-rail-list" id="vt-rail-list"></ul>
<div class="vt-rail-foot">
<button type="button" class="vt-rail-pin" id="vt-rail-pin" aria-pressed="false" aria-label="{{ entityencode(_('Expand navigation'), true) }}" title="{{ entityencode(_('Expand navigation'), true) }}">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M4 5v14M9 12h11M15 7l5 5-5 5"/></svg>
</button>
</div>
</nav>
<div class="vt-flyout" id="vt-flyout" hidden></div>
<div class="vt-scrim" id="vt-scrim" hidden></div>
{% endif %}
<div class="vt-frame">
<header class="vt-topbar">
{% if (!authed): %}
<span class="vt-brand vt-brand-static" aria-hidden="true">
<svg viewBox="0 0 32 32" width="28" height="28"><rect x="1" y="1" width="30" height="30" rx="7" class="vt-brand-tile"/><path d="M9 10.5 16 22" class="vt-brand-l"/><path d="M16 22 23 10.5" class="vt-brand-r"/><circle cx="23" cy="10.5" r="1.9" class="vt-brand-dot"/></svg>
</span>
{% endif %}
<nav class="vt-crumbs" id="vt-crumbs" aria-label="{{ entityencode(_('Breadcrumb'), true) }}"><span class="vt-crumb vt-crumb-current">{{ entityencode(page_title, true) }}</span></nav>
<div class="vt-top-actions">
{% if (authed): %}
<button type="button" class="vt-search" id="vt-search-btn" aria-haspopup="dialog" aria-keyshortcuts="Control+K /">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>
<span class="vt-search-label">{{ entityencode(_('Search pages'), true) }}</span>
<kbd class="vt-kbd">Ctrl K</kbd>
</button>
{% endif %}
<div id="indicators" class="vt-indicators" aria-live="polite"></div>
{% if (authed): %}
<span class="vt-host" id="vt-host" hidden></span>
<button type="button" class="vt-icon-btn" id="vt-theme" aria-label="{{ entityencode(_('Colour theme'), true) }}" title="{{ entityencode(_('Colour theme'), true) }}">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16z" class="vt-fill"/></svg>
</button>
<a class="vt-icon-btn" href="{{ dispatcher.build_url('admin/logout') }}" aria-label="{{ entityencode(_('Log out'), true) }}" title="{{ entityencode(_('Log out'), true) }}">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M14 4h4.5A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5H14"/><path d="M10 8l-4 4 4 4M6 12h10"/></svg>
</a>
{% endif %}
</div>
</header>
<nav class="vt-tabs" id="vt-tabs" aria-label="{{ entityencode(_('Page tabs'), true) }}" hidden></nav>
<main id="maincontent" class="vt-main">
{% endif %}
@@ -0,0 +1,83 @@
{#
Vantage theme - login page.
Rendered with { duser, fuser } for visitors who are not logged in, so it
shows nothing about the device: no hostname, model or firmware version.
fuser is attacker-controlled (formvalue also reads the query string): it is
only tested, never echoed; the error is generic. The form posts back to the
requested URL (no action attribute). The inline script is data-free: the
HTTPS probe reads its port list from an entity-encoded data attribute.
-#}
{%
let tls = uci.get('uhttpd', 'main', 'listen_https') ?? [];
if (type(tls) == 'string')
tls = split(tls, /\s+/);
const tls_ports = uniq(filter(map(tls, e => +(match(e, /\d+$/)?.[0] ?? 0)), p => (p > 0 && p <= 65535)));
-%}
<!DOCTYPE html>
<html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="robots" content="noindex">
<title>{{ entityencode(_('Log in'), true) }} · LuCI</title>
<script>
(function (r) {
var p = 'auto';
try { p = localStorage.getItem('vantage.theme') || 'auto'; } catch (e) {}
var dark = (p === 'dark') || (p !== 'light' && !!(window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches));
r.setAttribute('data-darkmode', dark ? 'true' : 'false');
r.setAttribute('data-theme', p);
})(document.documentElement);
</script>
<link rel="stylesheet" href="{{ media }}/vantage.css">
<link rel="icon" href="{{ media }}/logo.svg" type="image/svg+xml">
</head>
<body class="vt-login-page" data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
<main class="vt-login">
<div class="vt-login-card">
<div class="vt-login-head">
<svg class="vt-login-mark" viewBox="0 0 32 32" width="44" height="44" aria-hidden="true"><rect x="1" y="1" width="30" height="30" rx="7" class="vt-brand-tile"/><path d="M9 10.5 16 22" class="vt-brand-l"/><path d="M16 22 23 10.5" class="vt-brand-r"/><circle cx="23" cy="10.5" r="1.9" class="vt-brand-dot"/></svg>
<div>
<h1 class="vt-login-title">Vantage</h1>
<p class="vt-login-sub"><span class="vt-live-dot" aria-hidden="true"></span>{{ entityencode(_('Authorization Required'), true) }}</p>
</div>
</div>
{% if (fuser): %}
<div class="vt-login-error" role="alert">{{ entityencode(_('Invalid username and/or password! Please try again.'), true) }}</div>
{% endif %}
<form method="post" class="vt-login-form" data-https-ports="{{ entityencode(sprintf('%J', tls_ports), true) }}" data-https-probe="{{ entityencode(resource + '/icons/loading.svg', true) }}">
<label class="vt-field">
<span class="vt-field-label">{{ entityencode(_('Username'), true) }}</span>
<input class="cbi-input-text" type="text" name="luci_username" autocomplete="username" autocapitalize="off" spellcheck="false" required value="{{ entityencode(duser ?? '', true) }}">
</label>
<label class="vt-field">
<span class="vt-field-label">{{ entityencode(_('Password'), true) }}</span>
<input class="cbi-input-password" type="password" name="luci_password" autocomplete="current-password" autofocus>
</label>
<button type="submit" class="btn cbi-button cbi-button-apply vt-login-submit">{{ entityencode(_('Log in'), true) }}</button>
</form>
</div>
</main>
<script>
(function (form) {
var pw = document.querySelector('input[name="luci_password"]');
if (pw) pw.focus();
if (!form || location.protocol === 'https:') return;
var ports = [];
try { ports = JSON.parse(form.getAttribute('data-https-ports') || '[]'); } catch (e) {}
var probePath = form.getAttribute('data-https-probe') || '';
if (!Array.isArray(ports) || probePath.charAt(0) !== '/') return;
ports.forEach(function (port) {
if (typeof port !== 'number' || port < 1 || port > 65535 || port % 1) return;
var probe = new Image(), base = 'https://' + location.hostname + ':' + port;
probe.onload = function () { location = base + location.pathname; };
probe.src = base + probePath + '?' + Math.random();
setTimeout(function () { probe.src = ''; }, 5000);
});
})(document.querySelector('form.vt-login-form'));
</script>
</body>
</html>