v46.1-ui-mt76: Argon theme + hardened WG tunnel + Link Health dashboard

基于 v46 正式版(mt76)只恢复 UI 层:
- luci-theme-argon 2.4.3: local-background-wins 登录页 + bg1.jpg fallback
- luci-app-argon-config: 去 ui.changes.apply, ACL mutator 移 write
- luci-app-wgtunnel: rpcd ucode 后端(get/status/prepare/apply/rollback/reconnect),
  JSONMap 前端, 60s 一次性 token, 快照回滚, 无全局 network ACL
- luci-app-tr3000-status: 只读 rpcd ucode + 5s 轮询仪表盘
- tools/: audit_ui_packages.py + install_preview.sh + rollback_watchdog.sh
- docs/: 开发经历与翻车记录 + 固件哈希记录

固件本体(含烤入 WG 私钥/PSK)不入 git, 仅 K 盘保存.
kernel 成员与 v46 byte-identical; 尚未刷机.
This commit is contained in:
2026-08-19 15:04:02 +08:00
parent c6c90c1db8
commit 29cf67154a
92 changed files with 16036 additions and 0 deletions
@@ -0,0 +1,24 @@
#
# Copyright (C) 2008-2019 Jerrykuku
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Argon Theme
LUCI_DEPENDS:=+wget +jsonfilter
PKG_VERSION:=2.4.3
PKG_RELEASE:=20250722
LUCI_MINIFY_CSS:=0
define Package/luci-theme-argon/conffiles
/etc/config/argon
/www/luci-static/argon/background/
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,2 @@
Drop background here!
accept jpg png gif mp4 webm
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="#adaeaf" d="m8,10.033663l-6.898535,-6.013274l-1.060688,0.972974l7.959223,6.986249l7.959223,-6.986249l-1.060688,-0.972974l-6.898535,6.013274z"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,41 @@
{
"name": "Openwrt",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="#888" d="M8,0c-4.355,0-7.898,3.481-7.998,7.812,0.092-3.779,2.966-6.812,6.498-6.812,3.59,0,6.5,3.134,6.5,7,0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5c0-4.418-3.582-8-8-8zM8,16c4.355,0,7.898-3.481,7.998-7.812-0.092,3.779-2.966,6.812-6.498,6.812-3.59,0-6.5-3.134-6.5-7,0-0.828-0.672-1.5-1.5-1.5s-1.5,0.672-1.5,1.5c0,4.418,3.582,8,8,8z"/>
</svg>

After

Width:  |  Height:  |  Size: 417 B

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256"
style="enable-background:new 0 0 256 256;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:url(#svg_2_00000009581766544743910510000007087157279682564742_);}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#svg_3_00000013155245276689480680000010334395393893521599_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g id="svg_1">
<linearGradient id="svg_2_00000043442590260727270070000016472210641679865270_" gradientUnits="userSpaceOnUse" x1="11.1563" y1="247.3437" x2="245.4437" y2="13.0563" gradientTransform="matrix(1 0 0 -1 0 258)">
<stop offset="0" style="stop-color:#5E72E4"/>
<stop offset="1" style="stop-color:#778AFF"/>
</linearGradient>
<path id="svg_2" style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#svg_2_00000043442590260727270070000016472210641679865270_);" d="
M36.4,0.2h183.8c19.7,0,35.7,16,35.7,35.7v183.8c0,19.7-16,35.7-35.7,35.7H36.4c-19.7,0-35.7-16-35.7-35.7V35.9
C0.7,16.2,16.7,0.2,36.4,0.2z"/>
<linearGradient id="svg_3_00000010280352489557108120000000938545297310085033_" gradientUnits="userSpaceOnUse" x1="0.7" y1="257.8" x2="0.7" y2="257.8" gradientTransform="matrix(1 0 0 -1 0 258)">
<stop offset="0" style="stop-color:#5E72E4"/>
<stop offset="1" style="stop-color:#778AFF"/>
</linearGradient>
<path id="svg_3" style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#svg_3_00000010280352489557108120000000938545297310085033_);" d="
M0.7,0.2"/>
</g>
<path id="svg_4" class="st2" d="M128.3,45.4c-46.7,0-84.4,37.8-84.4,84.4c0,32.2,18.1,60.2,44.6,74.4c6.8,3.7,15.3-0.2,17.2-7.7
l4.3-17.6c1.5-6.2-1-12.6-6.1-16.4c-10-7.4-16.4-19.3-16.4-32.7c0-22.5,18.3-40.7,40.7-40.7c22.5,0,40.7,18.3,40.7,40.7
c0,13.4-6.4,25.2-16.4,32.7c-5.1,3.8-7.6,10.2-6.1,16.5l4.4,17.6c1.9,7.5,10.3,11.4,17.2,7.7c26.6-14.2,44.6-42.2,44.6-74.5
C212.8,83.3,174.9,45.4,128.3,45.4L128.3,45.4z"/>
</g>
<circle class="st2" cx="128.3" cy="131.6" r="18.3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024">
<path fill="#fff" d="M484.430769 51.2 236.307692 354.461538H118.153846c-43.323077 0-78.769231 35.446154-78.769231 78.769231v157.538462c0 43.323077 35.446154 78.769231 78.769231 78.769231h118.153846L484.430769 972.8c25.6 25.6 66.953846 7.876923 66.953846-27.569231V78.769231c0-35.446154-43.323077-53.169231-66.953846-27.569231zm354.461539 120.123077c-7.876923-7.876923-19.692308-7.876923-27.569231 0l-27.569231 27.569231c-7.876923 7.876923-7.876923 21.661538 0 27.56923C858.584615 299.323077 905.846154 399.753846 905.846154 512c0 112.246154-47.261538 212.676923-122.092308 285.538462-7.876923 7.876923-7.876923 19.692308 0 27.56923l27.569231 27.569231c7.876923 7.876923 19.692308 7.876923 27.569231 0C927.507692 768 984.615385 645.907692 984.615385 512s-55.138462-256-145.723077-340.676923zM714.830769 297.353846c-7.876923-7.876923-19.692308-7.876923-27.569231 0l-27.56923 27.569231c-7.876923 7.876923-7.876923 19.692308 0 27.569231C703.015385 391.876923 728.615385 448.984616 728.615385 512c0 63.015385-27.569231 120.123077-70.892308 159.507692-7.876923 7.876923-7.876923 19.692308 0 27.569231l27.569231 27.569231c7.876923 7.876923 19.692308 7.876923 27.56923 0 57.107692-53.169231 94.523077-129.969231 94.523077-216.615385 0-82.707692-35.446154-159.507692-92.553846-212.676923z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024">
<path fill="#fff" d="M484.430769 51.2 236.307692 354.461538H118.153846c-43.323077 0-78.769231 35.446154-78.769231 78.769231v157.538462c0 43.323077 35.446154 78.769231 78.769231 78.769231h118.153846L484.430769 972.8c25.6 25.6 66.953846 7.876923 66.953846-27.569231V78.769231c0-35.446154-43.323077-53.169231-66.953846-27.569231zM882.215385 512l96.492307-96.492308c7.876923-7.876923 7.876923-19.692308 0-27.56923l-27.56923-27.569231c-7.876923-7.876923-19.692308-7.876923-27.569231 0l-96.492308 96.492307-96.492308-96.492307c-7.876923-7.876923-19.692308-7.876923-27.56923 0l-27.569231 27.569231c-7.876923 7.876923-7.876923 19.692308 0 27.56923L771.938462 512l-96.492308 96.492308c-7.876923 7.876923-7.876923 19.692308 0 27.56923l27.569231 27.569231c7.876923 7.876923 19.692308 7.876923 27.56923 0l96.492308-96.492307 96.492308 96.492307c7.876923 7.876923 19.692308 7.876923 27.569231 0l27.56923-27.569231c7.876923-7.876923 7.876923-19.692308 0-27.56923L882.215385 512z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,448 @@
'use strict';
'require baseclass';
'require ui';
/**
* Native JavaScript slide animation utilities
* Replaces jQuery slideUp/slideDown functionality with better performance
*/
const SlideAnimations = {
/**
* Animation durations in milliseconds
*/
durations: {
fast: 200,
normal: 400,
slow: 600
},
/**
* Map to track running animations and their cleanup functions
*/
runningAnimations: new WeakMap(),
/**
* Slide element down (show) with animation
* @param {Element} element - DOM element to animate
* @param {string|number} duration - Animation duration ('fast', 'normal', 'slow' or milliseconds)
* @param {function} callback - Optional callback function when animation completes
*/
slideDown: function(element, duration, callback) {
if (!element) {
console.warn('SlideAnimations.slideDown: No element provided');
return;
}
// Stop any existing animation on this element
this.stop(element);
// Convert duration string to milliseconds
const animDuration = typeof duration === 'string' ?
this.durations[duration] || this.durations.normal :
(duration || this.durations.normal);
// Store original styles
const originalStyles = {
display: element.style.display,
overflow: element.style.overflow,
height: element.style.height,
transition: element.style.transition
};
// Set initial state for animation
element.style.display = 'block';
element.style.overflow = 'hidden';
element.style.height = '0px';
element.style.transition = `height ${animDuration}ms ease-out`;
// Force reflow to ensure initial state is applied
element.offsetHeight;
// Get the target height
const targetHeight = element.scrollHeight;
// Animate to full height
element.style.height = targetHeight + 'px';
// Set up cleanup function
const cleanup = () => {
element.style.height = originalStyles.height || '';
element.style.overflow = originalStyles.overflow || '';
element.style.transition = originalStyles.transition || '';
// Remove from running animations map
this.runningAnimations.delete(element);
if (callback && typeof callback === 'function') {
try {
callback.call(element);
} catch (e) {
console.error('SlideAnimations callback error:', e);
}
}
};
// Store cleanup function for potential cancellation
const timeoutId = setTimeout(cleanup, animDuration);
this.runningAnimations.set(element, { timeoutId, cleanup });
},
/**
* Slide element up (hide) with animation
* @param {Element} element - DOM element to animate
* @param {string|number} duration - Animation duration ('fast', 'normal', 'slow' or milliseconds)
* @param {function} callback - Optional callback function when animation completes
*/
slideUp: function(element, duration, callback) {
if (!element) {
console.warn('SlideAnimations.slideUp: No element provided');
return;
}
// Stop any existing animation on this element
this.stop(element);
// Convert duration string to milliseconds
const animDuration = typeof duration === 'string' ?
this.durations[duration] || this.durations.normal :
(duration || this.durations.normal);
// Store original styles
const originalStyles = {
display: element.style.display,
overflow: element.style.overflow,
height: element.style.height,
transition: element.style.transition
};
// Get current height before hiding
const currentHeight = element.scrollHeight;
// Set initial state for animation
element.style.overflow = 'hidden';
element.style.height = currentHeight + 'px';
element.style.transition = `height ${animDuration}ms ease-out`;
// Force reflow to ensure initial state is applied
element.offsetHeight;
// Animate to zero height
element.style.height = '0px';
// Set up cleanup function
const cleanup = () => {
element.style.display = 'none';
element.style.height = originalStyles.height || '';
element.style.overflow = originalStyles.overflow || '';
element.style.transition = originalStyles.transition || '';
// Remove from running animations map
this.runningAnimations.delete(element);
if (callback && typeof callback === 'function') {
try {
callback.call(element);
} catch (e) {
console.error('SlideAnimations callback error:', e);
}
}
};
// Store cleanup function for potential cancellation
const timeoutId = setTimeout(cleanup, animDuration);
this.runningAnimations.set(element, { timeoutId, cleanup });
},
/**
* Stop all running animations on an element
* @param {Element} element - DOM element to stop animations on
*/
stop: function(element) {
if (!element) return;
const animationData = this.runningAnimations.get(element);
if (animationData) {
// Clear the timeout
clearTimeout(animationData.timeoutId);
// Run cleanup immediately
animationData.cleanup();
}
// Clear transition to immediately stop any CSS animation
element.style.transition = '';
// Force reflow to apply changes immediately
element.offsetHeight;
},
/**
* Check if element has running animation
* @param {Element} element - DOM element to check
* @returns {boolean} - True if element has running animation
*/
isAnimating: function(element) {
return this.runningAnimations.has(element);
}
};
/**
* Argon Theme Menu Module
* Handles rendering and interaction of the main navigation menu and sidebar
*/
return baseclass.extend({
/**
* Initialize the menu module
* Load menu data and trigger rendering
*/
__init__: function () {
ui.menu.load().then(L.bind(this.render, this));
},
/**
* Main render function for the menu system
* @param {Object} tree - Menu tree structure from LuCI
*/
render: function (tree) {
var node = tree,
url = '',
children = ui.menu.getChildren(tree);
// Find and render the active main menu item
for (var i = 0; i < children.length; i++) {
var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0);
if (isActive) {
this.renderMainMenu(children[i], children[i].name);
}
}
// Render tab menu if we're deep enough in the navigation hierarchy
if (L.env.dispatchpath.length >= 3) {
for (var i = 0; i < 3 && node; i++) {
node = node.children[L.env.dispatchpath[i]];
url = url + (url ? '/' : '') + L.env.dispatchpath[i];
}
if (node) {
this.renderTabMenu(node, url);
}
}
// Attach event listeners for sidebar toggle functionality
var sidebarToggle = document.querySelector('a.showSide');
var darkMask = document.querySelector('.darkMask');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle'));
}
if (darkMask) {
darkMask.addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle'));
}
},
/**
* Handle menu expand/collapse functionality
* Manages the sliding animation and active states of menu items
* @param {Event} ev - Click event from menu item
*/
handleMenuExpand: function (ev) {
var target = ev.target;
var slide = target.parentNode;
var slideMenu = target.nextElementSibling;
var shouldCollapse = false;
// Close all currently active submenus
var activeMenus = document.querySelectorAll('.main .main-left .nav > li > ul.active');
activeMenus.forEach(function (ul) {
// Stop any running animations and slide up
SlideAnimations.stop(ul);
// Remove active classes immediately when starting slideUp animation
ul.classList.remove('active');
ul.previousElementSibling.classList.remove('active');
SlideAnimations.slideUp(ul, 'fast');
// Check if we're clicking on an already open menu (should collapse it)
if (!shouldCollapse && ul === slideMenu) {
shouldCollapse = true;
}
});
// Exit if there's no submenu to show
if (!slideMenu) {
return;
}
// Open the submenu if it's not already open
if (!shouldCollapse) {
// Find the slide menu within the slide element
var slideMenuElement = slide.querySelector(".slide-menu");
if (slideMenuElement) {
// Add active classes immediately when starting slideDown animation
slideMenu.classList.add('active');
target.classList.add('active');
SlideAnimations.slideDown(slideMenuElement, 'fast');
}
target.blur(); // Remove focus from the clicked element
}
// Prevent default link behavior and event bubbling
ev.preventDefault();
ev.stopPropagation();
},
/**
* Render the main navigation menu
* Creates hierarchical menu structure with active states and click handlers
* @param {Object} tree - Menu tree node to render
* @param {string} url - Base URL for menu items
* @param {number} level - Current nesting level (0-based)
* @returns {Element} - Generated menu element
*/
renderMainMenu: function (tree, url, level) {
var currentLevel = (level || 0) + 1;
var menuContainer = E('ul', { 'class': level ? 'slide-menu' : 'nav' });
var children = ui.menu.getChildren(tree);
// Don't render empty menus or menus deeper than 2 levels
if (children.length === 0 || currentLevel > 2) {
return E([]);
}
// Generate menu items for each child
for (var i = 0; i < children.length; i++) {
var child = children[i];
var isActive = (
(L.env.dispatchpath[currentLevel] === child.name) &&
(L.env.dispatchpath[currentLevel - 1] === tree.name)
);
// Recursively render submenu
var submenu = this.renderMainMenu(child, url + '/' + child.name, currentLevel);
var hasChildren = submenu.children.length > 0;
// Determine CSS classes based on state
var slideClass = hasChildren ? 'slide' : null;
var menuClass = hasChildren ? 'menu' : 'food';
if (isActive) {
menuContainer.classList.add('active');
slideClass += " active";
menuClass += " active";
}
// Create menu item with link and submenu
var menuItem = E('li', { 'class': slideClass }, [
E('a', {
'href': L.url(url, child.name),
'click': (currentLevel === 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : null,
'class': menuClass,
'data-title': child.title.replace(/ /g, "_"), // More robust space replacement
}, [_(child.title)]),
submenu
]);
menuContainer.appendChild(menuItem);
}
// Append to main menu container if this is the top level
if (currentLevel === 1) {
var mainMenuElement = document.querySelector('#mainmenu');
if (mainMenuElement) {
mainMenuElement.appendChild(menuContainer);
mainMenuElement.style.display = '';
}
}
return menuContainer;
},
/**
* Render tab navigation menu
* Creates horizontal tab menu for deeper navigation levels
* @param {Object} tree - Menu tree node to render
* @param {string} url - Base URL for tab items
* @param {number} level - Current nesting level (0-based)
* @returns {Element} - Generated tab menu element
*/
renderTabMenu: function (tree, url, level) {
var container = document.querySelector('#tabmenu');
var currentLevel = (level || 0) + 1;
var tabContainer = E('ul', { 'class': 'tabs' });
var children = ui.menu.getChildren(tree);
var activeNode = null;
// Don't render empty tab menus
if (children.length === 0) {
return E([]);
}
// Generate tab items for each child
for (var i = 0; i < children.length; i++) {
var child = children[i];
var isActive = (L.env.dispatchpath[currentLevel + 2] === child.name);
var activeClass = isActive ? ' active' : '';
var className = 'tabmenu-item-%s %s'.format(child.name, activeClass);
var tabItem = E('li', { 'class': className }, [
E('a', { 'href': L.url(url, child.name) }, [_(child.title)])
]);
tabContainer.appendChild(tabItem);
// Store reference to active node for recursive rendering
if (isActive) {
activeNode = child;
}
}
// Append tab container to main tab menu element
if (container) {
container.appendChild(tabContainer);
container.style.display = '';
// Recursively render nested tab menus if there's an active node
if (activeNode) {
var nestedTabs = this.renderTabMenu(activeNode, url + '/' + activeNode.name, currentLevel);
if (nestedTabs.children.length > 0) {
container.appendChild(nestedTabs);
}
}
}
return tabContainer;
},
/**
* Handle sidebar toggle functionality
* Toggles the mobile/responsive sidebar menu visibility
* @param {Event} ev - Click event from sidebar toggle button or dark mask
*/
handleSidebarToggle: function (ev) {
var showSideButton = document.querySelector('a.showSide');
var sidebar = document.querySelector('#mainmenu');
var darkMask = document.querySelector('.darkMask');
var scrollbarArea = document.querySelector('.main-right');
// Check if any required elements are missing
if (!showSideButton || !sidebar || !darkMask || !scrollbarArea) {
console.warn('Sidebar toggle elements not found');
return;
}
// Toggle sidebar visibility and related states
if (showSideButton.classList.contains('active')) {
// Close sidebar
showSideButton.classList.remove('active');
sidebar.classList.remove('active');
scrollbarArea.classList.remove('active');
darkMask.classList.remove('active');
} else {
// Open sidebar
showSideButton.classList.add('active');
sidebar.classList.add('active');
scrollbarArea.classList.add('active');
darkMask.classList.add('active');
}
}
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
// out: false
// ==========================================================================
// CSS Custom Properties for Font Management
// ==========================================================================
@font-face {
font-family: 'Google Sans';
src: local('Google Sans'),
local('GoogleSans-Regular'),
url('../fonts/GoogleSans-Regular.woff2') format('woff2'),
url('../fonts/GoogleSans-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Logo Font */
@font-face {
font-family: 'TypoGraphica';
src: local('TypoGraphica'),
url('../fonts/TypoGraphica.woff2') format('woff2'),
url('../fonts/TypoGraphica.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Icon Font */
@font-face {
font-family: 'argon';
src: url('../fonts/argon.woff2') format('woff2'),
url('../fonts/argon.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@@ -0,0 +1,605 @@
// out: false
// ==========================================================================
// Layout Styles
// Main layout components for LuCI admin interface
// ==========================================================================
// 1. Main Container Layout
// ==========================================================================
.main {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: row;
}
// 2. Left Sidebar Layout
// ==========================================================================
.main-left {
width: 15rem;
height: 100%;
flex-shrink: 0;
z-index: 100;
overflow-x: auto;
word-break: break-word;
background-color: var(--menu-bg-color);
box-shadow: rgba(0, 0, 0, 0.75) 0px 0px 15px -5px;
transition: all 0.2s;
// Sidebar Header
.sidenav-header {
padding: 1.5rem 0.5rem;
text-align: center;
.brand {
display: block;
margin: 0 2rem;
font-size: 1.8rem;
font-family: "TypoGraphica";
color: var(--primary);
text-decoration: none;
text-align: center;
cursor: default;
.logo {
max-width: 100%;
height: auto;
}
}
}
// Navigation Menu
.nav {
margin-top: 0.5rem;
// Top-level navigation links
&>li>a:first-child {
display: block;
position: relative;
margin: 0.1rem 0.5rem;
padding: 0.675rem 0 0.675rem 2.5rem;
font-size: 1rem;
text-decoration: none;
border-radius: 0.25rem;
cursor: default;
transition: all 0.2s;
// Active state
&.active {
color: #fff;
background: var(--primary);
&::before {
color: #fff !important;
}
&::after {
transform: rotate(90deg);
color: #fff !important;
}
}
// Hover state
&:hover {
cursor: pointer;
color: #fff;
background: var(--primary);
&::before {
color: #fff !important;
}
}
// Icon before text
&::before {
position: absolute;
left: 0.8rem;
padding-top: 3px;
font-family: 'argon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "\e915";
color: var(--primary);
transition: all 0.3s;
}
}
&>.slide>.menu::before {
transition: transform .1s ease-in-out;
}
&>.slide>.menu.active::before {
transition: transform .2s ease-in-out;
}
// Navigation list items
li {
padding: 0;
cursor: pointer;
user-select: none;
a {
display: block;
color: var(--menu-color);
}
// Collapsible menu items
&.slide {
padding: 0;
ul {
display: none;
overflow: hidden;
}
&:hover {
background: none;
}
// Submenu container
.slide-menu {
margin: 0 0.5rem 0 2.5rem;
padding: 0rem 0.5rem;
&.active {
display: block;
}
li {
position: relative;
margin: 0;
border-radius: 0.25rem;
background: none;
list-style: none;
a {
padding: 0.5rem 0rem;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Hover indicator line
&::after {
position: absolute;
left: 0;
bottom: 0;
width: 0;
height: 2px;
content: "";
background-color: var(--primary);
transition: all 0.2s;
}
&:hover {
background: none;
&::after {
width: 100%;
}
}
}
// Active submenu item
.active {
background: none;
color: var(--menu-color);
a {
color: var(--menu-color);
}
&::after {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
content: "";
background-color: var(--primary);
transition: all 0.2s;
}
&:hover {
background: none;
&::after {
width: 100%;
}
}
}
}
}
// Main menu items
.menu {
display: block;
position: relative;
margin: 0.1rem 0.5rem;
padding: 0.675rem 0 0.675rem 2.5rem;
font-size: 1rem;
text-decoration: none;
border-radius: 0.25rem;
cursor: default;
transition: all 0.2s;
// Active state
&.active {
color: #fff;
background: var(--primary);
&::before {
color: #fff !important;
}
&::after {
transform: rotate(90deg);
color: #fff !important;
}
}
// Hover state
&:hover {
cursor: pointer;
color: #fff;
background: var(--primary);
&::before {
color: #fff !important;
}
}
// Icon before text
&::before {
position: absolute;
left: 0.8rem;
padding-top: 3px;
font-family: 'argon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "\e915";
color: var(--primary);
transition: all 0.3s;
}
// Dropdown arrow
&::after {
position: absolute;
right: 0.5rem;
top: 0.8rem;
font-family: 'argon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: '\e90f';
color: #ced4da;
text-rendering: auto;
transition: all 0.3s;
}
}
// Menu-specific icons and colors
.menu[data-title="Status"]:before {
content: "\e906";
color: var(--primary);
}
.menu[data-title="System"]:before {
content: "\e90a";
color: #fb6340;
}
.menu[data-title="Services"]:before {
content: "\e909";
color: #11cdef;
}
.menu[data-title="NAS"]:before {
content: "\e90c";
color: #f3a4b5;
}
.menu[data-title="VPN"]:before {
content: "\e90b";
color: #8965e0;
}
.menu[data-title="Network"]:before {
content: "\e908";
color: #8965e0;
}
.menu[data-title="Bandwidth_Monitor"]:before {
content: "\e90d";
color: #2dce89;
}
.menu[data-title="Docker"]:before {
content: "\e911";
color: #6699ff;
}
.menu[data-title="Statistics"]:before {
content: "\e913";
color: #8965e0;
}
.menu[data-title="Control"]:before {
content: "\e912";
color: var(--primary);
}
.menu[data-title="Asterisk"]:before {
content: "\e914";
color: #fb6340;
}
// Logout link styling
a[data-title="Log_out"],
.food[data-title="Log_out"] {
&::before {
content: "\e907";
color: #adb5bd;
}
}
}
}
&[style*="overflow: hidden"]>.nav>.slide>.menu::before {
display: none;
}
// Scrollbar Styling
&::-webkit-scrollbar {
width: 5px;
height: 1px;
}
&::-webkit-scrollbar-thumb {
background-color: #f6f9fc;
}
&::-webkit-scrollbar-track {
background-color: #fff;
}
}
// 3. Right Content Area Layout
// ==========================================================================
.main-right {
height: 100%;
flex-grow: 1;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
transition: all 0.2s;
// Main Content Container
&>#maincontent {
position: relative;
z-index: 50;
flex: 1;
display: flex;
flex-direction: column;
&>.container {
margin: 0 1.25rem 1rem 1.25rem;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
// Dashboard Specific Styles
.Dashboard {
color: var(--gray-dark) !important;
h3 {
color: var(--gray-dark);
}
p {
margin-top: 3px;
margin-bottom: 3px;
}
hr {
border-top: 1px solid rgba(0, 0, 0, 1);
}
.dashboard-bg {
background-color: #fff;
}
.settings-info {
padding-top: 1em;
padding-bottom: 1em;
p span:nth-child(2) {
max-height: 18.5px;
top: 4px;
}
.label {
font-size: 0.7rem;
padding: 0.2rem 0.6rem;
}
}
}
}
}
// 4. Header Layout
// ==========================================================================
header {
position: relative;
width: 100%;
padding: 0;
color: var(--header-color);
// Primary background modifier
&.bg-primary {
background-color: var(--primary) !important;
}
// Header extension background
&::after {
content: "";
position: absolute;
width: 100%;
height: 2rem;
background-color: var(--primary) !important;
}
// Header Content Container
.fill {
padding: 0.8rem 0;
display: flex;
border-bottom: 0px solid rgba(255, 255, 255, 0.08) !important;
.container {
width: 100%;
height: 2rem;
padding: 0 1.25rem;
display: flex;
align-items: center;
// Left section (brand and menu toggle)
.flex1 {
flex: 1;
.showSide {
display: none;
font-size: 1.4rem;
color: #fff;
&:hover {
text-decoration: none;
}
}
.brand {
display: none;
padding-left: 1rem;
font-size: 1.5rem;
font-family: "TypoGraphica";
color: #fff;
text-decoration: none;
cursor: default;
vertical-align: text-bottom;
}
}
// Right section (status indicators)
.pull-right {
margin-top: 0rem;
float: right;
display: flex;
}
}
// Status Indicators
.status {
span {
display: inline-block;
margin: 0 0.25rem;
padding: 0.3rem 0.8rem;
font-size: 0.875rem;
font-weight: bold;
white-space: nowrap;
text-decoration: none;
text-transform: uppercase;
text-shadow: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
&:last-child {
margin-right: 0;
}
}
span[data-indicator="poll-status"] {
color: #fff;
}
span[data-style="active"] {
background-color: var(--green);
}
span[data-style="inactive"] {
color: #ffffff !important;
background-color: #32325d;
}
}
}
}
// 5. Footer Layout
// ==========================================================================
footer {
padding: 1rem;
font-size: 0.875rem;
color: #aaa;
text-align: right;
white-space: nowrap;
overflow: hidden;
>a {
color: #aaa;
text-decoration: none;
}
}
// 6. View
// ==========================================================================
#view {
border-radius: 0.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
&>.spinning {
position: fixed;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
padding: 1rem;
border-radius: 0.5rem;
background: #ffffff;
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .15);
}
&>div:first-child {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
}
@@ -0,0 +1,407 @@
// out: false
// ==========================================================================
// Normalize.less
// Based on normalize.css for style reset and standardization
// ==========================================================================
// 1. Global Reset
// ==========================================================================
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
// 2. Document Root Elements
// ==========================================================================
html {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
font-family: var(--font-family-sans-serif);
}
body {
background-color: var(--background-color);
color: var(--gray-dark);
-webkit-tap-highlight-color: transparent;
font-size: 0.875rem;
}
// 3. Semantic Elements
// ==========================================================================
main {
display: block;
}
// 4. Heading Elements
// ==========================================================================
.h1,
.h2,
.h3,
.h4,
.h5,
.h6,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: inherit;
font-weight: normal;
line-height: 1.1 !important;
color: inherit;
}
h1 {
font-size: 2rem;
margin: 0.67em 0;
padding-bottom: 0.5rem;
border-bottom: thin solid var(--lighter);
}
h2 {
padding: 1rem 1.25rem;
font-size: 1.25rem;
font-weight: bold;
color: var(--gray-dark);
border-radius: 0.25rem;
background: var(--white);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
}
h3 {
margin: 0;
padding: 0.8755rem 1.25rem;
font-size: 1.1rem;
font-weight: bold;
line-height: 1;
color: var(--gray-dark);
border-radius: 0.25rem;
background: var(--white);
display: block;
width: 100%;
}
h4 {
margin: 0;
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: bold;
color: var(--gray-dark-400);
em {
padding: 0 0.5rem;
}
}
h5 {
margin: 2rem 0 0 0;
padding-bottom: 0.5rem;
font-size: 1rem;
}
// 5. Text Elements
// ==========================================================================
a {
background-color: transparent;
}
abbr {
cursor: help;
text-decoration: underline;
color: var(--primary);
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: inherit;
font-size: inherit;
padding: 1px 3px;
color: var(--dark);
border-radius: 0.5rem;
background: var(--lighter);
}
small {
font-size: 90%;
line-height: 1.42857143;
white-space: normal;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
// 6. Horizontal Rules and Preformatted Text
// ==========================================================================
hr {
box-sizing: content-box;
height: 0;
margin: 1rem 0;
overflow: visible;
opacity: 0.1;
border-color: var(--lighter);
}
pre {
font-family: monospace, monospace;
font-size: 1em;
overflow: auto;
}
// 7. Media Elements
// ==========================================================================
img {
border-style: none;
}
// 8. Form Elements
// ==========================================================================
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
// Button Styles
[type=button],
[type=reset],
[type=submit],
button {
-webkit-appearance: button;
appearance: button;
}
[type=button]::-moz-focus-inner,
[type=reset]::-moz-focus-inner,
[type=submit]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type=button]:-moz-focusring,
[type=reset]:-moz-focusring,
[type=submit]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
// Input Styles
[type=checkbox],
[type=radio] {
box-sizing: border-box;
padding: 0;
}
[type=number]::-webkit-inner-spin-button,
[type=number]::-webkit-outer-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
appearance: textfield;
outline-offset: -2px;
}
[type=search]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
// checkbox style
input[type="checkbox"] {
appearance: none !important;
-webkit-appearance: none !important;
border: 1px solid var(--primary);
width: 1rem !important;
height: 1rem !important;
padding: 0;
cursor: pointer;
transition: all 0.2s;
}
input[type="checkbox"]:checked {
border: 1px solid var(--primary);
background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 8 8\'%3e%3cpath fill=\'%23fff\' d=\'M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z\'/%3e%3c/svg%3e') !important;
background-color: var(--primary);
background-size: 70%;
background-repeat: no-repeat;
background-position: center;
}
// Select Styles
select {
padding: 0.36rem 0.8rem;
color: var(--gray-dark);
border: thin solid var(--lighter);
background-color: var(--white);
background-image: none;
}
// Textarea Styles
textarea {
padding: 0.25rem;
overflow: auto;
&:focus-visible {
outline: none;
border: 1px solid var(--primary);
}
}
// Fieldset Styles
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
// 9. Interactive Elements
// ==========================================================================
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
progress {
vertical-align: baseline;
}
// 10. List Elements
// ==========================================================================
ul {
line-height: normal;
}
li {
list-style-type: none;
}
// 11. State Styles
// ==========================================================================
[disabled="disabled"] {
pointer-events: none;
}
::selection {
background-color: var(--primary);
color: var(--white);
}
::placeholder {
color: var(--lighter);
}
a:link,
a:visited,
a:active {
color: var(--primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
// 12 tools
// ==========================================================================
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.nowrap:not(.td) {
white-space: nowrap;
}
div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] {
border: 0 !important;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
// out: false
/**
Pure v3.0.0
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE
*/
/**
normalize.css v | MIT License | https://necolas.github.io/normalize.css/
Copyright (c) Nicolas Gallagher and Jonathan Neal
*/
/** normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
.pure-g {
display: flex;
flex-flow: row wrap;
align-content: flex-start
}
.pure-u {
display: inline-block;
vertical-align: top
}
.pure-u-1,
.pure-u-1-1,
.pure-u-1-12,
.pure-u-1-2,
.pure-u-1-24,
.pure-u-1-3,
.pure-u-1-4,
.pure-u-1-5,
.pure-u-1-6,
.pure-u-1-8,
.pure-u-10-24,
.pure-u-11-12,
.pure-u-11-24,
.pure-u-12-24,
.pure-u-13-24,
.pure-u-14-24,
.pure-u-15-24,
.pure-u-16-24,
.pure-u-17-24,
.pure-u-18-24,
.pure-u-19-24,
.pure-u-2-24,
.pure-u-2-3,
.pure-u-2-5,
.pure-u-20-24,
.pure-u-21-24,
.pure-u-22-24,
.pure-u-23-24,
.pure-u-24-24,
.pure-u-3-24,
.pure-u-3-4,
.pure-u-3-5,
.pure-u-3-8,
.pure-u-4-24,
.pure-u-4-5,
.pure-u-5-12,
.pure-u-5-24,
.pure-u-5-5,
.pure-u-5-6,
.pure-u-5-8,
.pure-u-6-24,
.pure-u-7-12,
.pure-u-7-24,
.pure-u-7-8,
.pure-u-8-24,
.pure-u-9-24 {
display: inline-block;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-u-1-24 {
width: 4.1667%
}
.pure-u-1-12,
.pure-u-2-24 {
width: 8.3333%
}
.pure-u-1-8,
.pure-u-3-24 {
width: 12.5%
}
.pure-u-1-6,
.pure-u-4-24 {
width: 16.6667%
}
.pure-u-1-5 {
width: 20%
}
.pure-u-5-24 {
width: 20.8333%
}
.pure-u-1-4,
.pure-u-6-24 {
width: 25%
}
.pure-u-7-24 {
width: 29.1667%
}
.pure-u-1-3,
.pure-u-8-24 {
width: 33.3333%
}
.pure-u-3-8,
.pure-u-9-24 {
width: 37.5%
}
.pure-u-2-5 {
width: 40%
}
.pure-u-10-24,
.pure-u-5-12 {
width: 41.6667%
}
.pure-u-11-24 {
width: 45.8333%
}
.pure-u-1-2,
.pure-u-12-24 {
width: 50%
}
.pure-u-13-24 {
width: 54.1667%
}
.pure-u-14-24,
.pure-u-7-12 {
width: 58.3333%
}
.pure-u-3-5 {
width: 60%
}
.pure-u-15-24,
.pure-u-5-8 {
width: 62.5%
}
.pure-u-16-24,
.pure-u-2-3 {
width: 66.6667%
}
.pure-u-17-24 {
width: 70.8333%
}
.pure-u-18-24,
.pure-u-3-4 {
width: 75%
}
.pure-u-19-24 {
width: 79.1667%
}
.pure-u-4-5 {
width: 80%
}
.pure-u-20-24,
.pure-u-5-6 {
width: 83.3333%
}
.pure-u-21-24,
.pure-u-7-8 {
width: 87.5%
}
.pure-u-11-12,
.pure-u-22-24 {
width: 91.6667%
}
.pure-u-23-24 {
width: 95.8333%
}
.pure-u-1,
.pure-u-1-1,
.pure-u-24-24,
.pure-u-5-5 {
width: 100%
}
.col-1 {
flex: 1 1 30px !important;
}
.col-2 {
flex: 2 2 60px !important;
}
.col-3 {
flex: 3 3 90px !important;
}
.col-4 {
flex: 4 4 120px !important;
}
.col-5 {
flex: 5 5 150px !important;
}
.col-6 {
flex: 6 6 180px !important;
}
.col-7 {
flex: 7 7 210px !important;
}
.col-8 {
flex: 8 8 240px !important;
}
.col-9 {
flex: 9 9 270px !important;
}
.col-10 {
flex: 10 10 300px !important;
}
@@ -0,0 +1,610 @@
// out: false
// ==========================================================================
// Responsive Styles
// Mobile-first responsive design for LuCI admin interface
// ==========================================================================
// 1. Browser-Specific Fixes
// ==========================================================================
// Internet Explorer Specific Fixes
@media all and (-ms-high-contrast: none) {
.main>.main-left>.nav>.slide>.menu::before {
top: 30.25%;
}
.main>.main-left>.nav>li:last-child::before {
top: 20%;
}
.showSide::before {
top: -12px;
}
}
// 2. Large Desktop Screens (1600px and below)
// ==========================================================================
@media screen and (max-width: 1600px) {
// Header adjustments
header>.fill>.container>#logo {
margin: 0 2.5rem 0 0.5rem;
}
// Sidebar width
.main-left {
width: calc(0% + 13rem);
}
// Button and label sizing
.btn:not(button),
.label {
padding: 0.5rem 0.75rem;
}
// Form field sizing
.cbi-value-title {
width: 15rem;
padding-right: 0.6rem;
}
.cbi-value-field .cbi-dropdown,
.cbi-value-field .cbi-input-select,
.cbi-value input[type="text"],
.cbi-value input[type="password"],
.cbi-value textarea {
min-width: 18rem;
}
// Specific component adjustments
#cbi-firewall-zone .cbi-input-select {
min-width: 9rem;
}
.cbi-input-textarea {
font-size: small;
}
.node-admin-status>.main fieldset li>a {
padding: 0.3rem 0.6rem;
}
}
// 3. Standard Desktop Screens (1366px and below)
// ==========================================================================
@media screen and (max-width: 1366px) {
// Header adjustments
header>.fill>.container {
cursor: default;
}
// Sidebar width
.main-left {
width: calc(0% + 13rem);
}
// Tab styling
.tabs>li>a,
.cbi-tabmenu>li>a {
padding: 0.2rem 0.8rem;
}
// Panel and table adjustments
.panel-title {
font-size: 1.1rem;
padding-bottom: 1rem;
}
table {
width: 100% !important;
}
.table .cbi-input-text {
width: 100%;
}
// Form field sizing
.cbi-value-field .cbi-dropdown,
.cbi-value-field .cbi-input-select,
.cbi-value input[type="text"],
.cbi-value input[type="password"] {
min-width: 16rem;
}
#cbi-firewall-zone .cbi-input-select {
min-width: 5.5rem;
}
// Navigation font sizes
.main>.main-left>.nav>li,
.main>.main-left>.nav>li>a,
.main .main-left .nav>li>a:first-child,
.main>.main-left>.nav>.slide>.menu,
.main>.main-left>.nav>li>[data-title="Log_out"] {
font-size: 0.9rem;
}
.main>.main-left>.nav>.slide>.slide-menu>li>a {
font-size: 0.875rem;
}
// Modal adjustments
#modal_overlay {
top: 0rem;
}
// Page-specific table fixes
[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) {
display: block;
}
[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table),
[data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table),
[data-page="admin-network-hosts"] .table,
[data-page="admin-network-routes"] .table {
overflow-y: visible;
}
// Button sizing
.btn:not(button),
.cbi-button {
font-size: 0.875rem;
}
}
// 4. Small Desktop/Large Tablet (1152px and below)
// ==========================================================================
@media screen and (max-width: 1152px) {
// Header adjustments
header>.fill>.container>#logo {
display: none;
}
header>.fill>.container>.brand {
position: relative;
}
// Layout adjustments
html,
.main {
overflow-y: visible;
}
.main>.loading>span {
top: 25%;
}
.main-left {
width: calc(0% + 13rem);
}
// Login page specific
body:not(.logged-in) .showSide {
visibility: hidden;
width: 0;
margin: 0;
}
.node-main-login>.main .cbi-value-title {
text-align: left;
}
// Form field adjustments
.cbi-value-title {
width: 12rem;
padding-right: 1rem;
}
.cbi-value-field .cbi-dropdown,
.cbi-value-field .cbi-input-select,
.cbi-value input[type="text"] {
width: 16rem;
min-width: 16rem;
}
// Password field specific sizing
.cbi-value input[name^="pw"],
.cbi-value input[data-update="change"]:nth-child(2) {
width: 13rem !important;
min-width: 13rem;
}
// Code output adjustments
#diag-rc-output>pre,
#command-rc-output>pre,
[data-page="admin-services-wol"] .notice code {
font-size: 1rem;
}
// Table responsive behavior
.table {
display: block;
}
.Interfaces .table {
overflow-x: hidden;
}
#packages.table {
display: grid;
}
// Table row adjustments
.tr {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.Overview .table[width="100%"]>.tr {
flex-wrap: nowrap;
}
.tr.placeholder {
border-bottom: thin solid #ddd;
}
.tr.placeholder>.td,
#cbi-firewall .tr>.td,
#cbi-network .tr:nth-child(2)>.td,
.cbi-section #wifi_assoclist_table .tr>.td {
border-top: 0;
}
// Table cell adjustments
.th,
.td {
display: inline-block;
align-self: flex-start;
flex: 2 2 10%;
text-overflow: ellipsis;
word-wrap: break-word;
}
.td select,
.td input[type="text"] {
width: 100%;
word-wrap: normal;
}
.td [data-dynlist]>input,
.td input.cbi-input-password {
width: calc(100% - 1.5rem);
}
.td[data-type="button"],
.td[data-type="fvalue"] {
flex: 1 1 12.5%;
text-align: left;
}
.th.cbi-value-field,
.td.cbi-value-field,
.th.cbi-section-table-cell,
.td.cbi-section-table-cell {
flex-basis: auto;
padding-top: 1rem;
}
// Section table adjustments
.cbi-section-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
}
.td.cbi-value-field,
.cbi-section-table-cell {
display: inline-block;
flex: 10 10 auto;
flex-basis: 50%;
text-align: center;
}
.td.cbi-section-actions {
vertical-align: bottom;
}
// Hide table headers on mobile
.tr.table-titles,
.tr.cbi-section-table-titles,
.tr.cbi-section-table-descr {
display: none;
}
.tr[data-title]::before,
.tr.cbi-section-table-titles.named::before {
font-size: 0.9rem;
display: block;
flex: 1 1 100%;
border-bottom: thin solid rgba(0, 0, 0, 0.26);
background: #e9ecef;
}
.td[data-title],
[data-page^="admin-status-realtime"] .td[id] {
text-align: left;
}
.td[data-title]::before {
display: block;
}
// Button spacing
.cbi-button+.cbi-button {
margin-left: 0;
}
.td.cbi-section-actions>*>*,
.td.cbi-section-actions>*>form>* {
margin: 2.1px 3px;
}
// Firewall form adjustments
.Firewall form {
position: static !important;
margin: 0 0 2rem 0;
padding: 2rem;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
input {
width: 100% !important;
margin: 0;
margin-top: 1rem;
}
}
.Firewall .center,
.Firewall .center::before {
text-align: left !important;
}
// Button sizing
.btn:not(button),
.cbi-button {
font-size: 0.875rem;
}
}
// 5. Tablet Portrait (768px and below)
// ==========================================================================
@media screen and (max-width: 768px) {
// Base font size
body {
font-size: 0.875rem;
}
// Progress bar adjustments
.cbi-progressbar::after {
font-size: 0.6rem;
}
// Mobile sidebar behavior
.main-left {
position: fixed;
z-index: 100;
width: 0;
&.active {
width: 13rem;
}
}
.main-right {
width: 100%;
&.active {
overflow-y: hidden;
}
}
.darkMask.active {
display: block;
}
// Mobile menu toggle
.showSide {
padding: 0.1rem;
position: relative;
z-index: 99;
display: inline-block !important;
&::before {
font-family: 'argon' !important;
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "\e20e";
font-size: 1.7rem;
}
}
// Brand visibility
header>.fill>.container>.flex1>.brand {
display: inline-block;
}
// Navigation font size
.main>.main-left>.nav>.slide>.slide-menu>li>a {
font-size: 0.875rem;
}
}
// 6. Mobile Portrait (600px and below)
// ==========================================================================
@media screen and (max-width: 600px) {
// Hide mobile elements
.mobile-hide {
display: none;
}
// Content margins
#maincontent>.container {
margin: 0 1rem 1rem 1rem;
}
// Form adjustments
.cbi-value-title {
text-align: left;
}
.cbi-dynlist p {
padding: 0.5rem 1rem;
}
// Prevent horizontal scroll
body {
overflow-x: hidden;
}
// Login page adjustments
.node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field {
width: 16rem;
}
.node-main-login footer {
display: none;
}
// Tab scrollbar hiding
.tabs,
.cbi-tabmenu {
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
}
// Form field layout
.cbi-value-field,
.cbi-value-description {
display: block !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
[data-page="admin-system-admin-password"] .cbi-value-field {
display: table-cell !important;
}
// Modal adjustments
.modal.cbi-modal {
max-width: 100%;
max-height: none;
}
.modal {
display: flex;
align-items: center;
flex-wrap: wrap;
width: 100%;
min-width: 270px;
max-width: 600px;
min-height: 32px;
margin: 5em auto;
padding: 1em;
border-radius: 3px !important;
background: #fff;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
}
// Dropdown adjustments
.cbi-dropdown[open]>ul.dropdown {
margin-bottom: 1rem;
}
// Login page footer
.login-page .login-container footer {
display: none;
}
}
// 7. Small Mobile (480px and below)
// ==========================================================================
@media screen and (max-width: 480px) {
// Hide mobile elements
.mobile-hide {
display: none;
}
// Interface box adjustments for overview page
div[style*="display:grid;grid-template-columns:repeat"] {
.ifacebox {
flex-basis: 80px;
.ifacebox-body {
padding: 0.875rem 0.5rem;
font-size: 0.6rem;
}
}
}
// Login page mobile adjustments
.login-page .login-container {
margin-left: 0rem !important;
width: 100%;
.login-form {
.form-login {
.input-group {
&::before {
color: #525461;
}
input {
color: #525461;
border-bottom: white 1px solid;
border-bottom: var(--white) 1px solid;
border-radius: 0;
}
}
}
}
}
}
// 8. Desktop Scrollbar Styling (600px and above)
// ==========================================================================
@media screen and (min-width: 600px) {
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar,
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 10px;
&:hover {
background: var(--primary);
}
&:active {
background: var(--primary);
}
}
}
@@ -0,0 +1,257 @@
// out: false
// ==========================================================================
// Login Page Styles
// Optimized with LESS best practices
// ==========================================================================
// Mixins for common patterns
.flex-center() {
display: flex;
align-items: center;
justify-content: center;
}
.absolute-full() {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}
.icon-base() {
font-family: 'argon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.login-page {
display: flex;
height: 100%;
flex-direction: column;
// Background video container
.video {
.absolute-full();
.flex-center();
background-color: var(--darker);
overflow: hidden;
video {
width: 100%;
height: auto;
}
}
// Volume control button
.volume-control {
position: fixed;
top: 1rem;
right: 1rem;
width: 1.5rem;
height: 1.5rem;
z-index: 5000;
cursor: pointer;
background: url(../img/volume_high.svg) no-repeat center/contain;
&.mute {
background-image: url(../img/volume_off.svg);
}
}
// Main background image
.main-bg {
.absolute-full();
background: url(../img/blank.png) no-repeat center/cover;
transition: all 0.5s ease;
}
// Login container
.login-container {
z-index: 10;
margin-left: 5%;
display: flex;
height: 100vh;
width: 26rem;
flex-direction: column;
background-color: var(--white);
backdrop-filter: blur(var(--blur-radius));
background-color: rgba(244, 245, 247, var(--blur-opacity));
box-shadow: rgba(0, 0, 0, 0.75) 0 0 35px -5px;
// Login form
.login-form {
.absolute-full();
display: flex;
flex-direction: column;
align-items: center;
max-width: 26rem;
// Brand/Logo section
.brand {
display: flex;
align-items: center;
justify-content: center;
margin: 50px auto 100px 50px;
color: var(--default);
text-decoration: none;
.icon {
width: 50px;
height: auto;
margin-right: 25px;
}
.brand-text {
margin-right: 45px;
font-size: 1.25rem;
font-weight: 700;
font-family: "TypoGraphica";
word-break: break-word;
}
&:hover {
text-decoration: none;
}
}
// Form container
.form-login {
width: 100%;
padding: 20px 50px;
box-sizing: border-box;
.errorbox {
padding-bottom: 2rem;
text-align: center;
color: var(--warning);
}
// Input group styling
.input-group {
position: relative;
margin-bottom: 1.25rem;
// Icon styling
&::before {
.icon-base();
position: absolute;
top: 10px;
left: 10px;
z-index: 100;
font-size: 1.5rem;
color: var(--default);
}
// Animated border
.border {
position: absolute;
bottom: 0;
width: 100%;
height: 1px;
border-bottom: 1px solid var(--primary);
transform: scaleX(0);
transition: transform 0.3s ease;
}
// Input field
input {
display: block;
width: 100%;
margin: 0.825rem 0;
padding: 0.5rem 0.75rem 0.5rem 3rem;
font-size: 1rem;
line-height: 1.5em;
color: var(--default);
background-color: transparent;
background-clip: padding-box;
border: 0;
border-bottom: 1px solid var(--white);
border-radius: 0;
outline: none;
box-sizing: border-box;
box-shadow: 0 3px 2px rgba(233, 236, 239, 0.05);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
&:focus+.border {
transform: scaleX(1);
}
}
.cbi-input-password {
position: relative;
margin-bottom: 2rem;
}
// Icon content
&.user-icon::before {
content: "\e971";
}
&.pass-icon::before {
content: "\e910";
}
}
}
// Submit button
.cbi-button-apply {
width: 100% !important;
min-height: 50px;
margin: 30px 0 100px;
padding: 10px 0;
font-size: 15px;
font-weight: 600;
color: var(--white);
text-align: center;
letter-spacing: 0.8rem;
background-color: var(--primary) !important;
border: none;
border-radius: 6px;
outline: none;
cursor: pointer;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 50px 0;
transition: all 0.3s ease !important;
&:hover,
&:focus {
opacity: 0.9;
}
}
}
// Footer
footer {
position: absolute;
bottom: 0;
z-index: 10;
display: flex;
justify-content: space-evenly;
width: 100%;
margin-top: auto;
padding: 0 0 30px;
color: var(--gray);
text-align: center;
line-height: 1.6rem;
font-size: 0.75rem;
.ftc {
position: absolute;
bottom: 30px;
width: 100%;
}
.luci-link {
display: block;
}
}
}
}
@@ -0,0 +1,12 @@
#!/bin/sh
if [ "$PKG_UPGRADE" != 1 ]; then
uci get luci.themes.Argon >/dev/null 2>&1 || \
uci batch <<-EOF
set luci.themes.Argon=/luci-static/argon
set luci.main.mediaurlbase=/luci-static/argon
commit luci
EOF
fi
exit 0
@@ -0,0 +1,108 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
. /usr/share/libubox/jshn.sh
src="$(uci -q get argon.@global[0].online_wallpaper || echo bing)"
case "$src" in
bing|none|ghser|unsplash|wallhaven) WEB_PIC_SRC="$src" ;;
unsplash_[0-9]*|wallhaven_[0-9]*) WEB_PIC_SRC="$src" ;;
*) WEB_PIC_SRC="bing" ;;
esac
API_KEY="$(uci -q get argon.@global[0].use_api_key)"
EXACT_RESO="$(uci -q get argon.@global[0].use_exact_resolution || echo '1')"
case "$API_KEY" in *[!A-Za-z0-9_.-]*|????????????????????????????????????????????????????????????????*) API_KEY="" ;; esac
case "$EXACT_RESO" in 1) ;; *) EXACT_RESO=0 ;; esac
cache_key="$(echo "$WEB_PIC_SRC" | tr -c 'A-Za-z0-9_-' '_')"
CACHE="/var/run/argon_${cache_key}.url"
WRLOCK="/var/lock/argon_${cache_key}.lock"
valid_url() {
case "$1" in
https://www.bing.com/*|//www.bing.com/*|https://api.vvhan.com/*|https://images.unsplash.com/*|https://wallhaven.cc/*) return 0 ;;
esac
return 1
}
fetch_pic_url() {
case "$WEB_PIC_SRC" in
bing)
local picpath="$(wget -T3 -qO- "https://www.bing.com/HPImageArchive.aspx?format=js&n=1" | jsonfilter -qe '@.images[0].url' | sed 's/1920x1080/UHD/g')"
[ -n "$picpath" ] && echo "//www.bing.com${picpath}"
;;
ghser)
echo "https://api.vvhan.com/api/wallpaper/acg"
;;
unsplash)
if [ -z "$API_KEY" ]; then
local pic_id="$(wget -T3 --spider "https://source.unsplash.com/1920x1080/daily?wallpapers" 2>&1 | grep -Eo "photo-\w+-\w+" | head -n1)"
[ -n "$pic_id" ] && echo "https://images.unsplash.com/${pic_id}?fm=jpg&fit=crop&w=1920&h=1080"
else
wget -T3 -qO- "https://api.unsplash.com/photos/random?client_id=${API_KEY}" | jsonfilter -qe '@["urls"]["regular"]'
fi
;;
unsplash_*)
local collection_id="${WEB_PIC_SRC#unsplash_}"
if [ -z "$API_KEY" ]; then
local pic_id="$(wget -T3 --spider "https://source.unsplash.com/collection/${collection_id}/1920x1080" 2>&1 | grep -Eo "photo-\w+-\w+" | head -n1)"
[ -n "$pic_id" ] && echo "https://images.unsplash.com/${pic_id}?fm=jpg&fit=crop&w=1920&h=1080"
else
wget -T3 -qO- "https://api.unsplash.com/photos/random?client_id=${API_KEY}&collections=${collection_id}" | jsonfilter -qe '@["urls"]["regular"]'
fi
;;
wallhaven)
wget -T3 -qO- "https://wallhaven.cc/api/v1/search?resolutions=1920x1080&sorting=random" | jsonfilter -qe '@.data[0].path'
;;
wallhaven_*)
local tag_id="${WEB_PIC_SRC#wallhaven_}"
local use_reso="resolutions"
[ "$EXACT_RESO" -eq 1 ] || use_reso="atleast"
[ -z "$API_KEY" ] || API_KEY="apikey=$API_KEY&"
wget -T3 -qO- "https://wallhaven.cc/api/v1/search?${API_KEY}q=id%3A${tag_id}&${use_reso}=1920x1080&sorting=random" | jsonfilter -qe '@.data[0].path'
;;
esac
}
try_update() {
exec 200>"$WRLOCK"
if flock -n 200 >/dev/null 2>&1; then
local picurl="$(fetch_pic_url)"
if valid_url "$picurl"; then
printf '%s\n' "$picurl" > "$CACHE"
printf '%s\n' "$picurl"
elif [ -s "$CACHE" ]; then
cat "$CACHE"
else
: > "$CACHE"
fi
flock -u 200 >/dev/null 2>&1
elif [ -s "$CACHE" ]; then
cat "$CACHE"
fi
}
case "$1" in
"list")
json_init; json_add_object "get_url"; json_close_object; json_dump; json_cleanup
;;
"call")
case "$2" in
"get_url")
read -r input
if [ -f "$CACHE" ]; then
idle_t="$(($(date '+%s') - $(date -r "$CACHE" '+%s' 2>/dev/null || echo '0')))"
if [ -s "$CACHE" ] && [ "$idle_t" -le 43200 ]; then
url="$(cat "$CACHE")"
valid_url "$url" || url=""
json_init; json_add_string "url" "$url"; json_dump; json_cleanup; return 0
elif [ ! -s "$CACHE" ] && [ "$idle_t" -le 120 ]; then
echo '{ "url": "" }'; return 1
fi
fi
json_init; json_add_string "url" "$(try_update)"; json_dump; json_cleanup; return 0
;;
esac
;;
esac
@@ -0,0 +1,8 @@
{
"luci-theme-argon": {
"description": "Grant UCI access for luci-theme-argon",
"read": {
"uci": [ "argon" ]
}
}
}
@@ -0,0 +1,55 @@
{#
Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template
luci-theme-argon
Copyright 2020 Jerrykuku <jerrykuku@qq.com>
Have a bug? Please create an issue here on GitHub!
https://github.com/jerrykuku/luci-theme-argon/issues
luci-theme-material:
Copyright 2015 Lutty Yang <lutty@wcan.in>
Agron Theme
https://demos.creative-tim.com/argon-dashboard/index.html
Licensed to the public under the Apache License 2.0
-#}
</div>
<footer class="mobile-hide" style="text-wrap: auto">
<div class="footer-content" style="display: flex; flex-wrap: wrap; gap: 0.5em; justify-content: end;">
<a class="luci-link" href="https://github.com/openwrt/luci" target="_blank">Powered by {{ version.luciname }} ({{ version.luciversion }})</a>
<span class="footer-separator">|</span>
<a href="https://github.com/jerrykuku/luci-theme-argon" target="_blank">ArgonTheme {# vPKG_VERSION #}</a>
<span class="footer-separator">|</span>
<a class="luci-link" href="{{ version.disturl }}" target="_blank">{{ version.distname }} {{ version.distversion }}-{{ version.distrevision }}</a>
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
</div>
</footer>
</div>
</div>
<script>
// thanks for Jo-Philipp Wich <jow@openwrt.org>
var luciLocation = {{ ctx.path }};
var winHeight = window.innerHeight;
window.addEventListener('resize', function () {
var winWidth = window.innerWidth;
if(winWidth < 600){
var newHeight = window.innerHeight;
var keyboradHeight = newHeight - winHeight;
var ftcElement = document.querySelector(".ftc");
if (ftcElement) {
ftcElement.style.bottom = (keyboradHeight + 30) + "px";
}
}
});
</script>
<script type="text/javascript">L.require('menu-argon')</script>
</body>
</html>
@@ -0,0 +1,47 @@
{#
Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template
luci-theme-argon
Copyright 2020 Jerrykuku <jerrykuku@qq.com>
Have a bug? Please create an issue here on GitHub!
https://github.com/jerrykuku/luci-theme-argon/issues
luci-theme-material:
Copyright 2015 Lutty Yang <lutty@wcan.in>
Agron Theme
https://demos.creative-tim.com/argon-dashboard/index.html
Licensed to the public under the Apache License 2.0
-#}
</div>
<!-- added style="text-wrap: auto" but on this page it is still not the best solution. I will take it to consideration next time :) -->
<footer style="text-wrap: auto">
<div>
<a class="luci-link" href="https://github.com/openwrt/luci" target="_blank">Powered by {{ version.luciname }} ({{ version.luciversion }})</a>
<a href="https://github.com/jerrykuku/luci-theme-argon" target="_blank">ArgonTheme {# vPKG_VERSION #}</a>
<a class="luci-link" href="{{ version.disturl }}" target="_blank">{{ version.distname }} {{ version.distversion }}-{{ version.distrevision }}</a>
</div>
</footer>
</div>
</div>
<script>
// thanks for Jo-Philipp Wich <jow@openwrt.org>
var luciLocation = {{ ctx.path }};
var winHeight = window.innerHeight;
window.addEventListener('resize', function () {
var winWidth = window.innerWidth;
if(winWidth < 600){
var newHeight = window.innerHeight;
var keyboradHeight = newHeight - winHeight;
var ftcElement = document.querySelector(".ftc");
if (ftcElement) {
ftcElement.style.bottom = (keyboradHeight + 30) + "px";
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,181 @@
{#
Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template
luci-theme-argon
Copyright 2020 Jerrykuku <jerrykuku@qq.com>
Have a bug? Please create an issue here on GitHub!
https://github.com/jerrykuku/luci-theme-argon/issues
luci-theme-material:
Copyright 2015 Lutty Yang <lutty@wcan.in>
Argon Theme
https://demos.creative-tim.com/argon-dashboard/index.html
Licensed to the public under the Apache License 2.0
-#}
{%
import { readfile, access } from 'fs';
import { cursor } from 'uci';
import { srand } from 'math';
import { getuid, getspnam } from 'luci.core';
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
let cfg = cursor();
//send as HTML5
http.prepare_content('text/html; charset=UTF-8');
srand(+substr(reverse(time() + ""), 0, 8));
//Custom settings
let mode = 'normal';
let bar_color = '#5e72e4';
let primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity, blur_opacity_dark;
const dark_css = readfile('/www/luci-static/argon/css/dark.css');
if (access('/etc/config/argon')) {
primary = cfg.get_first('argon', 'global', 'primary');
dark_primary = cfg.get_first('argon', 'global', 'dark_primary');
blur_radius = cfg.get_first('argon', 'global', 'blur');
blur_radius_dark = cfg.get_first('argon', 'global', 'blur_dark');
blur_opacity = cfg.get_first('argon', 'global', 'transparency');
blur_opacity_dark = cfg.get_first('argon', 'global', 'transparency_dark');
mode = cfg.get_first('argon', 'global', 'mode');
bar_color = (mode == 'dark') ? dark_primary : primary;
}
-%}
<!DOCTYPE html>
<html lang="{{ dispatcher.lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI</title>
<!-- SEO and Description -->
<meta name="robots" content="noindex, nofollow">
<!-- Security -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
<!-- Mobile and PWA -->
<meta name="format-detection" content="telephone=no, email=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{{ hostname }} - LuCI">
<!-- Theme and Colors -->
<meta name="theme-color" content="{{ bar_color }}">
<meta name="msapplication-TileColor" content="{{ bar_color }}">
<meta name="msapplication-TileImage" content="{{ media }}/icon/ms-icon-144x144.png">
<meta name="application-name" content="{{ hostname }} - LuCI">
<!-- Icons and Manifest -->
<link rel="icon" type="image/x-icon" href="{{ media }}/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="{{ media }}/icon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ media }}/icon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="{{ media }}/icon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="{{ media }}/icon/android-icon-192x192.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ media }}/icon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ media }}/icon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ media }}/icon/apple-icon-144x144.png">
<link rel="manifest" href="{{ media }}/icon/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ media }}/css/cascade.css{# ?v=PKG_VERSION #}">
<style title="text/css">
{% if (mode == 'normal'): %}
@media (prefers-color-scheme: dark) {
{{ dark_css }}
}
{% endif %}
{% if (mode == 'dark'): %}
{{ dark_css }}
{% endif -%}
{% if (access('/etc/config/argon')): %}
:root {
--primary: {{ primary }};
--dark-primary: {{ dark_primary }};
--blur-radius:{{ blur_radius }}px;
--blur-opacity:{{ blur_opacity }};
--blur-radius-dark:{{ blur_radius_dark }}px;
--blur-opacity-dark:{{ blur_opacity_dark }};
}
{% endif -%}
</style>
<!-- Stylesheets -->
{% if (node?.css): %}
<link rel="stylesheet" href="{{ resource }}/{{ node.css}}">
{% endif -%}
{% if (css): %}
<style title="text/css">
{{ css }}
</style>
{% endif -%}
<script src="{{ dispatcher.build_url('admin/translations', dispatcher.lang) }}?v={{ version.luciversion }}"></script>
<script src="{{ resource }}/cbi.js?v={{ version.luciversion }}"></script>
<script src="{{ resource }}/luci.js?v={{ version.luciversion }}"></script>
</head>
<body
class="lang_{{ dispatcher.lang }} {{ node?.title ? ` - ${striptags(node.title)}` : '' }} {% if (ctx.authsession): %}logged-in{% endif %}"
data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
<div class="main">
<div class="main-left" id="mainmenu" style="display:none">
<div class="sidenav-header d-flex align-items-center">
<a class="brand" href="#">{{ hostname }}</a>
<div class="ml-auto">
<!-- Sidenav toggler -->
<div class="sidenav-toggler d-none d-xl-block active" data-action="sidenav-unpin"
data-target="#sidenav-main">
<div class="sidenav-toggler-inner">
<i class="sidenav-toggler-line"></i>
<i class="sidenav-toggler-line"></i>
<i class="sidenav-toggler-line"></i>
</div>
</div>
</div>
</div>
</div>
<div class="main-right">
<header class="bg-primary">
<div class="fill">
<div class="container">
<div class="flex1">
<a class="showSide"></a>
<a class="brand" href="#">{{ hostname }}</a>
</div>
<div class="status" id="indicators"></div>
</div>
</div>
</header>
<div class="darkMask"></div>
<div id="maincontent">
<div class="container">
{% if (getuid() == 0 && getspnam('root')?.pwdp === ''): %}
<div class="alert-message error">
<h4>{{ _('No password set!') }}</h4>
<p>{{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}</p>
{% if (dispatcher.lookup("admin/system/admin")): %}
<div class="right"><a class="btn" href="{{ dispatcher.build_url("admin/system/admin") }}">{{ _('Go to password configuration...') }}</a></div>
{% endif %}
</div>
{% endif %}
<noscript>
<div class="alert-message error">
<h4>{{ _('JavaScript required!') }}</h4>
<p>{{ _('You must enable JavaScript in your browser or LuCI will not work properly.') }}</p>
</div>
</noscript>
<div id="tabmenu" style="display:none"></div>
@@ -0,0 +1,125 @@
{#
Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material Argon Template
luci-theme-argon
Copyright 2020 Jerrykuku <jerrykuku@qq.com>
Have a bug? Please create an issue here on GitHub!
https://github.com/jerrykuku/luci-theme-argon/issues
luci-theme-material:
Copyright 2015 Lutty Yang <lutty@wcan.in>
Argon Theme
https://demos.creative-tim.com/argon-dashboard/index.html
Licensed to the public under the Apache License 2.0
-#}
{%
import { readfile, access } from 'fs';
import { cursor } from 'uci';
import { srand } from 'math';
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
let cfg = cursor();
//send as HTML5
http.prepare_content('text/html; charset=UTF-8');
srand(+substr(reverse(time() + ""), 0, 8));
//Custom settings
let mode = 'normal';
let bar_color = '#5e72e4';
let primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity, blur_opacity_dark;
const dark_css = readfile('/www/luci-static/argon/css/dark.css');
if (access('/etc/config/argon')) {
primary = cfg.get_first('argon', 'global', 'primary');
dark_primary = cfg.get_first('argon', 'global', 'dark_primary');
blur_radius = cfg.get_first('argon', 'global', 'blur');
blur_radius_dark = cfg.get_first('argon', 'global', 'blur_dark');
blur_opacity = cfg.get_first('argon', 'global', 'transparency');
blur_opacity_dark = cfg.get_first('argon', 'global', 'transparency_dark');
mode = cfg.get_first('argon', 'global', 'mode');
bar_color = (mode == 'dark') ? dark_primary : primary;
}
-%}
<!DOCTYPE html>
<html lang="{{ dispatcher.lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI</title>
<!-- SEO and Description -->
<meta name="robots" content="noindex, nofollow">
<!-- Security -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
<!-- Mobile and PWA -->
<meta name="format-detection" content="telephone=no, email=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{{ hostname }} - LuCI">
<!-- Theme and Colors -->
<meta name="theme-color" content="{{ bar_color }}">
<meta name="msapplication-TileColor" content="{{ bar_color }}">
<meta name="msapplication-TileImage" content="{{ media }}/icon/ms-icon-144x144.png">
<meta name="application-name" content="{{ hostname }} - LuCI">
<!-- Icons and Manifest -->
<link rel="icon" type="image/x-icon" href="{{ media }}/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="{{ media }}/icon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ media }}/icon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="{{ media }}/icon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="{{ media }}/icon/android-icon-192x192.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ media }}/icon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ media }}/icon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ media }}/icon/apple-icon-144x144.png">
<link rel="manifest" href="{{ media }}/icon/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ media }}/css/cascade.css{# ?v=PKG_VERSION #}">
<style title="text/css">
{% if (mode == 'normal'): %}
@media (prefers-color-scheme: dark) {
{{ dark_css }}
}
{% endif %}
{% if (mode == 'dark'): %}
{{ dark_css }}
{% endif -%}
{% if (access('/etc/config/argon')): %}
:root {
--primary: {{ primary }};
--dark-primary: {{ dark_primary }};
--blur-radius:{{ blur_radius }}px;
--blur-opacity:{{ blur_opacity }};
--blur-radius-dark:{{ blur_radius_dark }}px;
--blur-opacity-dark:{{ blur_opacity_dark }};
}
{% endif -%}
</style>
<link rel="shortcut icon" href="{{ media }}/favicon.ico">
{% if (node?.css): %}
<link rel="stylesheet" href="{{ resource }}/{{ node.css}}">
{% endif -%}
{% if (css): %}
<style title="text/css">
{{ css }}
</style>
{% endif -%}
</head>
<body>
@@ -0,0 +1,12 @@
{#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2019 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-#}
{%
if (!ctx.template_header_sent) {
include("themes/" + theme + "/header_login");
ctx.template_header_sent = true;
}
%}
@@ -0,0 +1,165 @@
{#
Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI and Argon Template
luci-theme-argon
Copyright 2020 Jerryk <jerrykuku@gmail.com>
Have a bug? Please create an issue here on GitHub!
https://github.com/jerrykuku/luci-theme-argon/issues
luci-theme-bootstrap:
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2016 Jo-Philipp Wich <jow@openwrt.org>
Copyright 2012 David Menting <david@nut-bolt.nl>
MUI:
https://github.com/muicss/mui
Argon Theme
https://demos.creative-tim.com/argon-dashboard/index.html
Licensed to the public under the Apache License 2.0
-#}
{% include("themes/" + theme + "/out_header_login") %}
{%
import * as fs from 'fs';
import { cursor } from 'uci';
import { rand } from 'math';
let cfg = cursor();
//Fetch Local Background Media
const imageTypes = " jpg jpeg png gif webp ";
const videoTypes = " mp4 webm ";
const allTypes = imageTypes + videoTypes;
function fetchMedia(path, themeDir) {
let backgroundTable = [];
for (f in (fs.lsdir(path))) {
let ext = lc(split(f, '.')?.[1]);
if (ext && index(allTypes, " " + ext + " ") != -1) {
let bg = {};
bg.type = ext;
bg.url = themeDir + f;
push(backgroundTable, bg);
}
}
return backgroundTable;
}
function selectBackground(themeDir) {
let bgUrl = media + "/img/bg1.jpg";
let remoteBgUrl = "";
let backgroundType = "Image";
let mimeType = "";
if (fs.access("/etc/config/argon")) {
let online_wallpaper = cfg.get_first('argon', 'global', 'online_wallpaper') ?? (cfg.get_first('argon', 'global', 'bing_background') == '1' ? 'bing' : null);
if (online_wallpaper && online_wallpaper != "none") {
const picurl = ubus.call("luci.argon_wallpaper", "get_url") ?? {};
if (picurl?.url && match(picurl.url, /^(https?:\/\/|\/\/)www\.bing\.com\/|^(https?:\/\/)api\.vvhan\.com\/|^(https?:\/\/)images\.unsplash\.com\/|^(https?:\/\/)wallhaven\.cc\//))
remoteBgUrl = picurl.url;
}
}
let background = fetchMedia("/www" + themeDir, themeDir);
// Priority: local uploaded backgrounds win; only fall through to
// the remote Bing/preload URL when no local files exist.
if (length(background) > 0) {
let currentBg = background[(rand() % length(background))];
bgUrl = currentBg.url;
if (index(videoTypes, " " + currentBg.type + " ") != -1) {
backgroundType = "Video";
mimeType = "video/" + currentBg.type;
}
}
return {bgUrl, remoteBgUrl, backgroundType, mimeType};
}
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
const themeDir = media + "/background/";
const bgArry = selectBackground(themeDir);
%}
<!-- Login Page Start -->
<div class="login-page">
{% if ( bgArry.backgroundType == "Video" ): %}
<!-- Video Player Start -->
<div class="video">
<video autoplay loop muted id="video">
<source src="{{ bgArry.bgUrl }}" type="{{ bgArry.mimeType }}">
</video>
</div>
<div class="volume-control mute"></div>
<script>
document.querySelector(".volume-control").addEventListener("click", function(){
if(this.classList.contains("mute")){
this.classList.remove("mute");
document.getElementById("video").muted = false;
}else{
this.classList.add("mute");
document.getElementById("video").muted = true;
}
});
</script>
<!-- Video Player End -->
{% else %}
<!-- Image Background Start -->
<div class="main-bg" id="main-bg" style="background-image:url({{ bgArry.bgUrl }})"></div>
<script type="text/javascript">//<![CDATA[
(function() {
var remote = "{{ bgArry.remoteBgUrl }}";
if (!remote)
return;
var img = new Image();
img.onload = function() {
var el = document.getElementById("main-bg");
if (el)
el.style.backgroundImage = "url(" + remote + ")";
};
img.onerror = function() {};
img.src = remote;
})();
//]]></script>
<!-- Image Background End -->
{% endif %}
<!-- Login Container Start -->
<div class="login-container">
<div class="login-form">
<!-- Logo Start -->
<a class="brand" href="/"><img src="{{ media }}/img/argon.svg" class="icon">
<span class="brand-text">{{ hostname }}</span>
</a>
<!-- Logo End -->
<!-- Login Form Start -->
<form class="form-login" method="post" action="{{ http.getenv("REQUEST_URI") }}">
{%- if (fuser): %}
<div class="errorbox">{{ _('Invalid username and/or password! Please try again.') }}</div>
{% endif -%}
<div class="input-container">
<div class="input-group user-icon">
<input class="cbi-input-user" id="cbi-input-user" type="text" name="luci_username" value="{{ entityencode(duser, true) }}" />
<label class="border" for="cbi-input-user"></label>
</div>
<div class="input-group pass-icon">
<input class="cbi-input-password" id="cbi-input-password" type="password" name="luci_password" />
<label class="border" for="cbi-input-password"></label>
</div>
</div>
<div>
<input type="submit" value="{{ _('Log in') }}" class="cbi-button cbi-button-apply" />
</div>
</form>
<!-- Login Form End -->
<script type="text/javascript">//<![CDATA[
var input = document.getElementsByName('luci_password')[0];
if (input)
input.focus();
//]]></script>
{% include("themes/" + theme + "/footer_login") %}