💋 Sync 2026-10-04 04:16:09
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
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 |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 938 B |
|
After Width: | Height: | Size: 17 KiB |
|
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 |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// out: false
|
||||
/**
|
||||
Pure v3.0.0
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/pure-css/pure/blob/master/LICENSE
|
||||
*/
|
||||
/**
|
||||
normalize.css v | MIT License | https://necolas.github.io/normalize.css/
|
||||
Copyright (c) Nicolas Gallagher and Jonathan Neal
|
||||
*/
|
||||
/** normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
.pure-g {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start
|
||||
}
|
||||
|
||||
.pure-u {
|
||||
display: inline-block;
|
||||
vertical-align: top
|
||||
}
|
||||
|
||||
.pure-u-1,
|
||||
.pure-u-1-1,
|
||||
.pure-u-1-12,
|
||||
.pure-u-1-2,
|
||||
.pure-u-1-24,
|
||||
.pure-u-1-3,
|
||||
.pure-u-1-4,
|
||||
.pure-u-1-5,
|
||||
.pure-u-1-6,
|
||||
.pure-u-1-8,
|
||||
.pure-u-10-24,
|
||||
.pure-u-11-12,
|
||||
.pure-u-11-24,
|
||||
.pure-u-12-24,
|
||||
.pure-u-13-24,
|
||||
.pure-u-14-24,
|
||||
.pure-u-15-24,
|
||||
.pure-u-16-24,
|
||||
.pure-u-17-24,
|
||||
.pure-u-18-24,
|
||||
.pure-u-19-24,
|
||||
.pure-u-2-24,
|
||||
.pure-u-2-3,
|
||||
.pure-u-2-5,
|
||||
.pure-u-20-24,
|
||||
.pure-u-21-24,
|
||||
.pure-u-22-24,
|
||||
.pure-u-23-24,
|
||||
.pure-u-24-24,
|
||||
.pure-u-3-24,
|
||||
.pure-u-3-4,
|
||||
.pure-u-3-5,
|
||||
.pure-u-3-8,
|
||||
.pure-u-4-24,
|
||||
.pure-u-4-5,
|
||||
.pure-u-5-12,
|
||||
.pure-u-5-24,
|
||||
.pure-u-5-5,
|
||||
.pure-u-5-6,
|
||||
.pure-u-5-8,
|
||||
.pure-u-6-24,
|
||||
.pure-u-7-12,
|
||||
.pure-u-7-24,
|
||||
.pure-u-7-8,
|
||||
.pure-u-8-24,
|
||||
.pure-u-9-24 {
|
||||
display: inline-block;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
vertical-align: top;
|
||||
text-rendering: auto
|
||||
}
|
||||
|
||||
.pure-u-1-24 {
|
||||
width: 4.1667%
|
||||
}
|
||||
|
||||
.pure-u-1-12,
|
||||
.pure-u-2-24 {
|
||||
width: 8.3333%
|
||||
}
|
||||
|
||||
.pure-u-1-8,
|
||||
.pure-u-3-24 {
|
||||
width: 12.5%
|
||||
}
|
||||
|
||||
.pure-u-1-6,
|
||||
.pure-u-4-24 {
|
||||
width: 16.6667%
|
||||
}
|
||||
|
||||
.pure-u-1-5 {
|
||||
width: 20%
|
||||
}
|
||||
|
||||
.pure-u-5-24 {
|
||||
width: 20.8333%
|
||||
}
|
||||
|
||||
.pure-u-1-4,
|
||||
.pure-u-6-24 {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
.pure-u-7-24 {
|
||||
width: 29.1667%
|
||||
}
|
||||
|
||||
.pure-u-1-3,
|
||||
.pure-u-8-24 {
|
||||
width: 33.3333%
|
||||
}
|
||||
|
||||
.pure-u-3-8,
|
||||
.pure-u-9-24 {
|
||||
width: 37.5%
|
||||
}
|
||||
|
||||
.pure-u-2-5 {
|
||||
width: 40%
|
||||
}
|
||||
|
||||
.pure-u-10-24,
|
||||
.pure-u-5-12 {
|
||||
width: 41.6667%
|
||||
}
|
||||
|
||||
.pure-u-11-24 {
|
||||
width: 45.8333%
|
||||
}
|
||||
|
||||
.pure-u-1-2,
|
||||
.pure-u-12-24 {
|
||||
width: 50%
|
||||
}
|
||||
|
||||
.pure-u-13-24 {
|
||||
width: 54.1667%
|
||||
}
|
||||
|
||||
.pure-u-14-24,
|
||||
.pure-u-7-12 {
|
||||
width: 58.3333%
|
||||
}
|
||||
|
||||
.pure-u-3-5 {
|
||||
width: 60%
|
||||
}
|
||||
|
||||
.pure-u-15-24,
|
||||
.pure-u-5-8 {
|
||||
width: 62.5%
|
||||
}
|
||||
|
||||
.pure-u-16-24,
|
||||
.pure-u-2-3 {
|
||||
width: 66.6667%
|
||||
}
|
||||
|
||||
.pure-u-17-24 {
|
||||
width: 70.8333%
|
||||
}
|
||||
|
||||
.pure-u-18-24,
|
||||
.pure-u-3-4 {
|
||||
width: 75%
|
||||
}
|
||||
|
||||
.pure-u-19-24 {
|
||||
width: 79.1667%
|
||||
}
|
||||
|
||||
.pure-u-4-5 {
|
||||
width: 80%
|
||||
}
|
||||
|
||||
.pure-u-20-24,
|
||||
.pure-u-5-6 {
|
||||
width: 83.3333%
|
||||
}
|
||||
|
||||
.pure-u-21-24,
|
||||
.pure-u-7-8 {
|
||||
width: 87.5%
|
||||
}
|
||||
|
||||
.pure-u-11-12,
|
||||
.pure-u-22-24 {
|
||||
width: 91.6667%
|
||||
}
|
||||
|
||||
.pure-u-23-24 {
|
||||
width: 95.8333%
|
||||
}
|
||||
|
||||
.pure-u-1,
|
||||
.pure-u-1-1,
|
||||
.pure-u-24-24,
|
||||
.pure-u-5-5 {
|
||||
width: 100%
|
||||
}
|
||||
.col-1 {
|
||||
flex: 1 1 30px !important;
|
||||
}
|
||||
|
||||
.col-2 {
|
||||
flex: 2 2 60px !important;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
flex: 3 3 90px !important;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
flex: 4 4 120px !important;
|
||||
}
|
||||
|
||||
.col-5 {
|
||||
flex: 5 5 150px !important;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
flex: 6 6 180px !important;
|
||||
}
|
||||
|
||||
.col-7 {
|
||||
flex: 7 7 210px !important;
|
||||
}
|
||||
|
||||
.col-8 {
|
||||
flex: 8 8 240px !important;
|
||||
}
|
||||
|
||||
.col-9 {
|
||||
flex: 9 9 270px !important;
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
flex: 10 10 300px !important;
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"less": "^4.6.4",
|
||||
"ttf2woff2": "^8.0.1"
|
||||
}
|
||||
}
|
||||
@@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
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>
|
||||