💋 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

+15
View File
@@ -0,0 +1,15 @@
.DS_Store
node_modules
settings.local.json
.claude
./package-lock.json
./yarn.lock
./package.json
./.env
./.env.local
./.env.development
./.env.production
./.env.test
./.env.*.local
./.vscode
./.idea
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+18
View File
@@ -0,0 +1,18 @@
#
# Copyright (C) 2025 NoobLk
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Noobwrt Theme
LUCI_DEPENDS:=+wget +jsonfilter
PKG_VERSION:=1.1.7
PKG_RELEASE:=1
CONFIG_LUCI_CSSTIDY:=1
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+187
View File
@@ -0,0 +1,187 @@
<!-- markdownlint-configure-file {
"MD013": {
"code_blocks": false,
"tables": false,
"line_length":200
},
"MD033": false,
"MD041": false
} -->
[license]: /LICENSE
[license-badge]: https://img.shields.io/github/license/nooblk-98/luci-theme-noobwrt?style=flat-square&a=1
[prs]: https://github.com/nooblk-98/luci-theme-noobwrt/pulls
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[issues]: https://github.com/nooblk-98/luci-theme-noobwrt/issues/new
[issues-badge]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat-square
[release]: https://github.com/nooblk-98/luci-theme-noobwrt/releases
[release-badge]: https://img.shields.io/github/v/release/nooblk-98/luci-theme-noobwrt?style=flat-square
[download]: https://github.com/nooblk-98/luci-theme-noobwrt/releases
[download-badge]: https://img.shields.io/github/downloads/nooblk-98/luci-theme-noobwrt/total?style=flat-square
[official]: https://github.com/openwrt/openwrt
[immortalwrt]: https://github.com/immortalwrt/immortalwrt
# NoobWRT LuCI Theme
> A clean, modern OpenWrt LuCI theme with live wallpapers, animated progress bars, automatic light/dark scheduling, customizable accent colors, and a quick-access toolbar.
<p align="center">
<a href="https://github.com/nooblk-98/luci-theme-noobwrt/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/nooblk-98/luci-theme-noobwrt?style=flat-square" alt="license">
</a>
<a href="https://github.com/nooblk-98/luci-theme-noobwrt/pulls">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="prs">
</a>
<a href="https://github.com/nooblk-98/luci-theme-noobwrt/issues/new">
<img src="https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat-square" alt="issues">
</a>
<a href="https://github.com/nooblk-98/luci-theme-noobwrt/releases">
<img src="https://img.shields.io/github/v/release/nooblk-98/luci-theme-noobwrt?style=flat-square" alt="release">
</a>
<a href="https://github.com/nooblk-98/luci-theme-noobwrt/releases">
<img src="https://img.shields.io/github/downloads/nooblk-98/luci-theme-noobwrt/total?style=flat-square" alt="download">
</a>
</p>
<br>
<p align="center">
<img src="screenshots/banner.png" alt="NoobWRT Login Page" width="100%">
</p>
---
## Screenshots
### Desktop
<table>
<tr>
<td align="center" width="50%">
<img src="screenshots/sc_desktop_light.png" alt="Overview — Light Mode" width="100%">
<br><sub><b>Overview — Light Mode</b></sub>
</td>
<td align="center" width="50%">
<img src="screenshots/sc_desktop_dark.png" alt="Overview — Dark Mode" width="100%">
<br><sub><b>Overview — Dark Mode</b></sub>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<img src="screenshots/sc_settings.png" alt="Theme Settings — Toolbar Panel" width="100%">
<br><sub><b>Theme Settings — Toolbar Panel</b></sub>
</td>
</tr>
</table>
### Mobile
<table>
<tr>
<td align="center" valign="top" width="33%">
<img src="screenshots/sc_mobile.png" alt="Mobile — Status (Light)" height="500">
<br><sub><b>Mobile — Status (Light)</b></sub>
</td>
<td align="center" valign="top" width="33%">
<img src="screenshots/sc_mobile_dark.png" alt="Mobile — Status (Dark)" height="500">
<br><sub><b>Mobile — Status (Dark)</b></sub>
</td>
<td align="center" valign="top" width="33%">
<img src="screenshots/sc_mobile_login.png" alt="Mobile — Login" height="500">
<br><sub><b>Mobile — Login</b></sub>
</td>
</tr>
</table>
---
## Features
- **Animated progress bars** — diagonal stripe animation using your accent color
- **Online wallpapers** — Bing Daily Photo, Lorem Picsum, or Wikimedia Picture of the Day, no API key required
- **Custom wallpaper upload** — upload your own JPG/PNG/WebP login background
- **Accent color picker** — separate colors for light and dark mode, applied everywhere
- **Automatic Light / Dark scheduling** — switches to Dark from 6 PM to 6 AM and Light from 6 AM to 6 PM automatically; enable/disable in settings (default: enabled)
- **Light / Dark mode** — manual toggle via toolbar sun/moon button; respects OS preference when auto-schedule is off
- **Quick-access toolbar** — slides in from the right edge; hover the `‹` tab to peek, click to pin open/closed
- **Blur & opacity controls** — fine-tune the login page background effect
- **Responsive** — works on desktop and mobile
---
## Theme Settings
Go to **System → NoobWrt Theme** in the LuCI menu.
### Appearance
| Option | Description |
|--------|-------------|
| **Automatic Light / Dark Mode** | When enabled (default), the theme switches to Dark from **6 PM to 6 AM** and to Light from **6 AM to 6 PM** automatically. The toolbar toggle still works but the schedule takes effect on the next minute tick. Disable to use manual mode. |
| **Default Theme Mode** | `Light` or `Dark` — applied on page load when Automatic mode is disabled. Toggle anytime via the toolbar sun/moon button. |
| **Accent Color — Light Mode** | Hex color for the light theme, e.g. `#5e72e4`. Used for progress bars, buttons, active states. |
| **Accent Color — Dark Mode** | Hex color for the dark theme, e.g. `#7c8ff5`. |
### Wallpaper
| Action | Description |
|--------|-------------|
| **Online Wallpaper Source** | `Bing Daily Photo`, `Lorem Picsum (Random)`, or `Wikimedia Picture of the Day`. Set to `None` to use a custom upload or the default. |
| **Upload Wallpaper** | Upload a JPG, PNG, or WebP image (max 10 MB) as the login background. |
| **Revert to Default** | Remove the custom wallpaper and restore the built-in default. |
> Online sources are cached for 12 hours. Delete `/var/run/noobwrt_*.url` on the router to force a refresh.
### Background Effects
| Option | Description |
|--------|-------------|
| **Blur Radius — Light Mode** | Background blur in pixels, light mode. Default: `10` |
| **Blur Radius — Dark Mode** | Background blur in pixels, dark mode. Default: `10` |
| **Background Opacity — Light Mode** | Overlay opacity, `0.0` (transparent) to `1.0` (opaque). Default: `0.8` |
| **Background Opacity — Dark Mode** | Overlay opacity, dark mode. Default: `0.8` |
### Toolbar Panel
The toolbar is a quick-access icon panel fixed to the right edge of every page.
**Behaviour:**
- A `‹` tab is always visible on the right edge. **Hover** it to slide the panel in temporarily — move the mouse away and it slides back.
- **Click** the tab to pin the panel open or closed. The state is saved across page loads.
| Field | Description |
|-------|-------------|
| **Enable** | Show or hide this item. |
| **Label** | Tooltip text shown on hover. |
| **URL / Path** | Destination URL, e.g. `/cgi-bin/luci/admin/status/overview`. |
| **Icon** | Choose from: Home, Signal, Cell, SMS, Network, NAS, WiFi, Firewall, Settings, Terminal, VPN, Files, Info. |
| **Order** | Position in the toolbar — lower numbers appear first. |
---
## Installation
### Install via opkg
Download the latest `.ipk` from [Releases](https://github.com/nooblk-98/luci-theme-noobwrt/releases) and run:
```bash
opkg install luci-theme-noobwrt_*.ipk
```
### Build from source
```bash
cd openwrt/package
git clone https://github.com/nooblk-98/luci-theme-noobwrt.git
make menuconfig # LUCI → Themes → luci-theme-noobwrt
make -j1 V=s
```
---
## Credits
- [luci-theme-argon](https://github.com/jerrykuku/luci-theme-argon/) — original inspiration
- [luci-theme-material](https://github.com/LuttyYang/luci-theme-material/)
@@ -0,0 +1,2 @@
Drop background here!
accept jpg png gif mp4 webm
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,963 @@
/**
* NoobWrt is a clean HTML5 theme for LuCI. It is based on luci-theme-material and NoobWrt Template
*
* luci-theme-noobwrt
* Copyright 2025 Jerryk <jerrykuku@gmail.com>
*
* Have a bug? Please create an issue here on GitHub!
* https://github.com/jerrykuku/luci-theme-noobwrt/issues
*
* luci-theme-bootstrap:
* Copyright 2008 Steven Barth <steven@midlink.org>
* Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
* Copyright 2012 David Menting <david@nut-bolt.nl>
*
* MUI:
* https://github.com/muicss/mui
*
* luci-theme-material:
* https://github.com/LuttyYang/luci-theme-material/
*
* Agron Theme
* https://demos.creative-tim.com/argon-dashboard/index.html
*
* Login background
* https://unsplash.com/
*
* Licensed to the public under the Apache License 2.0
*/
body {
background: #1e1e1e;
color: #cccccc;
}
.spinning::before {
filter: invert(1);
}
.login-page .login-container .login-form .login-card {
background-color: rgba(18, 18, 28, 0.92);
-webkit-backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9);
backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}
.login-page .login-container .login-form .login-card .login-hero {
background: linear-gradient(135deg, rgba(33, 33, 44, 0.95), rgba(26, 27, 40, 0.95));
}
.login-page .login-container .login-form .login-card .login-hero .hero-particles span {
background: rgba(124, 118, 255, 0.4);
box-shadow: 0 0 18px rgba(124, 118, 255, 0.45);
}
.login-page .login-container .login-form .login-card .login-hero .hero-icon {
background: rgba(124, 118, 255, 0.08);
border: 1px solid rgba(124, 118, 255, 0.28);
box-shadow: 0 0 28px rgba(124, 118, 255, 0.35), inset 0 0 18px rgba(124, 118, 255, 0.12);
}
.login-page .login-container .login-form .brand {
color: #adb5bd;
}
.login-page .login-container .login-form .brand-text-only {
color: #f3f4f6;
}
.login-page .login-container .login-form .login-subtitle {
color: #b7bdcc;
font-weight: 500;
}
.login-page .login-container .login-form .form-login .errorbox {
color: #fecaca;
background: rgba(220, 38, 38, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
}
.login-page .login-container .login-form .form-login .input-group::before {
color: #a1a8b8;
}
.login-page .login-container .login-form .form-login .input-group input {
background-color: rgba(30, 30, 40, 0.9) !important;
color: #e5e7eb;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
border-radius: 999px !important;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 20px rgba(0, 0, 0, 0.35);
}
.login-page .login-container .login-form .form-login .input-group input:focus {
border-color: rgba(99, 102, 241, 0.65) !important;
background-color: rgba(36, 36, 48, 0.95) !important;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.25);
}
.login-page .login-container .login-form .form-login .input-group input::placeholder {
color: #8f96a8;
}
.login-page .login-container .login-form .form-login .cbi-button-apply {
background: linear-gradient(135deg, #7c76ff, #5b8dff) !important;
}
.login-page .login-container .login-form .form-login .cbi-button-apply:hover,
.login-page .login-container .login-form .form-login .cbi-button-apply:focus {
opacity: 0.95;
}
header::after {
background-color: #1e1e1e !important;
}
.main .main-left {
background-color: #333333 !important;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.15);
}
.main .main-left .sidenav-header .brand {
color: #cccccc;
}
.main .main-left .nav .slide .slide-menu .active a {
color: #cccccc;
}
.main .main-left .nav .slide .slide-menu .active a::after {
background-color: #cccccc !important;
}
.main .main-left .nav .slide .slide-menu .active::after {
background-color: var(--dark-primary) !important;
}
.main .main-left .nav .slide .slide-menu li a {
color: #cccccc;
}
.main .main-left .nav .slide .slide-menu li a:hover {
background: none !important;
}
.main .main-left .nav .slide .slide-menu li::after {
background-color: var(--dark-primary) !important;
}
.main .main-left .nav .slide .menu.active {
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
color: #ffffff !important;
}
.main .main-left .nav .slide .menu.active a::after {
background-color: #ffffff !important;
}
.main .main-left .nav li a {
color: #cccccc !important;
}
.main .main-left .nav li a:hover {
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
color: #ffffff !important;
}
.main .main-left::-webkit-scrollbar-thumb {
background-color: #252526 !important;
}
.main .main-left::-webkit-scrollbar-track {
background-color: #333;
}
.main .main-right {
background-color: #1e1e1e;
}
h2 {
color: #ccc;
background: #333333;
}
h3 {
color: #ccc;
border-bottom: 0;
background: #333333;
}
h4 {
color: #8C6900;
}
abbr {
color: var(--dark-primary) !important;
}
a:link,
a:visited,
a:active {
color: #a5b2ff;
}
input:-webkit-autofill {
background-color: #3c3c3c !important;
}
#channel_graph {
background-color: transparent !important;
}
.cbi-value-field .cbi-input-apply,
.cbi-button-apply,
.cbi-button-edit {
color: #fff !important;
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
border-color: #483d8b !important;
border-color: var(--dark-primary) !important;
}
.cbi-section em {
color: #ccc;
}
header.bg-primary {
background-color: #1e1e1e !important;
}
.cbi-map-descr {
color: #ccc;
}
.cbi-section {
background: none;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
}
.panel-title {
color: #ccc;
background-color: #333333;
}
div > table > tbody > tr:nth-of-type(2n),
div > .table > .tr:nth-of-type(2n) {
background-color: #252526;
}
table > tbody > tr > td,
table > tfoot > tr > td,
table > thead > tr > td {
color: #cccccc;
}
fieldset > table > tbody > tr:nth-of-type(2n) {
background-color: #252526;
}
table > tbody > tr > td,
table > tfoot > tr > td,
table > thead > tr > td {
border-top: 1px solid #252526;
}
#swaptotal > div > div,
#swapfree > div > div,
#memfree > div > div,
#membuff > div > div,
#conns > div > div,
#memtotal > div > div {
background-color: var(--dark-primary) !important;
}
#swaptotal > div > div > div > small,
#swapfree > div > div > div > small,
#memfree > div > div > div > small,
#membuff > div > div > div > small,
#conns > div > div > div > small,
#memtotal > div > div > div > small {
color: #ccc !important;
}
.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last {
line-height: 1.8em;
}
.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] {
border: 1px solid #999999 !important;
background-color: transparent !important;
}
.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] div {
background-color: #ba8b00 !important;
}
table > tbody > tr > th,
table > tfoot > tr > th,
table > thead > tr > th {
background-color: #252526;
border-bottom: black 1px solid !important;
}
.tr[data-title]::before,
.tr.cbi-section-table-titles.named::before {
background-color: #252526;
}
tr > td,
tr > th,
.tr > .td,
.tr > .th,
.cbi-section-table-row::before,
#cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) {
border-top: 0;
}
.cbi-rowstyle-2 {
background-color: #1e1e1e;
}
.cbi-rowstyle-1 {
background-color: #252526;
}
.cbi-rowstyle-2 .cbi-button-up,
.cbi-rowstyle-2 .cbi-button-down,
body:not(.Interfaces) .cbi-rowstyle-2:first-child {
background-color: #666666 !important;
}
.cbi-section > h3:first-child,
.panel-title,
h3 {
color: #ccc;
}
h4 {
background-color: #1e1e1f;
}
.cbi-progressbar {
background: #282a2c;
}
.cbi-progressbar div {
background-color: var(--dark-primary) !important;
}
.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell {
background-color: #1e1e1f;
}
.cbi-button {
color: #ffffff !important;
background-color: #483d8b;
background-color: var(--dark-primary);
}
.cbi-section-node {
background: none;
}
abbr {
color: #5e72e4;
}
div > table > tbody > tr:nth-of-type(2n),
div > .table > .tbody > .tr:nth-of-type(2n) {
background-color: #252526;
}
#content_syslog {
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
}
#syslog {
color: #ccc;
background-color: #1e1e1e;
}
#iwsvg,
#iwsvg2,
#bwsvg {
overflow: hidden;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
background-color: #1e1e1e !important;
}
.tabs {
background-color: #252526;
}
.tabs li[class~="active"],
.tabs li:hover {
cursor: pointer;
color: #ccc !important;
}
.tabs li[class~="active"] a,
.tabs li:hover a {
color: #ccc !important;
}
.tabs > li[class~="active"] > a {
color: #ccc;
}
.tabs > li[class~="active"],
.tabs > li:hover {
border-bottom: 0.18751rem solid #483d8b;
border-bottom: 0.18751rem solid var(--dark-primary);
color: var(--dark-primary) !important;
background-color: #181819;
}
.cbi-tabmenu > li > a,
.tabs > li > a {
color: #ccc;
}
.cbi-tabmenu > li > a:hover,
.tabs > li > a:hover {
color: #ccc;
}
.cbi-tabmenu > li {
background: #2d2d2d;
}
.cbi-tabmenu li[class~="cbi-tab"] a {
color: #ccc;
}
.cbi-tabmenu > li:hover {
color: #ccc;
background: #2d2d2d;
border-bottom: 0.18751rem solid var(--dark-primary) !important;
}
.cbi-tabmenu > li[class~="cbi-tab"] {
background-color: #181819;
border-bottom: 0.18751rem solid var(--dark-primary) !important;
}
.cbi-tabcontainer > .cbi-value:nth-of-type(2n) {
background-color: #252526;
}
.cbi-value-title {
color: #ccc;
}
select,
input {
color: #ccc;
background-color: transparent !important;
border: 1px solid #252526;
box-shadow: none;
}
select:not([multiple="multiple"]):focus,
input:focus {
border-color: #483d8b !important;
border-color: var(--dark-primary) !important;
outline: 0;
}
select:not([multiple="multiple"]):focus,
input:not(.cbi-button):focus,
.cbi-dropdown:focus {
border-color: #5e72e4;
border-color: var(--dark-primary) !important;
}
select {
background-color: #1e1e1e !important;
}
#cbi-dropbear h2,
#cbi-dropbear .cbi-map-descr,
#cbi-dropbear .cbi-map-descr abbr,
#cbi-rc h2,
#cbi-rc .cbi-map-descr,
#cbi-distfeedconf h2,
#cbi-distfeedconf .cbi-map-descr,
#cbi-customfeedconf h2,
#cbi-customfeedconf .cbi-map-descr,
#cbi-download h2,
#cbi-filelist h2 {
color: #ccc !important;
}
.cbi-value-field > ul > li .ifacebadge {
background-color: #3c3c3c;
}
.cbi-section-descr {
color: #ccc;
}
.cbi-input-textarea {
background-color: #1e1e1e;
color: #ccc;
border: 1px solid #3c3c3c !important;
}
textarea:focus-visible {
border: 1px solid var(--dark-primary);
}
.cbi-section-remove:nth-of-type(2n),
.cbi-section-node:nth-of-type(2n) {
background-color: #1e1e1e;
}
.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3),
.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) {
background-color: #1e1e1e;
}
.node-system-packages > .main table tr td:nth-last-child(1) {
color: #ccc;
}
.cbi-tooltip {
background-color: var(--primary);
color: #fff;
}
.ifacebox {
background-color: transparent !important;
border: 1px solid #1e1e1e;
}
.ifacebox-head {
color: #ccc;
background: #666;
}
.ifacebox-body {
background-color: #333;
}
.zonebadge strong {
color: #333;
}
.zonebadge > .ifacebadge {
background-color: #3c3c3c;
}
div.cbi-value var,
td.cbi-value-field var {
color: #5e72e4;
}
#diag-rc-output > pre {
color: #ccc;
background-color: #1e1e1e;
}
.node-services-vssr .block {
background-color: #3c3c3c !important;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
}
.node-services-vssr .block h4 {
color: #ccc !important;
}
.node-services-vssr .status-bar {
color: #ccc;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
background-color: #1e1e1e;
}
.node-services-vssr .cbi-section-table-row {
color: #ccc;
background-color: #3c3c3c !important;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.35);
}
.node-services-vssr .cbi-section-table-row.fast {
background: #483d8b !important;
background: var(--dark-primary) !important;
color: #fff;
}
.node-services-vssr .ssr-button {
color: #ccc;
}
.node-services-vssr .incon:nth-child(2) {
border-right: #1e1e1e 1px solid;
}
#xhr_poll_status > .label.success {
color: #ccc !important;
background-color: darkolivegreen !important;
}
.notice {
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
}
.cbi-section.fade-in .cbi-title > div:last-child span {
background: none!important;
color: #ced4da;
}
.cbi-input-find,
.cbi-input-save,
.cbi-button-add,
.cbi-button-save,
.cbi-button-find,
.cbi-input-reload,
.cbi-button-reload {
color: #fff !important;
background: darkolivegreen !important;
border-color: darkolivegreen !important;
}
.cbi-button-reset,
.cbi-input-remove {
color: #fff !important;
background-color: darkorange !important;
border-color: darkorange !important;
}
.cbi-page-actions .cbi-button-apply,
.cbi-section-actions .cbi-button-edit,
.cbi-button-edit.important,
.cbi-button-apply.important,
.cbi-button-reload.important,
.cbi-button-action.important {
border: 1px #483d8b solid !important;
border: 1px var(--dark-primary) solid !important;
}
.btn[value="Dismiss"],
.cbi-button[value="Terminate"],
.cbi-button[value="Reset"],
.cbi-button[value="Disabled"],
.cbi-button[onclick^="iface_reconnect"],
.cbi-button[onclick="handleReset(event)"],
.cbi-button-neutral[value="Disable"] {
color: #fff;
border: thin solid darkorange !important;
background-color: darkorange !important;
}
fieldset[id^="cbi-apply-"] {
background-color: #333333;
}
#detail-bubble > div {
border: 1px solid #ccc;
background: #252525;
}
.ifacebox-head.active {
background-color: var(--dark-primary) !important;
}
header .fill .status span[data-style="active"] {
color: #ccc !important;
background-color: darkolivegreen !important;
}
#cbi-wireless .td,
#cbi-network .tr:first-child > .td,
.table[width="100%"] > .tr:first-child > .td,
[data-page="admin-network-diagnostics"] .tr > .td,
.tr.table-titles > .th,
.tr.cbi-section-table-titles > .th {
background-color: #252526;
border-bottom: black 1px solid !important;
}
.network-status-table .ifacebox-body .ifacebadge {
background-color: #252526;
border-bottom: 0;
box-shadow: none;
}
td > .ifacebadge,
.td > .ifacebadge {
color: #fff;
background-color: #483d8b;
background-color: var(--dark-primary);
border: 0;
}
.alert,
.alert-message {
background-color: #333;
}
.alert-message.warning {
background-color: #986400 !important;
}
.alert.error,
.alert-message.error {
background-color: #784f00;
}
.alert h4,
.alert-message h4 {
background-color: #784f00;
color: #ffbf00;
}
.alert-message [class="btn"] {
background-color: #777777;
color: #ccc;
}
.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > ins {
border-color: #00ad00;
background-color: #688668;
}
.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > del {
border-color: #c60000;
background-color: #896565;
}
.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var {
background-color: #333;
border-color: #666;
}
.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var ins {
background-color: #688668;
}
.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var del {
background-color: #896565;
}
.uci-dialog .cbi-section .uci-change-list > var {
background-color: #333;
border-color: #666;
}
.uci-dialog .cbi-section .uci-change-list > var del {
background-color: #896565;
}
.uci-dialog .cbi-section .uci-change-list > var ins {
background-color: #688668;
}
.uci-dialog .cbi-section .uci-change-list > ins {
border-color: #00ad00;
background-color: #688668;
}
.uci-dialog .cbi-section .uci-change-list > del {
border-color: #c60000;
background-color: #896565;
}
.uci-dialog .cbi-section .uci-change-list + .right .btn {
color: #ccc;
}
.uci-dialog .cbi-section .uci-change-list + .right .cbi-button {
border: 1px solid #3c3c3c !important;
}
.btn.danger,
.cbi-section-remove > .cbi-button,
.cbi-button-remove,
.cbi-button-reset,
.cbi-button-negative,
.cbi-button[value="Stop"],
.cbi-button[value="Kill"],
.cbi-button[onclick="reboot(this)"],
.cbi-button-neutral[value="Restart"] {
border: thin solid darkorange !important;
background-color: darkorange !important;
}
.cbi-section,
.cbi-section-error,
#iptables,
.Firewall form,
#cbi-network > .cbi-section-node,
#cbi-wireless > .cbi-section-node,
#cbi-wireless > #wifi_assoclist_table,
[data-tab-title],
[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear),
[data-page="admin-system-opkg"] #maincontent > .container {
background: #1e1e1e !important;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
}
div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] {
background: transparent !important;
}
[data-page="admin-system-admin"] .cbi-map h2,
[data-page="admin-system-admin-password"] .cbi-map h2,
[data-page="admin-system-admin"] .cbi-map .cbi-map-descr,
[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr {
color: #ccc;
}
[data-page="admin-system-flash"] .modal label > input[type="checkbox"] {
vertical-align: text-top;
top: auto;
border-color: #fff !important;
}
[data-page="admin-system-flash"] .modal .btn {
white-space: normal !important;
background-color: darkseagreen;
}
[data-page="admin-system-flash"] .modal .alert-message {
background-color: transparent !important;
}
[data-page="admin-system-flash"] .modal .danger {
border: thin solid darkorange !important;
background-color: darkorange !important;
}
.cbi-value input[type="password"] + .cbi-button-neutral {
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
border-color: var(--dark-primary) !important;
}
.btn,
button,
select,
input,
.cbi-dropdown {
border: 1px solid #3c3c3c !important;
}
.cbi-dropdown .preview {
color: #ccc;
}
.cbi-section-table-row {
background-color: #1e1e1e !important;
}
.modal {
background-color: #1e1e1e;
}
.cbi-button-positive {
color: #fff !important;
background-color: darkolivegreen !important;
}
[data-page="admin-system-flash"] legend {
color: #ccc;
}
.logout:before {
color: #adb5bd !important;
}
.cbi-dropdown[open] {
border-color: #483d8b !important;
border-color: var(--dark-primary) !important;
}
.cbi-dropdown[open] > ul.dropdown {
background: #252526 !important;
color: #ffffff !important;
box-shadow: none;
border: 1px solid #3c3c3c !important;
}
.cbi-dropdown[open] > ul.dropdown li {
color: #ffffff;
border-bottom: 1px solid #3c3c3c !important;
}
.cbi-dropdown[open] > ul.dropdown > li[selected] {
background-color: #483d8b !important;
background-color: var(--dark-primary) !important;
border-bottom: 1px solid #3c3c3c !important;
}
.cbi-dropdown[open] > ul.dropdown > li.focus {
background: #483d8b;
background: var(--dark-primary);
outline: none;
}
.ifacebadge {
background-color: #333333;
}
.cbi-dynlist > .item > span {
border: 1px solid #3c3c3c !important;
}
.cbi-page-actions .cbi-button-apply,
.cbi-section-actions .cbi-button-edit,
.cbi-button-edit,
.cbi-button-apply,
.cbi-button-reload,
.cbi-button-action,
.cbi-button[value="Submit"],
.cbi-button[value="Upload"],
.cbi-button[value$="Apply"],
.cbi-button[onclick="addKey(event)"] {
background: #483d8b !important;
background: var(--dark-primary) !important;
border-color: var(--dark-primary) !important;
}
.btn.primary,
.cbi-page-actions .cbi-button-save,
.cbi-page-actions .cbi-button-apply + .cbi-button-save,
.cbi-button-add,
.cbi-button-save,
.cbi-button-positive,
.cbi-button-link,
.cbi-button[value="Enable"],
.cbi-button[value="Scan"],
.cbi-button[value^="Back"],
.cbi-button-neutral[onclick="handleConfig(event)"] {
background: #483d8b;
background: var(--dark-primary);
}
[data-page="admin-system-opkg"] h2 {
color: #ccc !important;
}
[data-page="admin-system-startup"] textarea {
color: #ccc;
background-color: transparent;
}
[data-page="admin-system-startup"] textarea:focus-visible {
border: 1px solid var(--dark-primary);
}
[data-page="admin-network-firewall-custom"] #view p textarea,
[data-page="admin-status-routes"] #view p textarea,
[data-page="admin-system-crontab"] #view p textarea {
color: #ccc;
background-color: transparent;
}
#view > .spinning {
background: #333333 !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03) !important;
}
@media screen and (min-width: 600px) {
::-webkit-scrollbar-thumb {
background: var(--dark-primary);
}
::-webkit-scrollbar-thumb:hover {
background: var(--dark-primary);
}
::-webkit-scrollbar-thumb:active {
background: var(--dark-primary);
}
}
@media screen and (max-width: 480px) {
.node-status-iptables > .main div > .cbi-map > form {
background-color: #1e1e1e;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35);
}
}
[data-page=""] .main-right > #maincontent .Dashboard,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard {
color: #ccc !important;
}
[data-page=""] .main-right > #maincontent .Dashboard h3,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard h3 {
color: #ccc;
}
[data-page=""] .main-right > #maincontent .Dashboard hr,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard hr {
border-top: 1px solid #ffffff;
}
[data-page=""] .main-right > #maincontent .Dashboard .dashboard-bg,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard .dashboard-bg {
background-color: #333333;
}
[data-page=""] .main-right > #maincontent .Dashboard img.svgmonochrome,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard img.svgmonochrome {
filter: invert(90%);
}
[data-page=""] .main-right > #maincontent .Dashboard tr,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard tr {
border-top: thin solid #4d4d4d;
}
[data-page=""] .main-right > #maincontent .Dashboard tr:last-child,
[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard tr:last-child {
border-bottom: thin solid #4d4d4d;
}
[data-page="admin-system-fileassistant"] .fb-container .panel-container {
border-bottom-color: #3c3c3c !important;
}
[data-page="admin-system-fileassistant"] .fb-container td[class$="-icon"]::before {
filter: invert(0.7);
}
[data-page="admin-system-fileassistant"] div#list-content table.cbi-section-table tbody tr:nth-child(1) td.parent-icon {
background: #1e1e1e !important;
}
[data-page^="admin-services-openclash"] .oc {
--bg-light: #1e1e1e !important;
--bg-gray: #1e1e1e !important;
}
[data-page^="admin-services-openclash"] .oc .developer-container {
background: transparent !important;
}
[data-page^="admin-services-openclash"] .config-upload-content {
background: #1e1e1e !important;
}
[data-page^="admin-services-openclash"] .cbi-tabmenu::-webkit-scrollbar-thumb {
background-color: #5b5b5b;
}
[data-page^="admin-services-openclash"] .cbi-tabmenu::-webkit-scrollbar-track {
background-color: rgba(60, 60, 60, 0.75);
}
[data-page^="admin-services-openclash"] #tab {
border-color: #3c3c3c;
}
[data-page^="admin-services-openclash"] #diag-rc-output > pre,
[data-page^="admin-services-openclash"] #dns-rc-output > pre {
color: #ccc;
border: 1px solid #3c3c3c !important;
background-color: #1e1e1e;
}
[data-page^="admin-services-openclash"] img[src$="arrow-clockwise-light.svg"],
[data-page^="admin-services-openclash"] img[src$="wrench-light.svg"],
[data-page^="admin-services-openclash"] img[src$="eye-light.svg"] {
filter: invert(0.9);
}
[data-page="admin-network-diagnostics"] .cbi-section {
background: #252526 !important;
}
[data-page="admin-network-diagnostics"] textarea {
background: transparent;
border-radius: 0.25rem;
color: #ccc;
border: 1px solid #3c3c3c !important;
}
[data-page="admin-network-diagnostics"] .tr > .td {
background-color: #252526 !important;
}
[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 144, 240, 144"] {
background-color: #497e49 !important;
color: #ccc !important;
}
[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 240, 144, 144;"] {
background-color: #9c4f4f !important;
color: #ccc !important;
}
[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 238, 238, 238;"] {
background-color: #666 !important;
}
[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea {
border: 1px solid #3c3c3c !important;
background-color: transparent !important;
}
[data-page="admin-network-network"] #modal_overlay div[data-tab="peers"] div[data-name="_peers"] .ifacebadge code {
color: #fff !important;
background-color: transparent !important;
}
[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea {
color: #ccc !important;
}
[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] {
--zone-color-rgb: 156, 79, 79 !important;
}
[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong {
color: #ccc !important;
}
[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] {
--zone-color-rgb: 112, 112, 112 !important;
}
[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] em {
color: #ccc !important;
}
[data-page="admin-network-firewall-rules"] #cbi-firewall-rule > table > .cbi-section-table-row[data-title]::before {
background-color: #252526 !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"],
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] {
--zone-color-rgb: 156, 79, 79 !important;
color: #ccc !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong,
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong {
color: #ccc !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"],
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] {
--zone-color-rgb: 73, 126, 73 !important;
color: #ccc !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] strong,
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] strong {
color: #ccc !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"],
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] {
--zone-color-rgb: 112, 112, 112 !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong,
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong {
color: #ccc !important;
}
[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong + span,
[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong + span {
color: #ccc !important;
}
[data-page="admin-system-commands"] .commandbox,
[data-page="admin-system-commands-dashboard"] .commandbox {
border-bottom: thin solid #333;
background: #252526;
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), 0 1px 2px rgba(255, 255, 255, 0.05);
}
.btn {
background-color: #707070;
color: #fff;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.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: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 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: 4.6 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="900" zoomAndPan="magnify" viewBox="0 0 675 675.000002" height="900" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="438b7503c7"><rect x="0" width="499" y="0" height="573"/></clipPath></defs><g transform="matrix(1, 0, 0, 1, 89, 90)"><g clip-path="url(#438b7503c7)"><g fill="#ff914d" fill-opacity="1"><g transform="translate(3.55271, 443.299439)"><g><path d="M 169.222656 -154.308594 C 169.222656 -154.308594 299.4375 -77.441406 439.402344 30.976562 L 454.890625 21.796875 C 454.890625 21.796875 438.253906 -110.710938 438.253906 -204.214844 C 438.253906 -330.984375 459.480469 -414.734375 459.480469 -414.734375 C 436.535156 -410.71875 416.457031 -409.574219 397.527344 -409.574219 C 360.242188 -409.574219 329.839844 -413.015625 298.289062 -418.179688 C 298.289062 -418.179688 315.496094 -325.824219 315.496094 -263.871094 L 315.496094 -238.632812 L 314.351562 -238.632812 C 302.878906 -242.644531 225.4375 -303.453125 154.878906 -354.503906 C 106.121094 -389.496094 65.96875 -432.519531 65.96875 -432.519531 L 49.332031 -427.929688 C 49.332031 -427.929688 51.054688 -345.898438 51.054688 -293.125 C 51.054688 -72.277344 28.109375 6.308594 28.109375 6.308594 C 53.347656 2.292969 81.457031 1.722656 106.695312 1.722656 C 133.65625 1.722656 165.78125 2.867188 193.886719 8.03125 C 193.886719 8.03125 169.222656 -45.890625 169.222656 -137.097656 Z M 169.222656 -154.308594 "/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 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.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -0,0 +1,514 @@
'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.
* When prefers-reduced-motion is active, all animations use 0ms.
*/
durations: {
fast: 200,
normal: 400,
slow: 600
},
/**
* Returns true if the user has requested reduced motion via OS/browser setting.
*/
prefersReducedMotion: function() {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
},
/**
* 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; skip animation for reduced-motion users
const animDuration = this.prefersReducedMotion() ? 0 :
(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; skip animation for reduced-motion users
const animDuration = this.prefersReducedMotion() ? 0 :
(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);
}
};
/**
* NoobWrt 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;
var url = '';
this.renderModeMenu(node);
// 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;
}
});
// Persist open section: clear since we just closed everything
try {
localStorage.setItem('noobwrt.sidenav.open', '[]');
} catch (e) {}
// 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');
// Persist which section is open
try {
var openTitle = target.getAttribute('data-title');
if (openTitle) {
localStorage.setItem('noobwrt.sidenav.open', JSON.stringify([openTitle]));
}
} catch (e) {}
}
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 = '';
// Restore previously opened submenus from localStorage
var openSections = [];
try {
openSections = JSON.parse(localStorage.getItem('noobwrt.sidenav.open') || '[]');
} catch (e) {}
menuContainer.querySelectorAll('.nav > li.slide > a.menu').forEach(function(anchor) {
var title = anchor.getAttribute('data-title');
var slideMenu = anchor.nextElementSibling;
if (!slideMenu) return;
var isActive = anchor.classList.contains('active');
var wasOpen = openSections.indexOf(title) !== -1;
if (isActive || wasOpen) {
anchor.classList.add('active');
slideMenu.classList.add('active');
slideMenu.style.display = 'block';
}
});
// Scroll the active menu item into view smoothly
var activeItem = mainMenuElement.querySelector('.nav > li > a.active, .nav > li > a.menu.active');
if (activeItem) {
setTimeout(function() {
activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 150);
}
}
}
return menuContainer;
},
renderModeMenu: function (tree) {
var menu = document.querySelector('#modemenu');
var children = ui.menu.getChildren(tree);
for (var i = 0; i < children.length; i++) {
var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0);
if (i > 0)
menu.appendChild(E([], ['\u00a0|\u00a0']));
menu.appendChild(E('li', {}, [
E('a', {
'href': L.url(children[i].name),
'class': isActive ? 'active' : null
}, [_(children[i].title)])
]));
if (isActive)
this.renderMainMenu(children[i], children[i].name);
}
if (menu.children.length > 1)
menu.style.display = '';
},
/**
* 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');
}
}
});
@@ -0,0 +1,306 @@
'use strict';
'require view';
'require form';
'require uci';
'require rpc';
'require ui';
var callSetCustomWallpaper = rpc.declare({
object: 'luci.noobwrt_wallpaper',
method: 'set_custom',
expect: { result: -1 }
});
var callRemoveCustomWallpaper = rpc.declare({
object: 'luci.noobwrt_wallpaper',
method: 'remove_custom',
expect: { result: -1 }
});
return view.extend({
load: function () {
return uci.load('noobwrt').catch(function () {
/* /etc/config/noobwrt is absent (fresh install or missing defaults).
* Seed an in-memory named section 'global' so form.NamedSection
* can render without firing uci/get RPCs that return code 4.
* uci.add() returns the internal sid; setting .name promotes it
* to a named section that form.NamedSection('global') will find. */
var sid = uci.add('noobwrt', 'global');
uci.set('noobwrt', sid, '.name', 'global');
uci.set('noobwrt', sid, 'mode', 'normal');
uci.set('noobwrt', sid, 'primary', '#5e72e4');
uci.set('noobwrt', sid, 'dark_primary', '#7c8ff5');
uci.set('noobwrt', sid, 'blur', '10');
uci.set('noobwrt', sid, 'blur_dark', '10');
uci.set('noobwrt', sid, 'transparency', '0.8');
uci.set('noobwrt', sid, 'transparency_dark', '0.8');
uci.set('noobwrt', sid, 'online_wallpaper', 'none');
uci.set('noobwrt', sid, 'auto_dark_mode', '0');
});
},
render: function () {
var m, s, o;
m = new form.Map('noobwrt', _('NoobWrt Theme Settings'),
_('Customize the appearance, wallpaper, background effects, and toolbar panel of the NoobWrt LuCI theme.'));
/* ============================================================
* SECTION: Global – tabbed (Appearance / Wallpaper / Effects / Toolbar / About)
* ============================================================ */
s = m.section(form.NamedSection, 'global', 'global');
s.addremove = false;
s.anonymous = true;
s.tab('appearance', _('Appearance'));
s.tab('wallpaper', _('Wallpaper'));
s.tab('effects', _('Background Effects'));
s.tab('toolbar', _('Toolbar Panel'));
s.tab('about', _('About'));
/* ---- Appearance ---- */
o = s.taboption('appearance', form.Flag, 'auto_dark_mode',
_('Automatic Light / Dark Mode'),
_('Automatically switch to Dark mode from 6 PM to 6 AM and Light mode from 6 AM to 6 PM. When enabled, the schedule overrides the manual toolbar toggle.'));
o.default = '1';
o = s.taboption('appearance', form.ListValue, 'mode',
_('Default Theme Mode'),
_('Theme applied on page load when Automatic mode is disabled. Users can also toggle it manually using the toolbar sun/moon button.'));
o.value('normal', _('Light'));
o.value('dark', _('Dark'));
o = s.taboption('appearance', form.Value, 'primary',
_('Accent Color — Light Mode'),
_('Primary accent color used throughout the light theme.'));
o.placeholder = '#5e72e4';
o.rmempty = false;
o.renderWidget = function(section_id, option_index, cfgvalue) {
var node = form.Value.prototype.renderWidget.apply(this, arguments);
var input = node.querySelector('input');
if (input) {
input.style.cssText = 'width:140px;display:inline-block;vertical-align:middle;margin-right:6px';
var picker = document.createElement('input');
picker.type = 'color';
picker.value = (cfgvalue && cfgvalue !== this.placeholder) ? cfgvalue : this.placeholder;
picker.style.cssText = 'width:40px;height:32px;padding:2px;border:1px solid #ccc;border-radius:4px;cursor:pointer;vertical-align:middle';
picker.addEventListener('input', function() { input.value = picker.value; input.dispatchEvent(new Event('input', { bubbles: true })); });
input.addEventListener('input', function() { if (/^#[0-9a-fA-F]{6}$/.test(input.value)) picker.value = input.value; });
node.appendChild(picker);
}
return node;
};
o = s.taboption('appearance', form.Value, 'dark_primary',
_('Accent Color — Dark Mode'),
_('Primary accent color used throughout the dark theme.'));
o.placeholder = '#7c8ff5';
o.rmempty = false;
o.renderWidget = function(section_id, option_index, cfgvalue) {
var node = form.Value.prototype.renderWidget.apply(this, arguments);
var input = node.querySelector('input');
if (input) {
input.style.cssText = 'width:140px;display:inline-block;vertical-align:middle;margin-right:6px';
var picker = document.createElement('input');
picker.type = 'color';
picker.value = (cfgvalue && cfgvalue !== this.placeholder) ? cfgvalue : this.placeholder;
picker.style.cssText = 'width:40px;height:32px;padding:2px;border:1px solid #ccc;border-radius:4px;cursor:pointer;vertical-align:middle';
picker.addEventListener('input', function() { input.value = picker.value; input.dispatchEvent(new Event('input', { bubbles: true })); });
input.addEventListener('input', function() { if (/^#[0-9a-fA-F]{6}$/.test(input.value)) picker.value = input.value; });
node.appendChild(picker);
}
return node;
};
/* ---- Wallpaper ---- */
var BGURL = '/luci-static/noobwrt/background/custom.jpg';
var BGTMP = '/tmp/noobwrt_wallpaper.tmp';
o = s.taboption('wallpaper', form.ListValue, 'online_wallpaper',
_('Online Wallpaper Source'),
_('Automatically fetch a wallpaper from an online source for the login page background. Set to None to use a custom uploaded image or the built-in default.'));
o.value('none', _('None (use custom / default)'));
o.value('bing', _('Bing Daily Photo'));
o.value('picsum', _('Lorem Picsum (Random)'));
o.value('wikimedia', _('Wikimedia Picture of the Day'));
o.default = 'none';
o = s.taboption('wallpaper', form.DummyValue, '_wallpaper_ui', '');
o.rawhtml = true;
o.default = [
'<div style="max-width:520px">',
'<p style="color:#888;font-size:13px;margin:0 0 12px">',
_('Upload a custom image to use as the login page background. Supported formats: JPG, PNG, WebP (max 10 MB).'),
'</p>',
'<div id="noobwrt-wp-preview" style="margin-bottom:12px;border-radius:10px;overflow:hidden;background:#f0f0f0;height:160px;display:flex;align-items:center;justify-content:center">',
'<img id="noobwrt-wp-img" src="" style="width:100%;height:100%;object-fit:cover;display:none" />',
'<span id="noobwrt-wp-placeholder" style="color:#aaa;font-size:13px">' + _('No custom wallpaper — using default') + '</span>',
'</div>',
'<p id="noobwrt-wp-status" style="font-size:12px;color:#888;min-height:18px;margin:0 0 12px"></p>',
'<div style="display:flex;gap:10px">',
'<button id="noobwrt-wp-upload" type="button" class="btn cbi-button cbi-button-action">' + _('Upload Wallpaper...') + '</button>',
'<button id="noobwrt-wp-revert" type="button" class="btn cbi-button cbi-button-reset">' + _('Revert to Default') + '</button>',
'</div>',
'</div>'
].join('');
/* ---- Background Effects ---- */
o = s.taboption('effects', form.Value, 'blur',
_('Blur Radius — Light Mode'),
_('Login page background blur radius in pixels for light mode. Default: 10'));
o.placeholder = '10';
o.datatype = 'uinteger';
o = s.taboption('effects', form.Value, 'blur_dark',
_('Blur Radius — Dark Mode'),
_('Login page background blur radius in pixels for dark mode. Default: 10'));
o.placeholder = '10';
o.datatype = 'uinteger';
o = s.taboption('effects', form.Value, 'transparency',
_('Background Opacity — Light Mode'),
_('Overlay opacity on the login page background for light mode. Range: 0.0 (transparent) to 1.0 (opaque). Default: 0.8'));
o.placeholder = '0.8';
o = s.taboption('effects', form.Value, 'transparency_dark',
_('Background Opacity — Dark Mode'),
_('Overlay opacity on the login page background for dark mode. Default: 0.8'));
o.placeholder = '0.8';
/* ---- Toolbar tab – placeholder only, real table rendered below ---- */
o = s.taboption('toolbar', form.DummyValue, '_toolbar_placeholder', '');
o.rawhtml = true;
o.default = '<div id="noobwrt-toolbar-tab-content"></div>';
/* ---- About ---- */
o = s.taboption('about', form.DummyValue, '_about', '');
o.rawhtml = true;
o.default = [
'<div style="max-width:480px;background:var(--color-bg-1,#fff);border:1px solid var(--color-border,#e5e7eb);border-radius:12px;padding:20px 24px;box-shadow:0 2px 8px rgba(0,0,0,0.06)">',
' <h3 style="margin:0 0 6px;font-size:16px;font-weight:600">' + _('NoobWrt Theme') + '</h3>',
' <p style="margin:0 0 16px;color:#888;font-size:13px">' + _('A clean modern LuCI theme for OpenWrt routers.') + '</p>',
' <table style="width:100%;border-collapse:collapse;line-height:1.9;font-size:13px">',
' <tr><td style="width:130px;color:#888;padding:3px 0">' + _('Developer') + '</td>',
' <td><strong>NoobLk</strong></td></tr>',
' <tr><td style="color:#888;padding:3px 0">' + _('Repository') + '</td>',
' <td><a href="https://github.com/nooblk-98/luci-theme-noobwrt" target="_blank" rel="noopener" style="color:#5e72e4">github.com/nooblk-98/luci-theme-noobwrt</a></td></tr>',
' <tr><td style="color:#888;padding:3px 0">' + _('Contributors') + '</td>',
' <td><a href="https://github.com/nooblk-98/luci-theme-noobwrt/graphs/contributors" target="_blank" rel="noopener" style="color:#5e72e4">' + _('View on GitHub') + '</a></td></tr>',
' <tr><td style="color:#888;padding:3px 0">' + _('License') + '</td>',
' <td>MIT</td></tr>',
' <tr><td style="color:#888;padding:3px 0">' + _('Issues / Support') + '</td>',
' <td><a href="https://github.com/nooblk-98/luci-theme-noobwrt/issues" target="_blank" rel="noopener" style="color:#5e72e4">' + _('Open an issue') + '</a></td></tr>',
' </table>',
' <p style="margin:16px 0 0;color:#bbb;font-size:12px;text-align:center;border-top:1px solid var(--color-border,#e5e7eb);padding-top:12px">' + _('Made with \u2665 for the OpenWrt community') + '</p>',
'</div>'
].join('\n');
/* ============================================================
* SECTION: Toolbar Panel Items (TableSection)
* Rendered separately, then moved into the Toolbar tab via JS.
* ============================================================ */
var ts = m.section(form.TableSection, 'toolbar_item',
_('Toolbar Panel Items'),
_('Quick-access links displayed in the right-side toolbar panel on every page. Items are rendered in ascending Order number. Use the Add button below to create new entries.'));
ts.addremove = true;
ts.anonymous = true;
ts.sortable = false;
ts.nodescriptions = true;
o = ts.option(form.Flag, 'enabled', _('Enable'));
o.default = '1';
o.editable = true;
o = ts.option(form.Value, 'title', _('Label'));
o.placeholder = 'Home';
o.rmempty = false;
o.editable = true;
o = ts.option(form.Value, 'url', _('URL / Path'));
o.placeholder = '/cgi-bin/luci/admin/status/overview';
o.rmempty = false;
o.editable = true;
o = ts.option(form.ListValue, 'icon', _('Icon'));
[
['home.png', _('Home')],
['signal.png', _('Signal / Status')],
['cell.png', _('Cell / Modem')],
['sms.png', _('SMS')],
['network.png', _('Network / Data Usage')],
['nas.png', _('NAS / Storage')],
['wifi.png', _('WiFi / Wireless')],
['firewall.png', _('Firewall')],
['settings.png', _('Settings / System')],
['terminal.png', _('Terminal')],
['vpn.png', _('VPN')],
['files.png', _('Files')],
['info.png', _('Info / Log')]
].forEach(function (v) { o.value(v[0], v[1]); });
o.editable = true;
o = ts.option(form.Value, 'order',
_('Order'),
_('Lower numbers appear first in the toolbar.'));
o.datatype = 'uinteger';
o.placeholder = '10';
o.editable = true;
return m.render().then(function (node) {
/* ---- Wire wallpaper buttons ---- */
var uploadBtn = node.querySelector('#noobwrt-wp-upload');
var revertBtn = node.querySelector('#noobwrt-wp-revert');
var wpImg = node.querySelector('#noobwrt-wp-img');
var wpPh = node.querySelector('#noobwrt-wp-placeholder');
var wpSt = node.querySelector('#noobwrt-wp-status');
function setStatus(msg, ok) {
wpSt.textContent = msg;
wpSt.style.color = ok === true ? '#2dce89' : ok === false ? '#f5365c' : '#888';
}
fetch(BGURL, { method: 'HEAD' })
.then(function (r) {
if (r.ok) { wpImg.src = BGURL + '?_=' + Date.now(); wpImg.style.display = ''; wpPh.style.display = 'none'; }
}).catch(function () {});
uploadBtn.addEventListener('click', function (ev) {
ui.uploadFile(BGTMP, ev.target)
.then(function () { return callSetCustomWallpaper(); })
.then(function (result) {
if (result === 0) {
wpImg.src = BGURL + '?_=' + Date.now(); wpImg.style.display = ''; wpPh.style.display = 'none';
setStatus(_('Wallpaper uploaded! Refresh the login page to see it.'), true);
} else {
setStatus(_('Failed to save wallpaper.'), false);
}
})
.catch(function (e) { setStatus(_('Upload error: ') + e.message, false); });
});
revertBtn.addEventListener('click', function () {
if (!window.confirm(_('Remove custom wallpaper and revert to default?'))) return;
callRemoveCustomWallpaper()
.then(function () {
wpImg.src = ''; wpImg.style.display = 'none'; wpPh.style.display = '';
setStatus(_('Reverted to default wallpaper.'), true);
})
.catch(function (e) { setStatus(_('Could not remove: ') + e.message, false); });
});
/* ---- Move TableSection into Toolbar tab ---- */
var tabContent = node.querySelector('#noobwrt-toolbar-tab-content');
var sections = node.querySelectorAll('.cbi-map > .cbi-section');
var tsNode = sections[sections.length - 1];
if (tabContent && tsNode) {
var heading = tsNode.querySelector('h3, .cbi-section-legend');
if (heading) heading.style.display = 'none';
tabContent.appendChild(tsNode);
}
return node;
});
}
});
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+46
View File
@@ -0,0 +1,46 @@
// out: false
// ==========================================================================
// CSS Custom Properties for Font Management
// ==========================================================================
@font-face {
font-family: 'Red Hat Display';
src: url('../fonts/RedHatDisplay-Variable.woff2') format('woff2 supports variations'),
url('../fonts/RedHatDisplay-Variable.woff2') format('woff2');
font-weight: 300 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Red Hat Display';
src: url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2 supports variations'),
url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2');
font-weight: 300 900;
font-style: italic;
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: 'noobwrt';
src: url('../fonts/noobwrt.woff2') format('woff2'),
url('../fonts/noobwrt.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
+630
View File
@@ -0,0 +1,630 @@
// 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: 'noobwrt' !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: 'noobwrt' !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: 'noobwrt' !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: hidden at rest, appears on hover
&::-webkit-scrollbar {
width: 4px;
height: 1px;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
transition: background-color 0.2s;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.18);
}
// Firefox scrollbar
scrollbar-width: thin;
scrollbar-color: transparent transparent;
&:hover {
scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
}
}
// 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. Sticky Table Headers
// ==========================================================================
.tr.table-titles,
.tr.cbi-section-table-titles {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--background-color);
}
// 7. 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;
}
}
+407
View File
@@ -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
+245
View File
@@ -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;
}
+641
View File
@@ -0,0 +1,641 @@
// out: false
// ==========================================================================
// Responsive Styles
// Mobile-first responsive design for LuCI admin interface
// ==========================================================================
// 1. 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: 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: 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: 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;
visibility: visible !important;
width: auto !important;
margin: 0 !important;
&::before {
font-family: 'noobwrt' !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%;
padding: 1.5rem 1rem;
.login-form {
max-width: 22rem;
.login-card {
padding: 0;
&.split {
grid-template-columns: 1fr;
}
.login-hero {
padding: 2rem 1.5rem 1.2rem;
.hero-orbit {
display: none;
}
.hero-particles {
span {
width: 6px;
height: 6px;
box-shadow: none;
}
span:nth-child(2),
span:nth-child(4),
span:nth-child(12) {
width: 8px;
height: 8px;
}
}
.hero-icon {
width: 7.5rem;
height: 7.5rem;
border-radius: 50%;
img {
width: 5.1rem;
height: 5.1rem;
}
}
.hero-brand {
max-width: 9rem;
}
}
.login-panel {
padding: 1.8rem 1.5rem 1.5rem;
}
}
.form-login {
.input-group {
&::before {
color: var(--primary);
}
}
}
}
}
}
// 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);
}
}
}
+452
View File
@@ -0,0 +1,452 @@
// 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: 'noobwrt' !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 {
position: relative;
display: flex;
min-height: 100vh;
flex-direction: column;
font-family: 'Google Sans', 'Segoe UI', Tahoma, sans-serif;
color: var(--default);
&::before {
content: none;
}
// Background video container
.video {
.absolute-full();
.flex-center();
background-color: var(--darker);
overflow: hidden;
z-index: 0;
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;
z-index: 0;
}
// Login container
.login-container {
z-index: 2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 2rem 1.5rem;
// Login form
.login-form {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 46rem;
.login-card {
width: 100%;
padding: 0;
border-radius: 1.5rem;
background: rgba(255, 255, 255, 0.98);
border: 1px solid rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px) saturate(120%);
box-shadow: 0 30px 80px rgba(24, 14, 84, 0.45);
overflow: hidden;
&.split {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 22rem;
}
.login-hero {
position: relative;
padding: 2.5rem;
background: linear-gradient(135deg, rgba(244, 245, 255, 0.95), rgba(233, 236, 255, 0.95));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
.hero-particles {
position: absolute;
inset: 0;
pointer-events: none;
span {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(109, 93, 246, 0.38);
box-shadow: 0 0 18px rgba(109, 93, 246, 0.4);
animation: floaty 7s ease-in-out infinite;
}
span:nth-child(1) {
top: 18%;
left: 18%;
width: 6px;
height: 6px;
animation-delay: 0s;
}
span:nth-child(2) {
top: 26%;
right: 22%;
width: 12px;
height: 12px;
animation-delay: 1s;
}
span:nth-child(3) {
bottom: 30%;
left: 28%;
width: 7px;
height: 7px;
animation-delay: 2s;
}
span:nth-child(4) {
bottom: 20%;
right: 30%;
width: 9px;
height: 9px;
animation-delay: 1.5s;
}
span:nth-child(5) {
top: 55%;
left: 12%;
width: 5px;
height: 5px;
animation-delay: 2.8s;
}
span:nth-child(6) {
top: 60%;
right: 15%;
width: 6px;
height: 6px;
animation-delay: 3.2s;
}
span:nth-child(7) {
top: 10%;
right: 45%;
width: 4px;
height: 4px;
animation-delay: 1.8s;
}
span:nth-child(8) {
bottom: 12%;
left: 45%;
width: 8px;
height: 8px;
animation-delay: 2.4s;
}
span:nth-child(9) {
top: 42%;
right: 8%;
width: 5px;
height: 5px;
animation-delay: 3.6s;
}
span:nth-child(10) {
top: 70%;
left: 6%;
width: 7px;
height: 7px;
animation-delay: 4.2s;
}
span:nth-child(11) {
top: 34%;
left: 60%;
width: 6px;
height: 6px;
animation-delay: 1.2s;
}
span:nth-child(12) {
bottom: 40%;
right: 40%;
width: 10px;
height: 10px;
animation-delay: 2.9s;
}
}
.hero-icon {
width: 9.5rem;
height: 9.5rem;
border-radius: 50%;
background: rgba(109, 93, 246, 0.08);
border: 1px solid rgba(109, 93, 246, 0.25);
box-shadow: 0 0 28px rgba(109, 93, 246, 0.25), inset 0 0 18px rgba(109, 93, 246, 0.12);
display: flex;
align-items: center;
justify-content: center;
img {
width: 6.3rem;
height: 6.3rem;
}
}
.hero-text {
text-align: center;
}
.hero-brand {
max-width: 12rem;
height: auto;
display: inline-block;
}
}
.login-panel {
padding: 2.5rem 2.5rem 2.25rem;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.brand-text-only {
text-align: center;
font-size: 1.5rem;
font-weight: 800;
font-family: "Google Sans";
color: #1f2a44;
letter-spacing: 0.02rem;
margin: 0 auto 0.3rem;
}
.login-subtitle {
text-align: center;
color: #7b849c;
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 1.5rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
white-space: nowrap;
}
// Form container
.form-login {
width: 100%;
padding: 0;
box-sizing: border-box;
.errorbox {
margin-bottom: 1.25rem;
padding: 0.75rem 1rem;
border-radius: 0.9rem;
text-align: center;
color: #b42318;
background: rgba(255, 87, 87, 0.12);
border: 1px solid rgba(255, 87, 87, 0.3);
}
// Input group styling
.input-group {
position: relative;
margin-bottom: 1.1rem;
display: flex;
align-items: center;
// Icon styling
&::before {
.icon-base();
position: absolute;
top: 50%;
left: 1.1rem;
z-index: 100;
font-size: 1.25rem;
color: #6b7280;
transform: translateY(-50%);
}
// Input field
input {
display: block;
width: 100%;
padding: 0.9rem 1rem 0.9rem 3.2rem;
font-size: 0.95rem;
line-height: 1.5em;
color: #1f2937;
background-color: #f6f7fb;
background-clip: padding-box;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 999px;
outline: none;
box-sizing: border-box;
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06), 0 8px 18px rgba(15, 23, 42, 0.08);
transition: all 0.25s ease;
&:focus {
border-color: rgba(109, 93, 246, 0.6);
background-color: #ffffff;
box-shadow: 0 0 0 4px rgba(109, 93, 246, 0.15);
}
&::placeholder {
color: #8b93a7;
letter-spacing: 0.01rem;
}
}
// Icon content
&.user-icon::before {
content: "\e971";
}
&.pass-icon::before {
content: "\e910";
}
}
}
// Submit button
.cbi-button-apply {
width: 100% !important;
min-height: 3rem;
margin: 1.2rem 0 0.2rem;
padding: 0.85rem 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--white);
text-align: center;
letter-spacing: 0.08rem;
background: linear-gradient(135deg, #6d5df6, #4d8bff) !important;
border: none;
border-radius: 0.9rem;
outline: none;
cursor: pointer;
box-shadow: 0 16px 30px rgba(84, 88, 245, 0.35);
transition: all 0.3s ease !important;
&:hover,
&:focus {
transform: translateY(-1px);
box-shadow: 0 22px 40px rgba(84, 88, 245, 0.45);
}
}
}
// 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;
}
}
}
}
@keyframes floaty {
0% {
transform: translateY(0) scale(1);
opacity: 0.45;
}
50% {
transform: translateY(-16px) scale(1.2);
opacity: 1;
}
100% {
transform: translateY(0) scale(1);
opacity: 0.45;
}
}
File diff suppressed because it is too large. Load diff
+6
View File
@@ -0,0 +1,6 @@
{
"devDependencies": {
"less": "^4.6.4",
"ttf2woff2": "^8.0.1"
}
}
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,221 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "NoobWrt Theme"
msgstr "NoobWrt 主题设置"
msgid "NoobWrt Theme Settings"
msgstr "NoobWrt 主题设置"
msgid "Customize the appearance, wallpaper, background effects, and toolbar panel of the NoobWrt LuCI theme."
msgstr "自定义 NoobWrt LuCI 主题的外观、壁纸、背景效果和工具栏面板。"
msgid "Appearance"
msgstr "外观"
msgid "Default Theme Mode"
msgstr "默认主题模式"
msgid "Theme applied on page load when Automatic mode is disabled. Users can also toggle it manually using the toolbar sun/moon button."
msgstr "禁用自动模式时页面加载应用的主题模式。用户也可通过工具栏日/月按钮手动切换。"
msgid "Light"
msgstr "亮色"
msgid "Dark"
msgstr "暗色"
msgid "Automatic Light / Dark Mode"
msgstr "自动亮色/暗色模式"
msgid "Automatically switch to Dark mode from 6 PM to 6 AM and Light mode from 6 AM to 6 PM. When enabled, the schedule overrides the manual toolbar toggle."
msgstr "自动在 18:00 至 06:00 切换为暗色模式,06:00 至 18:00 切换为亮色模式。启用后,定时计划将覆盖工具栏手动切换。"
msgid "Accent Color — Light Mode"
msgstr "主题色 — 亮色模式"
msgid "Primary accent color used throughout the light theme."
msgstr "整个亮色主题使用的主要强调色。"
msgid "Accent Color — Dark Mode"
msgstr "主题色 — 暗色模式"
msgid "Primary accent color used throughout the dark theme."
msgstr "整个暗色主题使用的主要强调色。"
msgid "Wallpaper"
msgstr "壁纸"
msgid "Online Wallpaper Source"
msgstr "在线壁纸来源"
msgid "Automatically fetch a wallpaper from an online source for the login page background. Set to None to use a custom uploaded image or the built-in default."
msgstr "自动从在线来源获取登录页背景壁纸。设为"无"可使用自定义上传图片或内置默认壁纸。"
msgid "None (use custom / default)"
msgstr "无(使用自定义/默认)"
msgid "Bing Daily Photo"
msgstr "必应每日图片"
msgid "Lorem Picsum (Random)"
msgstr "随机图片(Lorem Picsum)"
msgid "Wikimedia Picture of the Day"
msgstr "维基媒体每日图片"
msgid "Background Effects"
msgstr "背景效果"
msgid "Blur Radius — Light Mode"
msgstr "模糊半径 — 亮色模式"
msgid "Login page background blur radius in pixels for light mode. Default: 10"
msgstr "亮色模式下登录页背景模糊半径(像素)。默认:10"
msgid "Blur Radius — Dark Mode"
msgstr "模糊半径 — 暗色模式"
msgid "Login page background blur radius in pixels for dark mode. Default: 10"
msgstr "暗色模式下登录页背景模糊半径(像素)。默认:10"
msgid "Background Opacity — Light Mode"
msgstr "背景透明度 — 亮色模式"
msgid "Overlay opacity on the login page background for light mode. Range: 0.0 (transparent) to 1.0 (opaque). Default: 0.8"
msgstr "亮色模式下登录页背景遮罩透明度。范围:0.0(透明)至 1.0(不透明)。默认:0.8"
msgid "Background Opacity — Dark Mode"
msgstr "背景透明度 — 暗色模式"
msgid "Overlay opacity on the login page background for dark mode. Default: 0.8"
msgstr "暗色模式下登录页背景遮罩透明度。默认:0.8"
msgid "Toolbar Panel"
msgstr "工具栏面板"
msgid "Toolbar Panel Items"
msgstr "工具栏面板项目"
msgid "Upload a custom image to use as the login page background. Supported formats: JPG, PNG, WebP (max 10 MB)."
msgstr "上传自定义图片作为登录页背景。支持格式:JPG、PNG、WebP(最大 10 MB)。"
msgid "No custom wallpaper — using default"
msgstr "未设置自定义壁纸 — 使用默认"
msgid "Upload Wallpaper..."
msgstr "上传壁纸..."
msgid "Revert to Default"
msgstr "恢复默认"
msgid "Quick-access links displayed in the right-side toolbar panel on every page. Items are rendered in ascending Order number. Use the Add button below to create new entries."
msgstr "显示在每个页面右侧工具栏面板中的快捷链接。按排序数字升序显示。点击下方添加按钮创建新条目。"
msgid "Quick-access links displayed in the right-side toolbar panel on every page. "
msgstr "显示在每个页面右侧工具栏面板中的快捷链接。"
msgid "Enable"
msgstr "启用"
msgid "Label"
msgstr "标签"
msgid "URL / Path"
msgstr "URL / 路径"
msgid "Icon"
msgstr "图标"
msgid "Home"
msgstr "主页"
msgid "Signal / Status"
msgstr "信号 / 状态"
msgid "Cell / Modem"
msgstr "蜂窝 / 调制解调器"
msgid "SMS"
msgstr "短信"
msgid "Network / Data Usage"
msgstr "网络 / 流量"
msgid "NAS / Storage"
msgstr "NAS / 存储"
msgid "WiFi / Wireless"
msgstr "WiFi / 无线"
msgid "Firewall"
msgstr "防火墙"
msgid "Settings / System"
msgstr "设置 / 系统"
msgid "Terminal"
msgstr "终端"
msgid "VPN"
msgstr "VPN"
msgid "Files"
msgstr "文件"
msgid "Info / Log"
msgstr "信息 / 日志"
msgid "Order"
msgstr "排序"
msgid "Lower numbers appear first in the toolbar."
msgstr "数字越小越靠前显示。"
msgid "About"
msgstr "关于"
msgid "A clean modern LuCI theme for OpenWrt routers."
msgstr "一款简洁现代的 OpenWrt LuCI 主题。"
msgid "Developer"
msgstr "开发者"
msgid "Repository"
msgstr "代码仓库"
msgid "Contributors"
msgstr "贡献者"
msgid "View on GitHub"
msgstr "在 GitHub 上查看"
msgid "License"
msgstr "许可证"
msgid "Issues / Support"
msgstr "问题 / 支持"
msgid "Open an issue"
msgstr "提交 Issue"
msgid "Made with \u2665 for the OpenWrt community"
msgstr "用 \u2665 为 OpenWrt 社区制作"
msgid "Wallpaper uploaded! Refresh the login page to see it."
msgstr "壁纸已上传!刷新登录页面即可查看。"
msgid "Failed to save wallpaper."
msgstr "壁纸保存失败。"
msgid "Upload error: "
msgstr "上传错误:"
msgid "Remove custom wallpaper and revert to default?"
msgstr "删除自定义壁纸并恢复默认?"
msgid "Reverted to default wallpaper."
msgstr "已恢复默认壁纸。"
msgid "Could not remove: "
msgstr "无法删除:"
@@ -0,0 +1,67 @@
#!/bin/sh
# Register NoobWrt as a LuCI theme (skip on upgrade)
if [ "$PKG_UPGRADE" != 1 ]; then
uci get luci.themes.NoobWrt >/dev/null 2>&1 || \
uci batch <<-EOF
set luci.themes.NoobWrt=/luci-static/noobwrt
set luci.main.mediaurlbase=/luci-static/noobwrt
commit luci
EOF
fi
# Ensure the config file exists
touch /etc/config/noobwrt
# Initialise global section type (only if section absent)
uci get noobwrt.global >/dev/null 2>&1 || uci set noobwrt.global=global
# Set each global option only if not already present
uci get noobwrt.global.mode >/dev/null 2>&1 || uci set noobwrt.global.mode=normal
uci get noobwrt.global.primary >/dev/null 2>&1 || uci set noobwrt.global.primary='#7689f4'
uci get noobwrt.global.dark_primary >/dev/null 2>&1 || uci set noobwrt.global.dark_primary='#7c8ff5'
uci get noobwrt.global.blur >/dev/null 2>&1 || uci set noobwrt.global.blur=10
uci get noobwrt.global.blur_dark >/dev/null 2>&1 || uci set noobwrt.global.blur_dark=10
uci get noobwrt.global.transparency >/dev/null 2>&1 || uci set noobwrt.global.transparency=0.8
uci get noobwrt.global.transparency_dark >/dev/null 2>&1 || uci set noobwrt.global.transparency_dark=0.8
uci get noobwrt.global.online_wallpaper >/dev/null 2>&1 || uci set noobwrt.global.online_wallpaper=none
uci get noobwrt.global.use_exact_resolution >/dev/null 2>&1 || uci set noobwrt.global.use_exact_resolution=1
uci get noobwrt.global.auto_dark_mode >/dev/null 2>&1 || uci set noobwrt.global.auto_dark_mode=0
uci commit noobwrt
# Helper: seed a toolbar_item section and its fields if missing
seed_toolbar() {
local name="$1" title="$2" url="$3" icon="$4" enabled="$5" order="$6"
uci get noobwrt."$name" >/dev/null 2>&1 || uci set noobwrt."$name"=toolbar_item
uci get noobwrt."$name".title >/dev/null 2>&1 || uci set noobwrt."$name".title="$title"
uci get noobwrt."$name".url >/dev/null 2>&1 || uci set noobwrt."$name".url="$url"
uci get noobwrt."$name".icon >/dev/null 2>&1 || uci set noobwrt."$name".icon="$icon"
uci get noobwrt."$name".enabled >/dev/null 2>&1 || uci set noobwrt."$name".enabled="$enabled"
uci get noobwrt."$name".order >/dev/null 2>&1 || uci set noobwrt."$name".order="$order"
}
seed_toolbar toolbar_home 'Home' '/cgi-bin/luci/admin/status/overview' home.png 1 1
seed_toolbar toolbar_status 'Status' '/cgi-bin/luci/admin/modem/modemdata' signal.png 1 2
seed_toolbar toolbar_modem 'Modem' '/cgi-bin/luci/admin/modem/qmodem' cell.png 0 3
seed_toolbar toolbar_sms 'SMS' '/cgi-bin/luci/admin/modem/luci-app-sms-tool-js' sms.png 1 4
seed_toolbar toolbar_data 'Data Usage' '/cgi-bin/luci/admin/tools/netstat' network.png 1 5
seed_toolbar toolbar_nas 'NAS' '/cgi-bin/luci/admin/services/samba4' nas.png 0 6
seed_toolbar toolbar_wireless 'Wireless' '/cgi-bin/luci/admin/network/wireless' wifi.png 1 7
seed_toolbar toolbar_firewall 'Firewall' '/cgi-bin/luci/admin/network/firewall' firewall.png 0 8
seed_toolbar toolbar_log 'System Log' '/cgi-bin/luci/admin/status/logs' info.png 0 9
seed_toolbar toolbar_terminal 'Terminal' '/cgi-bin/luci/admin/system/ttyd' terminal.png 1 10
seed_toolbar toolbar_vpn 'VPN' '/cgi-bin/luci/admin/services/passwall' vpn.png 0 11
seed_toolbar toolbar_files 'Files' '/cgi-bin/luci/admin/nas/tinyfilemanager' files.png 1 12
seed_toolbar toolbar_system 'Settings' '/cgi-bin/luci/admin/system/noobwrt' settings.png 1 13
uci commit noobwrt
# Ensure the rpcd wallpaper script is executable so ubus registers it
chmod +x /usr/libexec/rpcd/luci.noobwrt_wallpaper 2>/dev/null
# Reload rpcd so luci.noobwrt_wallpaper is registered on first install
# (only needed when rpcd is already running; harmless if it isn't yet)
/etc/init.d/rpcd reload 2>/dev/null
exit 0
@@ -0,0 +1,136 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CUSTOM_BG="/www/luci-static/noobwrt/background/custom.jpg"
UPLOAD_TMP="/tmp/noobwrt_wallpaper.tmp"
# Online wallpaper source (bing/picsum/wikimedia or none) — no API key required
WEB_PIC_SRC="$(uci -q get noobwrt.@global[0].online_wallpaper || echo 'bing')"
# Normalise legacy / unknown values to 'bing' so the case always matches
case "$WEB_PIC_SRC" in
bing|picsum|wikimedia|none) ;;
*) WEB_PIC_SRC="bing" ;;
esac
CACHE="/var/run/noobwrt_${WEB_PIC_SRC}.url"
WRLOCK="/var/lock/noobwrt_${WEB_PIC_SRC}.lock"
fetch_pic_url() {
case "$WEB_PIC_SRC" in
bing)
local picpath="$(wget -T5 -qO- \
"https://www.bing.com/HPImageArchive.aspx?format=js&n=1" |
jsonfilter -qe '@.images[0].url' |
sed 's/1920x1080/UHD/g')"
[ -n "${picpath}" ] && echo "https://www.bing.com${picpath}"
;;
picsum)
# Lorem Picsum — free random photo, no API key needed
# Follow the 302 redirect and cache the direct CDN image URL
local seed="$(($(date '+%s') % 1000))"
local cdnurl="$(wget -T5 -S -O /dev/null \
"https://picsum.photos/seed/${seed}/1920/1080" 2>&1 |
grep -i 'location:' | head -n1 |
sed 's/.*[Ll]ocation: *//' | tr -d '\r\n')"
if [ -n "$cdnurl" ]; then
echo "$cdnurl"
else
echo "https://picsum.photos/seed/${seed}/1920/1080"
fi
;;
wikimedia)
# Wikimedia Commons Picture of the Day — free, no API key needed
# Fetch the featured image URL from the Wikimedia REST API
local today="$(date '+%Y/%m/%d')"
wget -T5 -qO- \
"https://api.wikimedia.org/feed/v1/wikipedia/en/featured/${today}" |
jsonfilter -qe '@.image.image.source' 2>/dev/null
;;
esac
}
try_update() {
local lock="$WRLOCK"
exec 200>"$lock"
if flock -n 200 >/dev/null 2>&1; then
local picurl="$(fetch_pic_url)"
if [ -n "$picurl" ]; then
echo "$picurl" | tee "$CACHE"
else
if [ -s "$CACHE" ]; then
cat "$CACHE"
else
touch "$CACHE"
fi
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_add_object "set_custom"
json_close_object
json_add_object "remove_custom"
json_close_object
json_dump
json_cleanup
;;
"call")
case "$2" in
"set_custom")
if [ -f "$UPLOAD_TMP" ]; then
mv "$UPLOAD_TMP" "$CUSTOM_BG" && chmod 0644 "$CUSTOM_BG"
echo '{ "result": 0 }'
else
echo '{ "result": 1, "error": "tmp file not found" }'
fi
;;
"remove_custom")
rm -f "$CUSTOM_BG"
echo '{ "result": 0 }'
;;
"get_url")
read -r input
if [ "$WEB_PIC_SRC" = "none" ]; then
echo '{ "url": "" }'
exit 0
fi
if [ -f "$CACHE" ]; then
idle_t="$(($(date '+%s') - $(date -r "$CACHE" '+%s' 2>/dev/null || echo '0')))"
if [ -s "$CACHE" ]; then
if [ "$idle_t" -le 43200 ]; then
json_init
json_add_string "url" "$(cat "$CACHE")"
json_dump
json_cleanup
exit 0
fi
else
# Empty cache file means a fetch is in progress or recently
# failed — suppress retry for 120 s to avoid hammering the API
if [ "$idle_t" -le 120 ]; then
echo '{ "url": "" }'
exit 0
fi
fi
fi
json_init
json_add_string "url" "$(try_update)"
json_dump
json_cleanup
exit 0
;;
esac
;;
esac
@@ -0,0 +1,10 @@
{
"admin/system/noobwrt": {
"title": "NoobWrt Theme",
"order": 80,
"action": {
"type": "view",
"path": "noobwrt/settings"
}
}
}
@@ -0,0 +1,21 @@
{
"luci-theme-noobwrt": {
"description": "Grant UCI read/write access and wallpaper upload for luci-theme-noobwrt",
"read": {
"uci": [ "noobwrt" ],
"file": {
"/www/luci-static/noobwrt/background/*": [ "read" ]
}
},
"write": {
"uci": [ "noobwrt" ],
"cgi-io": [ "upload" ],
"file": {
"/tmp/noobwrt_wallpaper.tmp": [ "write" ]
},
"ubus": {
"luci.noobwrt_wallpaper": [ "set_custom", "remove_custom" ]
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

@@ -0,0 +1,45 @@
{#
NoobWrt - A clean modern LuCI theme for OpenWrt routers.
https://github.com/nooblk-98/luci-theme-noobwrt
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/nooblk-98/luci-theme-noobwrt" target="_blank">NoobWrtTheme {# 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>
{% if (ctx.authsession): %}
{% include("themes/" + theme + "/toolbar") %}
{% endif %}
<script>
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-noobwrt')</script>
</body>
</html>
@@ -0,0 +1,34 @@
{#
NoobWrt - A clean modern LuCI theme for OpenWrt routers.
https://github.com/nooblk-98/luci-theme-noobwrt
Licensed to the public under the Apache License 2.0
-#}
</div>
<footer>
<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/nooblk-98/luci-theme-noobwrt" target="_blank">NoobWrt Theme</a>
</div>
</footer>
</div>
</div>
<script>
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,169 @@
{#
NoobWrt - A clean modern LuCI theme for OpenWrt routers.
https://github.com/nooblk-98/luci-theme-noobwrt
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 — defaults match uci-defaults seed values
let primary = '#5e72e4';
let dark_primary = '#7c8ff5';
let blur_radius = '10';
let blur_radius_dark = '10';
let blur_opacity = '0.8';
let blur_opacity_dark = '0.8';
let mode = 'normal';
const dark_css = readfile('/www/luci-static/noobwrt/css/dark.css');
if (access('/etc/config/noobwrt')) {
primary = cfg.get_first('noobwrt', 'global', 'primary') ?? primary;
dark_primary = cfg.get_first('noobwrt', 'global', 'dark_primary') ?? dark_primary;
blur_radius = cfg.get_first('noobwrt', 'global', 'blur') ?? blur_radius;
blur_radius_dark = cfg.get_first('noobwrt', 'global', 'blur_dark') ?? blur_radius_dark;
blur_opacity = cfg.get_first('noobwrt', 'global', 'transparency') ?? blur_opacity;
blur_opacity_dark = cfg.get_first('noobwrt', 'global', 'transparency_dark') ?? blur_opacity_dark;
mode = cfg.get_first('noobwrt', 'global', 'mode') ?? mode;
}
const 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={{ version.luciversion }}">
{% if (mode == 'dark'): %}
<style id="dark-theme-style">{{ dark_css }}</style>
{% endif -%}
<style>
: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 }};
}
</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 }} {% if (ctx.authsession): %}logged-in{% endif %}{% if (mode == 'dark'): %} server-dark dark-theme{% 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" href="#"></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,107 @@
{#
NoobWrt - A clean modern LuCI theme for OpenWrt routers.
https://github.com/nooblk-98/luci-theme-noobwrt
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/noobwrt/css/dark.css');
if (access('/etc/config/noobwrt')) {
primary = cfg.get_first('noobwrt', 'global', 'primary');
dark_primary = cfg.get_first('noobwrt', 'global', 'dark_primary');
blur_radius = cfg.get_first('noobwrt', 'global', 'blur');
blur_radius_dark = cfg.get_first('noobwrt', 'global', 'blur_dark');
blur_opacity = cfg.get_first('noobwrt', 'global', 'transparency');
blur_opacity_dark = cfg.get_first('noobwrt', 'global', 'transparency_dark');
mode = cfg.get_first('noobwrt', '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={{ version.luciversion }}">
<style title="text/css">
{% if (mode == 'dark'): %}
{{ dark_css }}
{% endif -%}
{% if (access('/etc/config/noobwrt')): %}
: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>
{% 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,158 @@
{#
NoobWrt - A clean modern LuCI theme for OpenWrt routers.
https://github.com/nooblk-98/luci-theme-noobwrt
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 parts = split(f, '.');
let ext = lc(parts?.[length(parts) - 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 backgroundType = "Image";
let mimeType = "";
if (fs.access("/etc/config/noobwrt")) {
let online_wallpaper = cfg.get_first('noobwrt', 'global', 'online_wallpaper') ?? (cfg.get_first('noobwrt', 'global', 'bing_background') == '1' ? 'bing' : null);
if (online_wallpaper && online_wallpaper != "none") {
const picurl = ubus.call("luci.noobwrt_wallpaper", "get_url") ?? {};
if (picurl?.url) {
bgUrl = picurl.url;
return {bgUrl, backgroundType, mimeType};
}
}
}
let background = fetchMedia("/www" + themeDir, themeDir);
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, 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>
<!-- Image Background End -->
{% endif %}
<!-- Login Container Start -->
<div class="login-container">
<div class="login-form">
<div class="login-card split">
<div class="login-hero">
<div class="hero-particles">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="hero-icon">
<img src="{{ media }}/img/logo.png" alt="router">
</div>
<div class="hero-text">
<img class="hero-brand" src="{{ media }}/img/brand.png" alt="{{ hostname }}">
</div>
</div>
<div class="login-panel">
<!-- Title Start -->
<div class="brand-text-only">{{ hostname }}</div>
<div class="login-subtitle">{{ _('Sign in to manage your router') }}</div>
<!-- Title 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) }}" placeholder="{{ _('Username') }}" />
<label class="sr-only" for="cbi-input-user">{{ _('Username') }}</label>
</div>
<div class="input-group pass-icon">
<input class="cbi-input-password" id="cbi-input-password" type="password" name="luci_password" placeholder="{{ _('Password') }}" />
<label class="sr-only" for="cbi-input-password">{{ _('Password') }}</label>
</div>
</div>
<div class="form-actions">
<input type="submit" value="{{ _('Log in') }}" class="cbi-button cbi-button-apply" />
</div>
</form>
</div>
</div>
<!-- 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") %}
@@ -0,0 +1,486 @@
{#
NoobWrt Theme - Compact Toolbar
Toolbar panel items are loaded dynamically from /etc/config/noobwrt
Copyright 2025 nooblk-98
-#}
{%
import { cursor } from 'uci';
import { access } from 'fs';
/* Default toolbar items — match uci-defaults seed values */
const default_toolbar = [
{ title: 'Home', url: '/cgi-bin/luci/admin/status/overview', icon: 'home.png', enabled: true, order: 1 },
{ title: 'Status', url: '/cgi-bin/luci/admin/modem/modemdata', icon: 'signal.png', enabled: true, order: 2 },
{ title: 'Modem', url: '/cgi-bin/luci/admin/modem/qmodem', icon: 'cell.png', enabled: false, order: 3 },
{ title: 'SMS', url: '/cgi-bin/luci/admin/modem/luci-app-sms-tool-js', icon: 'sms.png', enabled: true, order: 4 },
{ title: 'Data Usage', url: '/cgi-bin/luci/admin/tools/netstat', icon: 'network.png', enabled: true, order: 5 },
{ title: 'NAS', url: '/cgi-bin/luci/admin/services/samba4', icon: 'nas.png', enabled: false, order: 6 },
{ title: 'Wireless', url: '/cgi-bin/luci/admin/network/wireless', icon: 'wifi.png', enabled: true, order: 7 },
{ title: 'Firewall', url: '/cgi-bin/luci/admin/network/firewall', icon: 'firewall.png', enabled: false, order: 8 },
{ title: 'System Log', url: '/cgi-bin/luci/admin/status/logs', icon: 'info.png', enabled: false, order: 9 },
{ title: 'Terminal', url: '/cgi-bin/luci/admin/system/ttyd', icon: 'terminal.png', enabled: true, order: 10 },
{ title: 'VPN', url: '/cgi-bin/luci/admin/services/passwall', icon: 'vpn.png', enabled: false, order: 11 },
{ title: 'Files', url: '/cgi-bin/luci/admin/nas/tinyfilemanager', icon: 'files.png', enabled: true, order: 12 },
{ title: 'Settings', url: '/cgi-bin/luci/admin/system/noobwrt', icon: 'settings.png', enabled: true, order: 13 },
];
/* Build sorted list of enabled toolbar items from UCI config */
let toolbar_items = [];
let auto_dark_mode = false;
if (access('/etc/config/noobwrt')) {
const tcfg = cursor();
tcfg.foreach('noobwrt', 'toolbar_item', function(s) {
push(toolbar_items, {
title: s.title || 'Link',
url: s.url || '#',
icon: s.icon || 'home.png',
enabled: s.enabled !== '0',
order: +(s.order || 99)
});
});
sort(toolbar_items, (a, b) => a.order - b.order);
const adm = tcfg.get_first('noobwrt', 'global', 'auto_dark_mode');
auto_dark_mode = (adm !== '0');
}
if (length(toolbar_items) == 0) toolbar_items = default_toolbar;
-%}
<style>
/* ── Toolbar wrapper: sits at right edge, vertically centred ── */
.toolbar-wrapper {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 999;
display: flex;
align-items: center;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Hidden state: slide panel off-screen, leaving only the tab visible */
.toolbar-wrapper.hidden {
transform: translateY(-50%) translateX(calc(100% - 20px));
}
/* Peek state: hover while hidden — slide panel fully in temporarily */
.toolbar-wrapper.peek {
transform: translateY(-50%) translateX(0);
}
/* ── Pull tab (the ‹ / › chevron strip) ── */
.toolbar-pull-tab {
width: 20px;
min-width: 20px;
height: 52px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.85);
border-right: none;
border-radius: 8px 0 0 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
overflow: visible;
transition: background 0.2s ease, width 0.2s ease;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.12);
padding: 0;
}
.toolbar-pull-tab:hover {
background: color-mix(in srgb, var(--primary) 15%, white);
}
body.dark-theme .toolbar-pull-tab {
background: rgba(20, 20, 25, 0.98);
border-color: rgba(255, 255, 255, 0.85);
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.6);
}
body.dark-theme .toolbar-pull-tab:hover {
background: color-mix(in srgb, var(--dark-primary) 25%, rgba(20, 20, 25, 0.98));
}
.toolbar-pull-tab svg {
width: 12px;
height: 12px;
stroke: var(--primary);
flex-shrink: 0;
display: block;
transition: transform 0.35s ease, stroke 0.2s ease;
}
body.dark-theme .toolbar-pull-tab svg {
stroke: var(--dark-primary);
}
/* Hidden → chevron points right (›): rotate 180° */
.toolbar-wrapper.hidden .toolbar-pull-tab svg {
transform: rotate(180deg);
}
/* ── Main toolbar panel ── */
.toolbar-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.95);
padding: 8px;
border-radius: 12px 0 0 12px;
border: 1px solid rgba(0, 0, 0, 0.85);
border-right: none;
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.1);
transition: background 0.3s ease, box-shadow 0.3s ease;
}
body.dark-theme .toolbar-container {
background: rgba(20, 20, 25, 0.98);
border-color: rgba(255, 255, 255, 0.85);
box-shadow: -2px 0 16px rgba(0, 0, 0, 0.7);
}
.toolbar-fab {
display: none;
}
.toolbar-menu {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
}
.toolbar-item {
width: 40px;
height: 40px;
border-radius: 8px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
text-decoration: none;
padding: 0;
overflow: hidden;
position: relative;
}
.toolbar-item img {
width: 24px;
height: 24px;
object-fit: contain;
filter: opacity(0.7);
transition: filter 0.2s ease;
}
body.dark-theme .toolbar-item img {
filter: opacity(0.8) invert(1);
}
.toolbar-item:hover {
background: color-mix(in srgb, var(--primary) 12%, transparent);
}
body.dark-theme .toolbar-item:hover {
background: color-mix(in srgb, var(--dark-primary) 30%, transparent);
}
.toolbar-item:hover img {
filter: opacity(1);
}
body.dark-theme .toolbar-item:hover img {
filter: opacity(1) invert(1);
}
.toolbar-item:active {
transform: scale(0.95);
}
/* Tooltip — appears to the left since panel is on the right edge */
.toolbar-item:hover::after {
content: attr(title);
position: absolute;
right: 46px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
pointer-events: none;
z-index: 1000;
}
/* Theme Toggle */
.theme-toggle {
background: color-mix(in srgb, var(--primary) 15%, transparent);
border-radius: 8px;
margin-bottom: 4px;
transition: background 0.3s ease;
}
body.dark-theme .theme-toggle {
background: color-mix(in srgb, var(--dark-primary) 35%, transparent);
}
.theme-toggle svg {
width: 20px;
height: 20px;
stroke: var(--primary);
transition: transform 0.3s ease, stroke 0.3s ease;
}
body.dark-theme .theme-toggle svg {
stroke: var(--dark-primary);
}
.theme-toggle:hover svg {
transform: rotate(20deg);
}
@media (max-width: 768px) {
.toolbar-container {
padding: 6px;
gap: 2px;
}
.toolbar-item {
width: 36px;
height: 36px;
}
.toolbar-item img {
width: 20px;
height: 20px;
}
.toolbar-pull-tab {
height: 40px;
}
}
</style>
<div class="toolbar-wrapper" id="toolbar-wrapper">
<!-- Pull tab with chevron -->
<button class="toolbar-pull-tab" id="toolbar-pull-tab" aria-label="Toggle toolbar" title="Toggle toolbar">
<svg viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="7,2 2,8 7,14"></polyline>
</svg>
</button>
<!-- Main panel -->
<div class="toolbar-container">
<button class="toolbar-item theme-toggle" id="theme-toggle" title="Toggle Theme">
<svg class="theme-icon sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg class="theme-icon moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<div class="toolbar-menu" id="toolbar-menu">
{% if (length(toolbar_items) > 0): %}
{% for (let item in toolbar_items): %}
{% if (item.enabled): %}
<a href="{{ item.url }}" class="toolbar-item" title="{{ item.title }}">
<img src="/luci-static/noobwrt/navbar/{{ item.icon }}" alt="{{ item.title }}" loading="lazy">
</a>
{% endif %}
{% endfor %}
{% else %}
<!-- Fallback: no UCI toolbar items configured -->
<a href="/cgi-bin/luci/admin/status/overview" class="toolbar-item" title="Home">
<img src="/luci-static/noobwrt/navbar/home.png" alt="Home" loading="lazy">
</a>
<a href="/cgi-bin/luci/admin/network/wireless" class="toolbar-item" title="Wireless">
<img src="/luci-static/noobwrt/navbar/wifi.png" alt="Wireless" loading="lazy">
</a>
<a href="/cgi-bin/luci/admin/system/system" class="toolbar-item" title="System">
<img src="/luci-static/noobwrt/navbar/settings.png" alt="System" loading="lazy">
</a>
{% endif %}
</div>
</div>
</div>
<script type="text/javascript">//<![CDATA[
(function() {
const AUTO_DARK_MODE = {% if (auto_dark_mode): %}true{% else %}false{% endif %};
const themeToggle = document.getElementById('theme-toggle');
const sunIcon = themeToggle.querySelector('.sun-icon');
const moonIcon = themeToggle.querySelector('.moon-icon');
const toolbarWrapper = document.getElementById('toolbar-wrapper');
const pullTab = document.getElementById('toolbar-pull-tab');
const toolbarMenu = document.getElementById('toolbar-menu');
// Get or create dark theme style element
let darkThemeStyle = document.getElementById('dark-theme-style');
if (!darkThemeStyle) {
darkThemeStyle = document.createElement('style');
darkThemeStyle.id = 'dark-theme-style';
document.head.appendChild(darkThemeStyle);
}
const darkCssUrl = '/luci-static/noobwrt/css/dark.css';
// Returns true if the current local hour is in the dark period (18:00–05:59)
function isNightTime() {
const h = new Date().getHours();
return h >= 18 || h < 6;
}
function setIcons(dark) {
sunIcon.style.display = dark ? 'none' : 'block';
moonIcon.style.display = dark ? 'block' : 'none';
}
function loadDarkCSS(savePreference) {
if (darkThemeStyle.textContent) {
document.body.classList.add('dark-theme');
setIcons(true);
if (savePreference) localStorage.setItem('luci-theme', 'dark');
return;
}
fetch(darkCssUrl)
.then(r => r.text())
.then(css => {
darkThemeStyle.textContent = css;
document.body.classList.add('dark-theme');
setIcons(true);
if (savePreference) localStorage.setItem('luci-theme', 'dark');
})
.catch(err => {
console.error('Failed to load dark theme:', err);
document.body.classList.add('dark-theme');
setIcons(true);
if (savePreference) localStorage.setItem('luci-theme', 'dark');
});
}
function removeDarkCSS(savePreference) {
darkThemeStyle.textContent = '';
document.body.classList.remove('dark-theme');
setIcons(false);
if (savePreference) localStorage.setItem('luci-theme', 'light');
}
// Apply theme based on schedule (no localStorage side-effect)
function applyScheduledTheme() {
if (isNightTime()) {
loadDarkCSS(false);
} else {
removeDarkCSS(false);
}
}
/* ---- Initial theme application ---- */
if (AUTO_DARK_MODE) {
// Auto mode: schedule drives the theme, ignore saved manual preference
// If server already inlined dark CSS keep it and sync icons
if (document.body.classList.contains('server-dark')) {
setIcons(true);
}
applyScheduledTheme();
// Re-check every minute so the switch happens close to the hour boundary
setInterval(applyScheduledTheme, 60000);
} else {
// Manual mode — honour server-side setting first, then localStorage
if (document.body.classList.contains('server-dark')) {
localStorage.setItem('luci-theme', 'dark');
setIcons(true);
} else {
// On first visit (no saved preference), honour OS colour scheme
if (localStorage.getItem('luci-theme') === null) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
localStorage.setItem('luci-theme', 'dark');
} else {
localStorage.setItem('luci-theme', 'light');
}
}
if (localStorage.getItem('luci-theme') === 'dark') {
loadDarkCSS(true);
}
}
}
// Manual toggle — always works; in auto mode it applies until next schedule tick
themeToggle.addEventListener('click', function(e) {
e.preventDefault();
const isDark = document.body.classList.contains('dark-theme');
if (isDark) {
removeDarkCSS(!AUTO_DARK_MODE);
} else {
loadDarkCSS(!AUTO_DARK_MODE);
}
});
/* ---- Toolbar slide hide / show ---- */
function applyToolbarState(hidden) {
if (!toolbarWrapper || !pullTab) return;
toolbarWrapper.classList.toggle('hidden', hidden);
toolbarWrapper.classList.remove('peek');
pullTab.setAttribute('aria-pressed', hidden ? 'true' : 'false');
document.body.classList.toggle('toolbar-hidden', hidden);
}
function initToolbarState() {
let state = null;
try { state = localStorage.getItem('noobwrt.toolbar'); } catch (e) {}
applyToolbarState(state === 'hidden');
}
/* Click tab: permanently toggle hidden / visible */
if (pullTab) {
pullTab.addEventListener('click', function(e) {
e.preventDefault();
const isHidden = toolbarWrapper.classList.contains('hidden');
applyToolbarState(!isHidden);
try { localStorage.setItem('noobwrt.toolbar', !isHidden ? 'hidden' : 'visible'); } catch (e) {}
});
}
/* Hover peek: while hidden, mouse-enter slides panel in temporarily;
mouse-leave slides it back — but only when the user hasn't pinned it open */
var peekTimeout = null;
if (toolbarWrapper) {
toolbarWrapper.addEventListener('mouseenter', function() {
clearTimeout(peekTimeout);
if (toolbarWrapper.classList.contains('hidden')) {
toolbarWrapper.classList.add('peek');
}
});
toolbarWrapper.addEventListener('mouseleave', function() {
if (toolbarWrapper.classList.contains('hidden')) {
/* small delay so clicking items doesn't race with the hide */
peekTimeout = setTimeout(function() {
toolbarWrapper.classList.remove('peek');
}, 200);
}
});
}
initToolbarState();
})();
//]]></script>