mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 14:45:13 +08:00
💋 Sync 2026-10-04 04:16:09
This commit is contained in:
1 parent
f6b2df73ec
commit
f62992fd80
745 files changed
+212416
-1173
No files matched your search
@@ -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,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-rmm
|
||||
PKG_VERSION:=0.9.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-theme-rmm
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=4. Themes
|
||||
TITLE:=OpenWrt RMM theme for LuCI
|
||||
DEPENDS:=+luci-base +luci-theme-bootstrap
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-theme-rmm/description
|
||||
Compact dark LuCI theme styled after the OpenWrt RMM console.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-theme-rmm/install
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/rmm
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/rmm/cascade.css $(1)/www/luci-static/rmm/cascade.css
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/rmm/icons.svg $(1)/www/luci-static/rmm/icons.svg
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/rmm/navigation.js $(1)/www/luci-static/rmm/navigation.js
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/rmm/components.js $(1)/www/luci-static/rmm/components.js
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/rmm/search.js $(1)/www/luci-static/rmm/search.js
|
||||
$(INSTALL_DIR) $(1)/usr/share/ucode/luci/template/themes/rmm
|
||||
$(INSTALL_DATA) ./ucode/template/themes/rmm/*.ut $(1)/usr/share/ucode/luci/template/themes/rmm/
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./root/etc/uci-defaults/30_luci-theme-rmm $(1)/etc/uci-defaults/30_luci-theme-rmm
|
||||
$(INSTALL_DIR) $(1)/usr/share/licenses/luci-theme-rmm
|
||||
$(INSTALL_DATA) ./LICENSE $(1)/usr/share/licenses/luci-theme-rmm/LICENSE
|
||||
$(INSTALL_DATA) ./licenses/Tabler-Icons-MIT.txt $(1)/usr/share/licenses/luci-theme-rmm/Tabler-Icons-MIT.txt
|
||||
$(INSTALL_DATA) ./NOTICE.md $(1)/usr/share/licenses/luci-theme-rmm/NOTICE.md
|
||||
endef
|
||||
|
||||
define Package/luci-theme-rmm/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
if [ -f /etc/uci-defaults/30_luci-theme-rmm ]; then
|
||||
sh /etc/uci-defaults/30_luci-theme-rmm || exit $$?
|
||||
rm -f /etc/uci-defaults/30_luci-theme-rmm
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-theme-rmm/postrm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
if [ "$$(uci -q get luci.main.mediaurlbase)" = '/luci-static/rmm' ]; then
|
||||
uci set luci.main.mediaurlbase='/luci-static/bootstrap'
|
||||
fi
|
||||
uci -q delete luci.themes.RMM
|
||||
uci commit luci
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-theme-rmm))
|
||||
@@ -0,0 +1,5 @@
|
||||
# Third-party notices
|
||||
|
||||
Navigation symbols are from Tabler Icons v3.46.0 (8ac7d81), copyright 2020–2026 Paweł Kuna, licensed under MIT. The seven-symbol subset is copied from the pinned RMM sprite. See licenses/Tabler-Icons-MIT.txt.
|
||||
|
||||
LuCI templates and theme code retain their Apache-2.0 notices.
|
||||
@@ -0,0 +1,55 @@
|
||||
# RMM LuCI theme
|
||||
|
||||
`luci-theme-rmm` provides a compact dark LuCI shell matching `DESIGN.md`.
|
||||
It uses the upstream Bootstrap LuCI menu data and login module, so the official
|
||||
`luci-theme-bootstrap` package is a dependency. At 900 px and wider, the menu
|
||||
is a scrollable sidebar; below 600 px, its primary sections move to a fixed
|
||||
bottom bar with touch-sized submenus. The tablet layout keeps the top menu.
|
||||
The active page is marked in the menu, and the same LuCI links and access
|
||||
permissions are preserved. The theme does not change any network settings and
|
||||
does not become active on installation.
|
||||
|
||||
Install the package from the signed RMM package feed, then select **RMM** in
|
||||
**System → Language and Style**. To switch by SSH:
|
||||
|
||||
```sh
|
||||
uci set luci.main.mediaurlbase='/luci-static/rmm'
|
||||
uci commit luci
|
||||
```
|
||||
|
||||
Refresh LuCI and verify the login page, navigation, forms, and mobile layout.
|
||||
Check direct links and submenus with mouse, touch, and keyboard at 320, 390,
|
||||
768, 1024, and 1440 px. On mobile and tablet, Escape closes an open submenu and restores focus to its trigger. Tablet submenus open on keyboard focus; Tab and Shift+Tab follow the existing links. Click or touch also toggles a submenu. At 200% zoom,
|
||||
the bottom bar uses two rows when the CSS viewport is narrower than 300 px.
|
||||
To return to the default theme:
|
||||
|
||||
```sh
|
||||
uci set luci.main.mediaurlbase='/luci-static/bootstrap'
|
||||
uci commit luci
|
||||
```
|
||||
|
||||
The theme is based on the Apache-2.0 licensed LuCI Bootstrap templates.
|
||||
|
||||
## P2 components
|
||||
|
||||
The theme owns responsive rules based on viewport width. Bootstrap mobile.css
|
||||
is no longer imported, so phone screen size does not change the CSS cascade.
|
||||
Below 600 px, supported data tables show labelled stacked records; header
|
||||
content and sorting controls remain in a compact scroll row. Read-only tables
|
||||
have their own keyboard-focusable scroll region. Editable tables keep their
|
||||
native nodes, events and visible overflow for dropdowns/tooltips. Labels are
|
||||
refreshed when LuCI replaces rows during polling.
|
||||
|
||||
Diagnostics command groups stack on mobile and have visible destination labels.
|
||||
Normal controls use 36 px; login, mobile and coarse-pointer controls use 42 px.
|
||||
Tabs scroll locally; technical output wraps or scrolls inside its own component.
|
||||
Open dropdowns stay inside the viewport and account for the mobile navigation.
|
||||
Non-login dialogs have bounded height, accessible titles, Tab containment and
|
||||
focus restoration. Native LuCI save/cancel/Escape handlers remain responsible
|
||||
for their actions. No configuration writes or additional RPC calls are added.
|
||||
|
||||
After upgrade, perform a full browser reload and check the base LuCI entry URL,
|
||||
DHCP, interfaces, Wi-Fi, firewall, startup, DDNS, diagnostics (without running
|
||||
commands), dropdowns and a safe dialog at 320/390/768/1024/1440 px. Also check
|
||||
physical touch devices, 200% zoom and the same widgets in installed third-party
|
||||
packages. DOM tests do not replace this device validation.
|
||||
@@ -0,0 +1,521 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
@import url('/luci-static/bootstrap/cascade.css');
|
||||
|
||||
:root[data-darkmode="true"] {
|
||||
--background-color-high: #090b0d;
|
||||
--background-color-medium: #0e1113;
|
||||
--background-color-low: #131719;
|
||||
--text-color-highest: #e4e8ea;
|
||||
--text-color-high: #e4e8ea;
|
||||
--text-color-medium: #a3aaae;
|
||||
--text-color-low: #747d82;
|
||||
--border-color-high: #394348;
|
||||
--border-color-medium: #283034;
|
||||
--border-color-low: #283034;
|
||||
--primary-color-high: #819bab;
|
||||
--primary-color-medium: #6f8999;
|
||||
--primary-color-low: #536b79;
|
||||
--on-primary-color: #090b0d;
|
||||
--error-color-high: #bd8a8a;
|
||||
--error-color-medium: #a87575;
|
||||
--error-color-low: #8d6060;
|
||||
--success-color-high: #92ad98;
|
||||
--success-color-medium: #77957d;
|
||||
--success-color-low: #5c7862;
|
||||
--warn-color-high: #c2ad7b;
|
||||
--warn-color-medium: #b19a6b;
|
||||
--warn-color-low: #8f794f;
|
||||
--font-sans: Inter, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', SFMono-Regular, Consolas, 'Liberation Mono', monospace;
|
||||
--rmm-bg: #090b0d;
|
||||
--rmm-surface: #0e1113;
|
||||
--rmm-surface-2: #131719;
|
||||
--rmm-surface-3: #191e21;
|
||||
--rmm-border: #283034;
|
||||
--rmm-border-strong: #394348;
|
||||
--rmm-text: #e4e8ea;
|
||||
--rmm-muted: #a3aaae;
|
||||
--rmm-accent: #6f8999;
|
||||
--rmm-accent-hover: #819bab;
|
||||
--rmm-sidebar-width: 72px;
|
||||
--rmm-control-compact: 30px;
|
||||
--rmm-control-default: 36px;
|
||||
--rmm-control-touch: 42px;
|
||||
--rmm-success: #77957d;
|
||||
--rmm-warning: #b19a6b;
|
||||
--error-color-high-rgb: 189, 138, 138;
|
||||
--success-color-high-rgb: 146, 173, 152;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html, body { min-width: 0; }
|
||||
body {
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
padding: 0;
|
||||
background: var(--rmm-bg);
|
||||
}
|
||||
.container { max-width: 1440px; padding: 0 16px; }
|
||||
#maincontent { flex: 1 0 auto; min-width: 0; }
|
||||
/* Overflow belongs to individual tables and technical output, not the page. */
|
||||
#view { min-width: 0; max-width: 100%; }
|
||||
header {
|
||||
margin: 0 0 16px;
|
||||
padding: 0 max(16px, calc((100% - 1408px) / 2));
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
background: var(--rmm-surface);
|
||||
border-bottom: 1px solid var(--rmm-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
header .brand {
|
||||
float: none;
|
||||
margin: 0;
|
||||
padding: 10px 16px 10px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
header .brand:hover, header ul .active > a { background: var(--rmm-surface-3); }
|
||||
header a { color: var(--rmm-muted); text-shadow: none; }
|
||||
.rmm-brand-separator { color: var(--rmm-accent); padding: 0 4px; }
|
||||
.rmm-global-nav { flex: 1 1 auto; min-width: 0; }
|
||||
.rmm-global-nav .nav { float: none; display: flex; flex-wrap: wrap; margin: 0; }
|
||||
.nav > li > a { border-radius: 4px 4px 0 0; }
|
||||
.nav > li > a:hover, .nav > li.active > a { background: var(--rmm-surface-3); color: var(--rmm-text); }
|
||||
.rmm-global-nav .nav .dropdown-menu li a.rmm-current,
|
||||
.rmm-global-nav .nav > li > a.rmm-current { color: var(--rmm-text); background: #1a2228; }
|
||||
.rmm-global-nav .rmm-active > a.menu { color: var(--rmm-text); }
|
||||
|
||||
.rmm-nav-icon { display: none; width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
||||
|
||||
footer {
|
||||
padding: 12px max(16px, calc((100% - 1408px) / 2));
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid var(--rmm-border);
|
||||
background: var(--rmm-surface);
|
||||
font-size: 12px;
|
||||
}
|
||||
footer a { color: var(--rmm-accent-hover); }
|
||||
h1 { font-size: 24px; line-height: 1.3; margin: 0 0 16px; }
|
||||
h2 { font-size: 18px; line-height: 1.4; }
|
||||
h3, legend { font-size: 16px; line-height: 1.4; }
|
||||
h4, h5, h6 { line-height: 1.4; }
|
||||
p, .cbi-map-descr, .cbi-section-descr { font-size: 14px; line-height: 1.45; }
|
||||
a { color: var(--rmm-accent-hover); }
|
||||
a:hover { color: #a8bdc9; }
|
||||
code, pre, kbd, samp, .ifacebadge, .ifacebox-body { font-family: var(--font-mono); }
|
||||
hr { border-top: 1px solid var(--rmm-border); }
|
||||
.cbi-section, .cbi-map, .cbi-tabmenu, .cbi-page-actions,
|
||||
.table, .cbi-section-node, .ifacebox, .modal {
|
||||
box-shadow: none;
|
||||
}
|
||||
.cbi-section, .ifacebox, .modal {
|
||||
border: 1px solid var(--rmm-border);
|
||||
border-radius: 6px;
|
||||
background: var(--rmm-surface);
|
||||
}
|
||||
.cbi-section { padding: 12px; margin-bottom: 16px; }
|
||||
.cbi-section legend, .cbi-section > h3 { color: var(--rmm-text); }
|
||||
.cbi-value, .cbi-section-table-row, .tr { border-color: var(--rmm-border); }
|
||||
.cbi-value { padding-top: 8px; padding-bottom: 8px; }
|
||||
.cbi-value-description { margin-top: 6px; font-size: 12px; color: var(--rmm-muted); }
|
||||
.cbi-value-error { color: var(--error-color-high); }
|
||||
.cbi-rowstyle-2, .tr.table-titles, .tr.cbi-section-table-titles { background: var(--rmm-surface-2); }
|
||||
.cbi-section-table .tr:hover .td, .cbi-section-table .tr:hover .th,
|
||||
.cbi-section-table .tr:hover::before { background: var(--rmm-surface-3); }
|
||||
input:not([type="checkbox"]):not([type="radio"]), textarea, select,
|
||||
.cbi-dropdown:not(.btn):not(.cbi-button) {
|
||||
background: var(--rmm-surface-2);
|
||||
border: 1px solid var(--rmm-border-strong);
|
||||
border-radius: 4px;
|
||||
color: var(--rmm-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select { min-height: var(--rmm-control-default); }
|
||||
textarea { min-height: 96px; max-width: 100%; resize: vertical; }
|
||||
button:disabled, input:disabled, select:disabled, textarea:disabled { opacity: .65; cursor: not-allowed; }
|
||||
.cbi-page-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.cbi-page-actions > .cbi-button { float: none; margin: 0; min-height: var(--rmm-control-default); }
|
||||
input:focus, button:focus-visible, textarea:focus, select:focus,
|
||||
.btn:focus-visible, .cbi-button:focus-visible, a:focus-visible, summary:focus-visible {
|
||||
outline: 2px solid var(--rmm-accent-hover);
|
||||
outline-offset: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn, .cbi-button, button {
|
||||
background: var(--rmm-surface-2);
|
||||
border: 1px solid var(--rmm-border-strong);
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
color: var(--rmm-text);
|
||||
}
|
||||
.btn:focus, .cbi-button:focus, button:focus { box-shadow: none; }
|
||||
.btn:hover, .cbi-button:hover, button:hover { background: var(--rmm-surface-3); box-shadow: none; }
|
||||
.btn.primary, .cbi-button-action.important,
|
||||
.cbi-page-actions .cbi-button-apply, .cbi-section-actions .cbi-button-edit,
|
||||
.cbi-button-positive.important, .cbi-page-actions > .cbi-button.cbi-button-save {
|
||||
background-color: var(--rmm-accent);
|
||||
background-image: none;
|
||||
border-color: var(--rmm-accent-hover);
|
||||
color: var(--rmm-bg);
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
.alert-message { border-radius: 4px; box-shadow: none; }
|
||||
.alert-message.warning { color: var(--rmm-text); background: #29251c; border: 1px solid #8f794f; }
|
||||
.alert-message.error { color: var(--rmm-text); background: #291d1d; border: 1px solid #8d6060; }
|
||||
.ifacebox { margin: 4px; }
|
||||
.ifacebox .ifacebox-head { background: var(--rmm-surface-2); border-color: var(--rmm-border); color: var(--rmm-text); }
|
||||
.cbi-section-error { background: #291d1d; border-color: #8d6060; }
|
||||
.cbi-section-error ul li { color: var(--rmm-text); }
|
||||
|
||||
body:has(> section[hidden]) { background: var(--rmm-bg); }
|
||||
body:has(> section[hidden]) #view { margin: auto; width: min(100%, 440px); }
|
||||
body:has(> section[hidden]) .modal { background: var(--rmm-surface); border: 1px solid var(--rmm-border-strong); }
|
||||
.modal.login .rmm-login-context { margin: 0 0 12px; color: var(--rmm-muted); font-size: 12px; overflow-wrap: anywhere; }
|
||||
@media (max-width: 599px) {
|
||||
.modal.login .cbi-value { display: block; }
|
||||
.modal.login .cbi-value-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 6px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.modal.login .cbi-value-field { width: 100%; margin: 0; padding: 0; }
|
||||
.modal.login .cbi-value-field input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs, .cbi-tabmenu {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
background: var(--rmm-surface);
|
||||
border-bottom: 1px solid var(--rmm-border-strong);
|
||||
}
|
||||
.tabs > li.active > a, .cbi-tabmenu > .cbi-tab > a { color: var(--rmm-text); background: var(--rmm-surface-2); border-bottom: 2px solid var(--rmm-accent); }
|
||||
.tabs > li, .cbi-tabmenu > li { float: none; flex: 0 0 auto; height: auto; }
|
||||
.tabs > li > a, .cbi-tabmenu > li > a { display: flex; align-items: center; min-height: var(--rmm-control-default); }
|
||||
.tabs > li:not(.active), .cbi-tabmenu > .cbi-tab-disabled,
|
||||
.tabs > li:not(.active):hover, .cbi-tabmenu > .cbi-tab-disabled:hover {
|
||||
background: var(--rmm-surface-2);
|
||||
}
|
||||
.breadcrumb { background: var(--rmm-surface); border-color: var(--rmm-border); border-radius: 4px; }
|
||||
.dropdown-menu a:hover { background: var(--rmm-surface-3); color: var(--rmm-text); box-shadow: none; text-shadow: none; }
|
||||
.item::after, .ifacebadge { background: var(--rmm-surface-2); box-shadow: none; text-shadow: none; }
|
||||
.alert-message.success, .cbi-tooltip.success { background: #1b2620; border: 1px solid #5c7862; color: var(--rmm-text); }
|
||||
.alert-message.info, .cbi-tooltip.info { background: #1a2228; border: 1px solid #536b79; color: var(--rmm-text); }
|
||||
.alert-message.notice, .cbi-tooltip.notice { background: var(--rmm-surface-2); border: 1px solid var(--rmm-border-strong); color: var(--rmm-text); }
|
||||
.btn.danger, .btn.error, .cbi-tooltip.error { background: #291d1d; border: 1px solid #8d6060; color: var(--rmm-text); }
|
||||
[data-darkmode="true"] .zonebadge[style],
|
||||
[data-darkmode="true"] .ifacebox-head[style] { background-image: none !important; }
|
||||
/* LuCI components: preserve native widget handlers, IDs and validation. */
|
||||
button, .btn, .cbi-button, select, .cbi-dropdown,
|
||||
input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]) { min-height: var(--rmm-control-default); }
|
||||
.cbi-value-field { min-width: 0; }
|
||||
.cbi-value-field > input:not([type="checkbox"]):not([type="radio"]),
|
||||
.cbi-value-field > select, .cbi-value-field > .cbi-dropdown { max-width: 100%; }
|
||||
.cbi-value-description, .cbi-map-descr, .cbi-section-descr,
|
||||
.alert-message, .cbi-value-error { overflow-wrap: anywhere; }
|
||||
.cbi-input-invalid, input[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: var(--error-color-high); }
|
||||
input:disabled, select:disabled, textarea:disabled,
|
||||
button:disabled, .cbi-button:disabled, .cbi-dropdown[disabled] { opacity: .7; cursor: not-allowed; }
|
||||
.cbi-dropdown:focus-visible, .rmm-table-scroll:focus-visible {
|
||||
outline: 2px solid var(--rmm-accent-hover);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.cbi-dropdown[open] > ul.dropdown {
|
||||
background: var(--rmm-surface-2);
|
||||
border: 1px solid var(--rmm-border-strong);
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
min-width: min(100%, calc(100vw - 24px));
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
/* LuCI writes inline popup geometry; own only geometry to keep it in the viewport. */
|
||||
.cbi-dropdown[open] > ul.dropdown.rmm-dropdown-panel {
|
||||
position: fixed;
|
||||
left: var(--rmm-dropdown-left) !important;
|
||||
top: var(--rmm-dropdown-top) !important;
|
||||
width: var(--rmm-dropdown-width) !important;
|
||||
min-width: 0;
|
||||
max-height: var(--rmm-dropdown-height) !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.cbi-dropdown[open] > ul.dropdown > li { white-space: normal; overflow-wrap: anywhere; }
|
||||
.cbi-dropdown[open] > ul.dropdown > li[selected].focus,
|
||||
.cbi-dropdown[open] > ul.dropdown > li[selected]:hover { background: var(--rmm-accent-hover); }
|
||||
.rmm-table-scroll { max-width: 100%; overflow: auto; margin-bottom: 16px; }
|
||||
.rmm-table-scroll > .table { margin-bottom: 0; }
|
||||
.rmm-records { table-layout: fixed; }
|
||||
.rmm-records :is(.td, .th), .rmm-key-values :is(.td, .th) { white-space: normal; overflow-wrap: anywhere; }
|
||||
.rmm-records .nowrap { white-space: normal; }
|
||||
.rmm-records :is(input, select, .cbi-dropdown), .rmm-key-values :is(input, select, .cbi-dropdown) { min-width: 0; max-width: 100%; }
|
||||
.ifacebadge { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
||||
.ifacebadge > span { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; max-width: 100%; white-space: normal; }
|
||||
.ifacebadge img { position: static; flex: 0 0 auto; margin: 0; width: 24px; height: 24px; object-fit: contain; }
|
||||
.rmm-records :is(.ifacebox, .ifacebadge, .zonebadge) { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
||||
pre, #syslog, .command-output, .uci-change-list {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1.55;
|
||||
background: var(--rmm-bg);
|
||||
border: 1px solid var(--rmm-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
.uci-change-list { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.rmm-diag-label { display: block; font-size: 12px; color: var(--rmm-muted); margin-bottom: 6px; }
|
||||
.rmm-diagnostics input { min-width: 0; max-width: 100%; }
|
||||
.modal { min-width: 0; }
|
||||
.modal:not(.login) {
|
||||
width: min(560px, calc(100% - 24px));
|
||||
max-height: calc(100dvh - 24px);
|
||||
overflow: auto;
|
||||
margin: 12px auto;
|
||||
}
|
||||
/* Upstream IDs own wide dialog sizes, so retain their specificity here. */
|
||||
#modal_overlay > .modal.cbi-modal, #modal_overlay > .modal.uci-dialog { max-width: 760px; width: calc(100% - 24px); }
|
||||
.modal .button-row { display: flex; flex-wrap: wrap; gap: 8px; position: sticky; bottom: 0; background: var(--rmm-surface); padding-top: 8px; }
|
||||
.modal .button-row > button { float: none; margin: 0; }
|
||||
.modal.login input, .modal.login button { min-height: var(--rmm-control-touch); }
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.rmm-global-nav .nav .dropdown-menu li a { min-height: var(--rmm-control-touch); }
|
||||
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { display: inline-flex; align-items: center; min-height: var(--rmm-control-touch); }
|
||||
button, .btn, .cbi-button, select,
|
||||
input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), .cbi-dropdown {
|
||||
min-height: var(--rmm-control-touch);
|
||||
}
|
||||
button, .btn, .cbi-button, .cbi-dropdown > .open { min-width: var(--rmm-control-touch); }
|
||||
.cbi-dropdown[open] > ul.dropdown > li, .tabs > li > a, .cbi-tabmenu > li > a { min-height: var(--rmm-control-touch); }
|
||||
.cbi-value-field > label { display: inline-flex; align-items: center; min-height: var(--rmm-control-touch); }
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.rmm-table-scroll:has(> .rmm-records), .rmm-table-scroll:has(> .rmm-key-values) { overflow: visible; }
|
||||
.rmm-records, .rmm-records > :is(thead, tbody, tfoot), .rmm-key-values,
|
||||
.rmm-key-values > :is(thead, tbody, tfoot), .rmm-diagnostics, .rmm-diagnostics > tbody { display: block; width: 100%; }
|
||||
/* Retain header content/semantics and sorting controls in a compact scroll row. */
|
||||
.rmm-records .rmm-table-head { display: flex; max-width: 100%; overflow-x: auto; }
|
||||
.rmm-records .rmm-table-head > :is(.th, th) { display: block; flex: 0 0 auto; font-size: 11px; padding: 6px; }
|
||||
.rmm-records .rmm-record-row {
|
||||
display: block;
|
||||
border-top: 1px solid var(--rmm-border-strong);
|
||||
padding: 8px 0;
|
||||
}
|
||||
.rmm-records .rmm-record-row[data-title]::before {
|
||||
content: attr(data-title);
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
/* LuCI supplies per-column inline widths; mobile records own these dimensions. */
|
||||
.rmm-records .rmm-record-row > :is(.td, td, .th, th) {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100%;
|
||||
padding: 6px 8px;
|
||||
border-top: 0;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
}
|
||||
.rmm-records .rmm-record-row > [data-rmm-label]:not([data-rmm-label=""])::before {
|
||||
content: attr(data-rmm-label);
|
||||
display: block;
|
||||
color: var(--rmm-muted);
|
||||
font: 400 12px/1.4 var(--font-sans);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.rmm-records .cbi-section-actions > div { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.rmm-key-values .tr { display: grid; grid-template-columns: minmax(0, 1fr); padding: 6px 0; }
|
||||
.rmm-key-values .tr > :is(.td, .th) { display: block; width: auto; padding: 4px 8px; border: 0; }
|
||||
.rmm-key-values .tr > :first-child { color: var(--rmm-muted); }
|
||||
.rmm-diagnostics .tr, .rmm-diagnostics .td { display: block; width: 100%; }
|
||||
.rmm-diagnostics .td { padding: 12px 0; }
|
||||
.rmm-diagnostics :is(input, .cbi-dropdown, .diag-action, button) { width: 100%; }
|
||||
.rmm-diagnostics input { margin-bottom: 8px; }
|
||||
.cbi-page-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px 0; }
|
||||
.cbi-page-actions > :is(button, input, .btn) { float: none; margin: 0; }
|
||||
button, .btn, .cbi-button, select, .cbi-dropdown,
|
||||
input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]) { min-height: var(--rmm-control-touch); }
|
||||
.cbi-dropdown[open] > ul.dropdown > li { min-height: var(--rmm-control-touch); }
|
||||
.tabs > li > a, .cbi-tabmenu > li > a { min-height: var(--rmm-control-touch); }
|
||||
.modal .button-row > button { flex: 1 1 100%; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cbi-dropdown[open] > ul.dropdown, .cbi-tooltip { transition: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 854px) {
|
||||
header { padding: 0 12px; }
|
||||
header .brand { min-width: 0; max-width: 65%; overflow-wrap: anywhere; white-space: normal; }
|
||||
.container { padding: 0 12px; }
|
||||
footer { padding: 12px; }
|
||||
.cbi-section { padding: 8px; }
|
||||
}
|
||||
@media (min-width: 600px) and (max-width: 899px) {
|
||||
.rmm-global-nav .nav > li.dropdown { position: relative; }
|
||||
.rmm-global-nav .nav .dropdown-menu {
|
||||
display: none;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: calc(100dvh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.rmm-global-nav .nav > li.rmm-open > .dropdown-menu,
|
||||
body:not(.rmm-nav-enhanced) .rmm-global-nav .nav > li.dropdown:focus-within > .dropdown-menu,
|
||||
body:not(.rmm-nav-enhanced) .rmm-global-nav .nav > li.dropdown:hover > .dropdown-menu { display: block; }
|
||||
.rmm-global-nav .nav > li.dropdown:nth-last-child(2) > .dropdown-menu { left: auto; right: 0; }
|
||||
.rmm-global-nav .nav .dropdown-menu li a { white-space: normal; overflow-wrap: anywhere; }
|
||||
}
|
||||
@media (min-width: 900px) {
|
||||
header { position: fixed; inset: 0 auto 0 0; z-index: 800; width: var(--rmm-sidebar-width); min-height: 0; margin: 0; padding: 0; flex-direction: column; align-items: stretch; overflow: hidden; border-right: 1px solid var(--rmm-border); border-bottom: 0; }
|
||||
header .brand { flex: none; display: flex; justify-content: center; padding: 16px 4px; font-size: 14px; border-bottom: 1px solid var(--rmm-border); }
|
||||
header .brand .rmm-brand-host, header .brand .rmm-brand-separator { display: none; }
|
||||
.rmm-global-nav { flex: 1 1 auto; min-height: 0; padding: 8px; overflow-y: auto; overflow-x: hidden; }
|
||||
.rmm-global-nav .nav { display: block; width: 100%; }
|
||||
.rmm-global-nav .nav > li { float: none; width: 100%; }
|
||||
.rmm-global-nav .nav > li > a { display: flex; justify-content: center; align-items: center; min-height: 48px; padding: 8px; border-radius: 4px; }
|
||||
.rmm-global-nav .nav > li.rmm-active > a.menu, .rmm-global-nav .nav > li > a.rmm-current { border-left: 3px solid var(--rmm-accent); padding-left: 5px; }
|
||||
.rmm-nav-icon-link::after { display: none; }
|
||||
.rmm-nav-icon { display: block; }
|
||||
/* Requested icon rail: labels remain accessible and visible in category flyouts. */
|
||||
.rmm-nav-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.rmm-global-nav .nav .dropdown-menu { position: fixed; display: none; top: var(--rmm-flyout-top, 12px); left: calc(var(--rmm-sidebar-width) + 8px); float: none; width: 280px; min-width: 0; max-width: calc(100vw - var(--rmm-sidebar-width) - 24px); max-height: calc(100dvh - 24px); overflow-y: auto; margin: 0; padding: 8px; background: var(--rmm-surface); border: 1px solid var(--rmm-border-strong); border-radius: 6px; box-shadow: none; }
|
||||
.rmm-global-nav .nav > li.rmm-open > .dropdown-menu,
|
||||
body:not(.rmm-nav-enhanced) .rmm-global-nav .nav > li.dropdown:focus-within > .dropdown-menu { display: block; }
|
||||
.rmm-global-nav .nav .dropdown-menu li a { display: block; min-height: 36px; padding: 8px 12px; color: var(--rmm-muted); white-space: normal; overflow-wrap: anywhere; }
|
||||
.rmm-global-nav .nav .dropdown-menu li a.rmm-current { padding-left: 9px; border-left: 3px solid var(--rmm-accent); }
|
||||
.rmm-global-nav .nav > li:last-child { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--rmm-border); }
|
||||
header #indicators { flex: none; align-self: stretch; margin: 0; padding: 8px 4px; border-top: 1px solid var(--rmm-border); overflow-wrap: anywhere; }
|
||||
#maincontent { width: calc(100% - var(--rmm-sidebar-width)); max-width: none; margin-left: var(--rmm-sidebar-width); padding: 24px; }
|
||||
footer { margin-left: var(--rmm-sidebar-width); padding: 12px 24px; }
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
body {
|
||||
--rmm-mobile-nav-fallback: calc(56px + env(safe-area-inset-bottom));
|
||||
padding-bottom: var(--rmm-mobile-nav-height, var(--rmm-mobile-nav-fallback));
|
||||
}
|
||||
header { min-height: 48px; margin-bottom: 12px; }
|
||||
header .brand {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
max-width: none;
|
||||
padding: 8px 8px 8px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
header #indicators { flex: 0 0 auto; max-width: 35%; margin: 0; }
|
||||
.rmm-global-nav {
|
||||
position: fixed;
|
||||
inset: auto 0 0;
|
||||
z-index: 900;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: var(--rmm-surface);
|
||||
border-top: 1px solid var(--rmm-border);
|
||||
}
|
||||
.rmm-global-nav .nav { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); width: 100%; }
|
||||
.rmm-global-nav .nav.rmm-nav-many { display: flex; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; }
|
||||
.rmm-global-nav .nav.rmm-nav-many > li { flex: 1 0 56px; }
|
||||
.rmm-global-nav .nav > li { float: none; min-width: 0; }
|
||||
.rmm-global-nav .rmm-nav-icon { display: block; width: 18px; height: 18px; }
|
||||
.rmm-global-nav .rmm-nav-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
|
||||
.rmm-global-nav .rmm-nav-icon-link::after { display: none; }
|
||||
.rmm-global-nav .nav > li > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 56px;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px 3px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
border-radius: 0;
|
||||
}
|
||||
.rmm-global-nav .nav > li.rmm-active > a.menu { border-top: 2px solid var(--rmm-accent); padding-top: 4px; }
|
||||
.rmm-global-nav .nav > li.rmm-open > a.menu { background: var(--rmm-surface-3); color: var(--rmm-text); }
|
||||
.rmm-global-nav .nav .dropdown-menu {
|
||||
position: fixed;
|
||||
display: none;
|
||||
inset: auto 12px calc(var(--rmm-mobile-nav-height, var(--rmm-mobile-nav-fallback)) + 8px);
|
||||
width: auto;
|
||||
max-height: calc(100dvh - var(--rmm-mobile-nav-height, var(--rmm-mobile-nav-fallback)) - 40px);
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
overflow-y: auto;
|
||||
background: var(--rmm-surface);
|
||||
border: 1px solid var(--rmm-border-strong);
|
||||
border-radius: 6px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.rmm-global-nav .nav > li.rmm-open > .dropdown-menu,
|
||||
body:not(.rmm-nav-enhanced) .rmm-global-nav .nav > li.dropdown:focus-within > .dropdown-menu { display: block; left: 12px; }
|
||||
.rmm-global-nav .nav .dropdown-menu li a {
|
||||
display: block;
|
||||
min-height: 42px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
.rmm-global-nav .nav .dropdown-menu li a.rmm-current { border-left: 3px solid var(--rmm-accent); padding-left: 9px; }
|
||||
.cbi-value { display: block; }
|
||||
.cbi-value > label.cbi-value-title { display: block; width: 100%; text-align: left; float: none; padding: 0 0 6px; }
|
||||
.cbi-value-field { width: 100%; min-width: 0; max-width: 100%; margin-left: 0; }
|
||||
.cbi-value-field input:not([type="checkbox"]):not([type="radio"]) { max-width: 100%; }
|
||||
.cbi-value-field .control-group { display: flex; flex-wrap: wrap; gap: 4px; max-width: 100%; }
|
||||
}
|
||||
@media (max-width: 299px) {
|
||||
body { --rmm-mobile-nav-fallback: calc(112px + env(safe-area-inset-bottom)); }
|
||||
.rmm-global-nav .nav { grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* Headerless native status summaries share the stacked mobile layout. */
|
||||
.rmm-status-table { width: 100%; table-layout: fixed; }
|
||||
.rmm-status-table .nowrap { white-space: normal; overflow-wrap: anywhere; }
|
||||
.rmm-status-table .cbi-section-actions > div { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
@media (max-width: 599px) {
|
||||
/* LuCI may set inline column widths; mobile rows own the full available width. */
|
||||
.rmm-status-table, .rmm-status-table > tbody, .rmm-status-table > .tbody,
|
||||
.rmm-status-table .tr, .rmm-status-table .td {
|
||||
display: block; min-width: 0; max-width: 100%; width: 100% !important;
|
||||
}
|
||||
.rmm-status-table .tr { padding: 12px 0; border-top: 1px solid var(--rmm-border); }
|
||||
.rmm-status-table .td { text-align: left; white-space: normal; overflow-wrap: anywhere; }
|
||||
.rmm-status-table .cbi-section-actions > div { justify-content: flex-start; }
|
||||
}
|
||||
|
||||
/* Search indexes only links rendered by the authorized LuCI menu. */
|
||||
.rmm-search-launcher { display: flex; justify-content: flex-end; margin-bottom: 12px; }
|
||||
.rmm-search-launcher .btn[hidden], .rmm-search-launcher:has(> .btn[hidden]) { display: none; }
|
||||
.rmm-search-launcher .btn, .rmm-search-heading .btn { min-height: 36px; }
|
||||
dialog.rmm-menu-search { box-sizing: border-box; width: calc(100% - 24px); max-width: 560px; max-height: min(640px, calc(100dvh - 24px)); overflow-y: auto; margin: auto; padding: 16px; color: var(--rmm-text); background: var(--rmm-surface); border: 1px solid var(--rmm-border-strong); border-radius: 8px; box-shadow: none; }
|
||||
dialog.rmm-menu-search::backdrop { background: rgb(9 11 13 / 75%); }
|
||||
.rmm-search-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
|
||||
.rmm-search-heading h2 { font-size: 18px; margin: 0; }
|
||||
.rmm-menu-search label { display: block; margin-bottom: 8px; }
|
||||
.rmm-menu-search input { width: 100%; box-sizing: border-box; min-height: 36px; }
|
||||
.rmm-search-meta { color: var(--rmm-muted); font-size: 12px; margin: 8px 0; }
|
||||
.rmm-search-results { list-style: none; margin: 12px 0 0; padding: 0; }
|
||||
.rmm-search-results li { border-top: 1px solid var(--rmm-border); }
|
||||
.rmm-search-results a { display: block; padding: 12px 8px; color: var(--rmm-text); overflow-wrap: anywhere; }
|
||||
.rmm-search-results a:hover, .rmm-search-results a:focus-visible { background: var(--rmm-surface-3); }
|
||||
@media (pointer: coarse) { .rmm-search-launcher .btn, .rmm-search-heading .btn, .rmm-menu-search input, .rmm-search-results a { min-height: 42px; } }
|
||||
@@ -0,0 +1,187 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function translate(message) {
|
||||
var russian = {
|
||||
'Table': 'Таблица', 'Actions': 'Действия',
|
||||
'Ping destination': 'Адрес для пинга',
|
||||
'Traceroute destination': 'Адрес для трассировки',
|
||||
'DNS destination': 'Адрес для DNS-запроса'
|
||||
};
|
||||
return /^ru(?:-|$)/i.test(document.documentElement.lang) && russian[message] ||
|
||||
(typeof window._ === 'function' ? window._(message) : message);
|
||||
}
|
||||
|
||||
function cells(row) {
|
||||
return Array.from(row.children).filter(function(cell) {
|
||||
return cell.matches('td, th, .td, .th');
|
||||
});
|
||||
}
|
||||
|
||||
function prepareTable(table) {
|
||||
var rows = Array.from(table.querySelectorAll('tr, .tr')).filter(function(row) {
|
||||
return row.closest('.table') === table;
|
||||
});
|
||||
var heading = rows.find(function(row) {
|
||||
return row.matches('.table-titles, .cbi-section-table-titles') ||
|
||||
cells(row).some(function(cell) { return cell.matches('th, .th'); });
|
||||
});
|
||||
var labels = heading ? cells(heading).map(function(cell) { return cell.textContent.trim(); }) : [];
|
||||
var diagnostics = /(?:^|-)admin-network-diagnostics$/.test(document.body.getAttribute('data-page') || '');
|
||||
|
||||
if (diagnostics) {
|
||||
table.classList.add('rmm-diagnostics');
|
||||
var destinations = [ 'Ping destination', 'Traceroute destination', 'DNS destination' ];
|
||||
rows.forEach(function(row) {
|
||||
cells(row).forEach(function(cell, index) {
|
||||
var input = cell.querySelector('input[type="text"]');
|
||||
if (!input || index >= destinations.length || cell.querySelector('.rmm-diag-label'))
|
||||
return;
|
||||
var label = document.createElement('label');
|
||||
label.className = 'rmm-diag-label';
|
||||
label.textContent = translate(destinations[index]);
|
||||
if (!input.id)
|
||||
input.id = 'rmm-diag-destination-' + index;
|
||||
label.htmlFor = input.id;
|
||||
cell.insertBefore(label, input);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (heading) {
|
||||
table.classList.add('rmm-records');
|
||||
heading.classList.add('rmm-table-head');
|
||||
rows.forEach(function(row) {
|
||||
if (row === heading || row.matches('.cbi-section-table-descr, .placeholder'))
|
||||
return;
|
||||
var values = cells(row);
|
||||
if (values.length !== labels.length || values.some(function(cell) {
|
||||
return Number(cell.getAttribute('colspan') || 1) > 1;
|
||||
}))
|
||||
return;
|
||||
row.classList.add('rmm-record-row');
|
||||
values.forEach(function(cell, index) {
|
||||
// Own label attribute leaves LuCI's data-title and widget hooks intact.
|
||||
var label = cell.getAttribute('data-title') || labels[index];
|
||||
if (!label && cell.matches('.cbi-section-actions'))
|
||||
label = translate('Actions');
|
||||
if (cell.getAttribute('data-rmm-label') !== label)
|
||||
cell.setAttribute('data-rmm-label', label);
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (rows.length && rows.every(function(row) { return cells(row).length === 2; })) {
|
||||
table.classList.add('rmm-key-values');
|
||||
}
|
||||
|
||||
else if (rows.length && rows.every(function(row) { return cells(row).length >= 3 && !cells(row).some(function(cell) { return Number(cell.getAttribute('colspan') || 1) > 1; }); })) {
|
||||
// Native interface/Wi-Fi summaries have no column heading.
|
||||
table.classList.add('rmm-status-table');
|
||||
}
|
||||
|
||||
// Form widgets need visible overflow for their native dropdowns and tooltips.
|
||||
// Only read-only tables get a local scroll wrapper; nodes/events are retained.
|
||||
if (table.querySelector('input, select, textarea, .cbi-dropdown, .cbi-tooltip-container'))
|
||||
return;
|
||||
if (!table.parentElement.classList.contains('rmm-table-scroll')) {
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'rmm-table-scroll';
|
||||
wrapper.setAttribute('role', 'region');
|
||||
wrapper.setAttribute('aria-label', translate('Table'));
|
||||
wrapper.tabIndex = 0;
|
||||
table.parentElement.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
}
|
||||
}
|
||||
|
||||
function positionDropdowns() {
|
||||
var width = document.documentElement.clientWidth || window.innerWidth;
|
||||
var height = window.innerHeight;
|
||||
var reserved = parseFloat(window.getComputedStyle(document.body).paddingBottom) || 0;
|
||||
document.querySelectorAll('.cbi-dropdown[open]').forEach(function(dropdown) {
|
||||
var list = dropdown.querySelector(':scope > ul.dropdown');
|
||||
if (!list) return;
|
||||
var anchor = dropdown.getBoundingClientRect();
|
||||
var panelWidth = Math.min(Math.max(anchor.width, list.scrollWidth), Math.max(0, width - 24));
|
||||
var below = Math.max(0, height - reserved - anchor.bottom - 12);
|
||||
var above = Math.max(0, anchor.top - 12);
|
||||
var useAbove = below < Math.min(list.scrollHeight, 180) && above > below;
|
||||
var available = useAbove ? above : below;
|
||||
var panelHeight = Math.min(list.scrollHeight, available);
|
||||
dropdown.style.setProperty('--rmm-dropdown-left', Math.max(12, Math.min(anchor.left, width - panelWidth - 12)) + 'px');
|
||||
dropdown.style.setProperty('--rmm-dropdown-top', (useAbove ? anchor.top - panelHeight : anchor.bottom) + 'px');
|
||||
dropdown.style.setProperty('--rmm-dropdown-width', panelWidth + 'px');
|
||||
dropdown.style.setProperty('--rmm-dropdown-height', available + 'px');
|
||||
list.classList.add('rmm-dropdown-panel');
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var scheduled = false;
|
||||
var previousFocus = document.activeElement;
|
||||
var modalWasOpen = false;
|
||||
|
||||
function refresh() {
|
||||
scheduled = false;
|
||||
document.querySelectorAll('#view .table, #modal_overlay .table').forEach(prepareTable);
|
||||
positionDropdowns();
|
||||
var overlay = document.getElementById('modal_overlay');
|
||||
var modal = overlay && overlay.querySelector('.modal:not(.login)');
|
||||
var open = !!modal && document.body.classList.contains('modal-overlay-active');
|
||||
if (open) {
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
var title = modal.querySelector('h4');
|
||||
if (title) {
|
||||
if (!title.id) title.id = 'rmm-dialog-title';
|
||||
modal.setAttribute('aria-labelledby', title.id);
|
||||
}
|
||||
}
|
||||
if (modalWasOpen && !open && previousFocus && previousFocus.isConnected &&
|
||||
(overlay?.contains(document.activeElement) || document.activeElement === document.body))
|
||||
previousFocus.focus();
|
||||
modalWasOpen = open;
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
if (!scheduled) {
|
||||
scheduled = true;
|
||||
window.requestAnimationFrame(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('focusin', function(event) {
|
||||
if (!document.body.classList.contains('modal-overlay-active'))
|
||||
previousFocus = event.target;
|
||||
});
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key !== 'Tab' || !document.body.classList.contains('modal-overlay-active'))
|
||||
return;
|
||||
var overlay = document.getElementById('modal_overlay');
|
||||
var modal = overlay && overlay.querySelector('.modal:not(.login)');
|
||||
if (!modal) return;
|
||||
var controls = Array.from(modal.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [tabindex]')).filter(function(control) {
|
||||
return !control.disabled && control.tabIndex >= 0 && control.getClientRects().length;
|
||||
});
|
||||
if (!controls.length) return;
|
||||
var index = controls.indexOf(document.activeElement);
|
||||
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
||||
event.preventDefault();
|
||||
controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
||||
}
|
||||
});
|
||||
new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||
new MutationObserver(schedule).observe(document.body, { attributes: true, attributeFilter: [ 'class' ] });
|
||||
new MutationObserver(schedule).observe(document.body, { attributes: true, subtree: true, attributeFilter: [ 'open' ] });
|
||||
window.addEventListener('resize', schedule);
|
||||
window.addEventListener('scroll', schedule, { capture: true, passive: true });
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
else
|
||||
init();
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="icon-layout-grid" viewBox="0 0 24 24">
|
||||
<path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path d="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path d="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
</symbol>
|
||||
<symbol id="icon-activity-heartbeat" viewBox="0 0 24 24">
|
||||
<path d="M3 12h4.5l1.5 -6l4 12l2 -9l1.5 3h4.5" />
|
||||
</symbol>
|
||||
<symbol id="icon-router" viewBox="0 0 24 24">
|
||||
<path d="M3 15a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -4" />
|
||||
<path d="M17 17l0 .01" />
|
||||
<path d="M13 17l0 .01" />
|
||||
<path d="M15 13l0 -2" />
|
||||
<path d="M11.75 8.75a4 4 0 0 1 6.5 0" />
|
||||
<path d="M8.5 6.5a8 8 0 0 1 13 0" />
|
||||
</symbol>
|
||||
<symbol id="icon-terminal-2" viewBox="0 0 24 24">
|
||||
<path d="M8 9l3 3l-3 3" />
|
||||
<path d="M13 15l3 0" />
|
||||
<path d="M3 6a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -12" />
|
||||
</symbol>
|
||||
<symbol id="icon-network" viewBox="0 0 24 24">
|
||||
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 0 0 -12 0" />
|
||||
<path d="M12 3c1.333 .333 2 2.333 2 6s-.667 5.667 -2 6" />
|
||||
<path d="M12 3c-1.333 .333 -2 2.333 -2 6s.667 5.667 2 6" />
|
||||
<path d="M6 9h12" />
|
||||
<path d="M3 20h7" />
|
||||
<path d="M14 20h7" />
|
||||
<path d="M10 20a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M12 15v3" />
|
||||
</symbol>
|
||||
<symbol id="icon-shield-lock" viewBox="0 0 24 24">
|
||||
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
|
||||
<path d="M11 11a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 12l0 2.5" />
|
||||
</symbol>
|
||||
<symbol id="icon-logout" viewBox="0 0 24 24">
|
||||
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
|
||||
<path d="M9 12h12l-3 -3" />
|
||||
<path d="M18 15l3 -3" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,222 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function init() {
|
||||
var menu = document.getElementById('topmenu');
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
var nav = menu.parentElement;
|
||||
var mobile = window.matchMedia('(max-width: 599px)');
|
||||
var desktop = window.matchMedia('(min-width: 900px)');
|
||||
var scheduled = false;
|
||||
var restoringFocus = false;
|
||||
|
||||
function groups() {
|
||||
return menu.querySelectorAll(':scope > li.dropdown');
|
||||
}
|
||||
|
||||
function decorateLinks() {
|
||||
var icons = { dashboard: 'layout-grid', status: 'activity-heartbeat', system: 'router', services: 'terminal-2', network: 'network', vpn: 'shield-lock', logout: 'logout' };
|
||||
var sprite = document.body.getAttribute('data-rmm-icons') || '/luci-static/rmm/icons.svg';
|
||||
menu.querySelectorAll(':scope > li > a').forEach(function(link) {
|
||||
if (link.querySelector('.rmm-nav-icon')) return;
|
||||
var destination = link.getAttribute('href') === '#' ? link.parentElement.querySelector('.dropdown-menu a[href]') : link;
|
||||
var section = '';
|
||||
try { section = new URL(destination && destination.getAttribute('href') || '', location.href).pathname.match(/\/admin\/([^/]+)/)?.[1] || ''; } catch (_) {}
|
||||
var label = link.textContent.trim();
|
||||
var text = document.createElement('span');
|
||||
text.className = 'rmm-nav-label';
|
||||
while (link.firstChild) text.appendChild(link.firstChild);
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.classList.add('rmm-nav-icon');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
svg.setAttribute('focusable', 'false');
|
||||
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('href', sprite + '#icon-' + (icons[section] || 'layout-grid'));
|
||||
svg.appendChild(use);
|
||||
link.append(svg, text);
|
||||
link.setAttribute('aria-label', label);
|
||||
link.setAttribute('title', label);
|
||||
link.classList.add('rmm-nav-icon-link');
|
||||
});
|
||||
}
|
||||
|
||||
function positionFlyout(group) {
|
||||
if (!desktop.matches || !group.classList.contains('rmm-open')) return;
|
||||
var trigger = group.querySelector(':scope > a.menu');
|
||||
var submenu = group.querySelector(':scope > .dropdown-menu');
|
||||
if (!trigger || !submenu) return;
|
||||
var available = Math.max(0, window.innerHeight - 24);
|
||||
var height = Math.min(submenu.scrollHeight, available);
|
||||
var top = Math.max(12, Math.min(trigger.getBoundingClientRect().top, window.innerHeight - height - 12));
|
||||
submenu.style.setProperty('--rmm-flyout-top', top + 'px');
|
||||
}
|
||||
|
||||
function updateNavHeight() {
|
||||
if (mobile.matches)
|
||||
document.documentElement.style.setProperty('--rmm-mobile-nav-height', Math.ceil(nav.getBoundingClientRect().height) + 'px');
|
||||
else
|
||||
document.documentElement.style.removeProperty('--rmm-mobile-nav-height');
|
||||
}
|
||||
|
||||
function closeGroups(restoreFocus) {
|
||||
var open = menu.querySelector(':scope > li.rmm-open');
|
||||
groups().forEach(function(group) {
|
||||
group.classList.remove('rmm-open');
|
||||
var trigger = group.querySelector(':scope > a.menu');
|
||||
if (trigger)
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
if (restoreFocus && open) {
|
||||
restoringFocus = true;
|
||||
open.querySelector(':scope > a.menu')?.focus();
|
||||
restoringFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
scheduled = false;
|
||||
decorateLinks();
|
||||
menu.classList.toggle('rmm-nav-many', menu.children.length > 5);
|
||||
var current = location.pathname.replace(/\/+$/, '');
|
||||
// The dispatcher resolves firstchild/alias entry URLs without redirecting.
|
||||
var route = document.body.getAttribute('data-rmm-route');
|
||||
if (route) {
|
||||
try {
|
||||
var resolved = new URL(route, location.href);
|
||||
if (resolved.origin === location.origin)
|
||||
current = resolved.pathname.replace(/\/+$/, '');
|
||||
}
|
||||
catch (_) { /* Keep the visible URL when the route is malformed. */ }
|
||||
}
|
||||
var best = null;
|
||||
var bestLength = -1;
|
||||
|
||||
menu.querySelectorAll('.rmm-current').forEach(function(link) {
|
||||
link.classList.remove('rmm-current');
|
||||
link.removeAttribute('aria-current');
|
||||
});
|
||||
menu.querySelectorAll('.rmm-active').forEach(function(group) {
|
||||
group.classList.remove('rmm-active');
|
||||
});
|
||||
|
||||
menu.querySelectorAll('a[href]').forEach(function(link) {
|
||||
if (link.getAttribute('href') === '#')
|
||||
return;
|
||||
var url;
|
||||
try { url = new URL(link.href, location.href); }
|
||||
catch (_) { return; }
|
||||
if (url.origin !== location.origin)
|
||||
return;
|
||||
var path = url.pathname.replace(/\/+$/, '');
|
||||
if ((current === path || current.startsWith(path + '/')) && path.length > bestLength) {
|
||||
best = link;
|
||||
bestLength = path.length;
|
||||
}
|
||||
});
|
||||
|
||||
if (best) {
|
||||
best.classList.add('rmm-current');
|
||||
best.setAttribute('aria-current', 'page');
|
||||
best.closest('li.dropdown')?.classList.add('rmm-active');
|
||||
}
|
||||
|
||||
groups().forEach(function(group, index) {
|
||||
var trigger = group.querySelector(':scope > a.menu');
|
||||
var submenu = group.querySelector(':scope > .dropdown-menu');
|
||||
if (!trigger || !submenu)
|
||||
return;
|
||||
if (!submenu.id)
|
||||
submenu.id = 'rmm-submenu-' + index;
|
||||
trigger.setAttribute('aria-controls', submenu.id);
|
||||
trigger.setAttribute('aria-expanded', group.classList.contains('rmm-open') ? 'true' : 'false');
|
||||
positionFlyout(group);
|
||||
});
|
||||
updateNavHeight();
|
||||
}
|
||||
|
||||
function scheduleUpdate() {
|
||||
if (!scheduled) {
|
||||
scheduled = true;
|
||||
Promise.resolve().then(update);
|
||||
}
|
||||
}
|
||||
|
||||
menu.addEventListener('click', function(event) {
|
||||
var trigger = event.target.closest('li.dropdown > a.menu');
|
||||
if (!trigger || !menu.contains(trigger))
|
||||
return;
|
||||
event.preventDefault();
|
||||
var group = trigger.parentElement;
|
||||
var wasOpen = group.classList.contains('rmm-open');
|
||||
closeGroups(false);
|
||||
group.classList.toggle('rmm-open', !wasOpen);
|
||||
trigger.setAttribute('aria-expanded', wasOpen ? 'false' : 'true');
|
||||
positionFlyout(group);
|
||||
});
|
||||
menu.addEventListener('keydown', function(event) {
|
||||
var primary=Array.from(menu.querySelectorAll(':scope > li > a'));
|
||||
var index=primary.indexOf(event.target);
|
||||
if(index>=0 && ['ArrowLeft','ArrowRight','Home','End'].includes(event.key)){
|
||||
event.preventDefault();var next=event.key==='Home'?0:event.key==='End'?primary.length-1:(index+(event.key==='ArrowLeft'?-1:1)+primary.length)%primary.length;primary[next].focus();return;
|
||||
}
|
||||
if(index>=0 && event.key==='ArrowDown' && event.target.matches('a.menu')){
|
||||
event.preventDefault();if(!event.target.parentElement.classList.contains('rmm-open'))event.target.click();event.target.parentElement.querySelector('.dropdown-menu a')?.focus();return;
|
||||
}
|
||||
if ((event.key === ' ' || event.key === 'Enter') && event.target.matches('li.dropdown > a.menu')) {
|
||||
event.preventDefault();
|
||||
event.target.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Tablet and desktop submenus open when their trigger receives keyboard focus.
|
||||
menu.addEventListener('focusin', function(event) {
|
||||
if (mobile.matches || restoringFocus)
|
||||
return;
|
||||
if (event.target.matches('a.menu') && !event.target.matches(':focus-visible'))
|
||||
return;
|
||||
var group = event.target.closest('li.dropdown');
|
||||
if (!group || !menu.contains(group))
|
||||
return;
|
||||
closeGroups(false);
|
||||
group.classList.add('rmm-open');
|
||||
group.querySelector(':scope > a.menu')?.setAttribute('aria-expanded', 'true');
|
||||
positionFlyout(group);
|
||||
});
|
||||
menu.addEventListener('focusout', function(event) {
|
||||
if (mobile.matches)
|
||||
return;
|
||||
var group = event.target.closest('li.dropdown');
|
||||
if (group && (!event.relatedTarget || !group.contains(event.relatedTarget)))
|
||||
closeGroups(false);
|
||||
});
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!menu.contains(event.target))
|
||||
closeGroups(false);
|
||||
});
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape' && menu.querySelector('.rmm-open')) {
|
||||
event.preventDefault();
|
||||
closeGroups(true);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', function() {
|
||||
closeGroups(false);
|
||||
scheduleUpdate();
|
||||
});
|
||||
window.addEventListener('scroll', function() { groups().forEach(positionFlyout); }, { capture: true, passive: true });
|
||||
if (window.ResizeObserver)
|
||||
new ResizeObserver(updateNavHeight).observe(nav);
|
||||
new MutationObserver(scheduleUpdate).observe(menu, { childList: true, subtree: true });
|
||||
update();
|
||||
document.body.classList.add('rmm-nav-enhanced');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
else
|
||||
init();
|
||||
})();
|
||||
@@ -0,0 +1,90 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
(function() {
|
||||
'use strict';
|
||||
function init() {
|
||||
var menu = document.getElementById('topmenu'), content = document.getElementById('maincontent');
|
||||
if (!menu || !content || document.getElementById('rmm-menu-search')) return;
|
||||
var dictionaries = {
|
||||
ru: {'Menu search':'Поиск по меню','Search':'Поиск','Close':'Закрыть','Search available pages':'Поиск доступных страниц','No matching pages':'Нет подходящих страниц','Results':'Результаты','Use Up/Down to select, Enter to open, Escape to close.':'Выбор: вверх/вниз. Открыть: Enter. Закрыть: Escape.'},
|
||||
zh: {'Menu search':'菜单搜索','Search':'搜索','Close':'关闭','Search available pages':'搜索可用页面','No matching pages':'没有匹配页面','Results':'结果','Use Up/Down to select, Enter to open, Escape to close.':'使用上下键选择,Enter 打开,Escape 关闭。'}
|
||||
};
|
||||
function text(value) { var language = (document.documentElement.lang || '').split('-')[0];return dictionaries[language] && dictionaries[language][value] || value; }
|
||||
function node(tag, attributes, value) { var el=document.createElement(tag);Object.keys(attributes || {}).forEach(function(key) { el.setAttribute(key,attributes[key]); });if (value) el.textContent=value;return el; }
|
||||
var trigger=node('button',{type:'button','class':'btn',hidden:'','aria-haspopup':'dialog','aria-controls':'rmm-menu-search','aria-keyshortcuts':'Control+k Meta+k'},text('Search') + ' · Ctrl+K');
|
||||
var launch=node('div',{'class':'rmm-search-launcher'});launch.appendChild(trigger);content.prepend(launch);
|
||||
var dialog=node('dialog',{id:'rmm-menu-search','class':'rmm-menu-search','aria-labelledby':'rmm-search-title'});
|
||||
var title=node('h2',{id:'rmm-search-title'},text('Menu search'));
|
||||
var close=node('button',{type:'button','class':'btn'},text('Close'));
|
||||
var heading=node('div',{'class':'rmm-search-heading'});heading.append(title,close);
|
||||
var label=node('label',{for:'rmm-search-query'},text('Search available pages'));
|
||||
var input=node('input',{id:'rmm-search-query',type:'search',autocomplete:'off',maxlength:'128','aria-describedby':'rmm-search-help'});
|
||||
var help=node('p',{id:'rmm-search-help','class':'rmm-search-meta'},text('Use Up/Down to select, Enter to open, Escape to close.'));
|
||||
var status=node('p',{'class':'rmm-search-meta',role:'status','aria-live':'polite'});
|
||||
var results=node('ul',{'class':'rmm-search-results'});
|
||||
dialog.append(heading,label,input,help,status,results);document.body.appendChild(dialog);
|
||||
var entries=[],previousFocus=null,scheduled=false;
|
||||
function indexMenu() {
|
||||
var seen = new Set();entries=[];
|
||||
menu.querySelectorAll('a[href]').forEach(function(link) {
|
||||
if (link.closest('[hidden], [aria-hidden="true"], [aria-disabled="true"]') || link.hasAttribute('disabled')) return;
|
||||
var raw=link.getAttribute('href'),url;
|
||||
if (!raw || raw.charAt(0)==='#') return;
|
||||
try { url=new URL(raw,location.href); } catch (_) { return; }
|
||||
var marker=location.pathname.indexOf('/admin'), root=marker < 0 ? null : location.pathname.slice(0,marker+6);
|
||||
if (root && location.pathname.length > root.length && location.pathname.charAt(root.length)!=='/') root=null;
|
||||
if (!root || url.origin!==location.origin || !url.pathname.startsWith(root+'/') || url.pathname.split('/').includes('logout')) return;
|
||||
var label=(link.getAttribute('aria-label') || link.textContent).trim();
|
||||
if (!label || seen.has(url.href)) return;
|
||||
var parent=link.parentElement.parentElement.closest('li');
|
||||
var group=parent && parent.querySelector(':scope > a');
|
||||
var category=group && (group.getAttribute('aria-label') || group.textContent).trim();
|
||||
var name=category && category!==label ? category + ' / ' + label : label;
|
||||
var synonyms={'dashboard':'обзор дашборд панель сводка','wireless':'вайфай wi-fi wifi беспроводная ssid','network':'сеть lan wan интерфейсы','dhcp':'аренды адреса клиенты leases','dns':'днс резолвер имена','firewall':'файрвол фаервол правила защита','system':'система время часы timezone','flash':'прошивка резервная копия backup обновление','startup':'автозапуск службы сервисы','reboot':'перезагрузка рестарт','package-manager':'пакеты установка opkg apk','admin':'пароль ssh доступ'};
|
||||
var aliases=url.pathname.split('/').slice(4).map(function(part){return synonyms[part] || '';}).join(' ');
|
||||
entries.push({name:name,href:url.href,search:(name+' '+url.pathname+' '+aliases).toLowerCase()});seen.add(url.href);
|
||||
});
|
||||
trigger.hidden=!entries.length || typeof dialog.showModal!=='function';
|
||||
if (dialog.hasAttribute('open')) renderResults();
|
||||
}
|
||||
function renderResults() {
|
||||
var active=document.activeElement,oldHref=results.contains(active) ? active.getAttribute('href') : null;
|
||||
var terms=input.value.trim().toLowerCase().split(/[ ]+/).filter(Boolean);
|
||||
var matching=entries.filter(function(entry) { return terms.every(function(term) { return entry.search.includes(term); }); });
|
||||
results.replaceChildren();
|
||||
matching.slice(0,50).forEach(function(entry) { var li=node('li'),link=node('a',{href:entry.href},entry.name);li.appendChild(link);results.appendChild(li); });
|
||||
var message=matching.length ? text('Results') + ': ' + Math.min(50,matching.length) + ' / ' + matching.length : text('No matching pages');
|
||||
if (status.textContent!==message) status.textContent=message;
|
||||
if (oldHref) { var replacement=Array.from(results.querySelectorAll('a')).find(function(link) { return link.getAttribute('href')===oldHref; });(replacement || input).focus(); }
|
||||
}
|
||||
function open() {
|
||||
if (document.body.classList.contains('modal-overlay-active') || Array.from(document.querySelectorAll('dialog[open]')).some(function(other) { return other!==dialog; })) return;
|
||||
indexMenu();if (trigger.hidden) return;
|
||||
if (!dialog.hasAttribute('open')) { previousFocus=document.activeElement;dialog.showModal(); }
|
||||
input.value='';renderResults();input.focus();
|
||||
}
|
||||
trigger.addEventListener('click',open);close.addEventListener('click',function() { dialog.close(); });
|
||||
dialog.addEventListener('close',function() { if (previousFocus && previousFocus.isConnected) previousFocus.focus(); });
|
||||
input.addEventListener('input',renderResults);
|
||||
dialog.addEventListener('keydown',function(event) {
|
||||
var links=Array.from(results.querySelectorAll('a')),active=document.activeElement,index=links.indexOf(active);
|
||||
if (event.key==='ArrowDown' || event.key==='ArrowUp') {
|
||||
event.preventDefault();if (links.length) links[index < 0 ? event.key==='ArrowDown' ? 0 : links.length-1 : (index+(event.key==='ArrowDown'?1:-1)+links.length)%links.length].focus();
|
||||
}
|
||||
if (event.key==='Enter' && active===input && links.length) { event.preventDefault();links[0].click(); }
|
||||
if (event.key==='Escape') { event.preventDefault();dialog.close(); }
|
||||
if (event.key==='Tab') {
|
||||
var controls=[close,input].concat(links),first=controls[0],last=controls[controls.length-1];
|
||||
if (event.shiftKey && active===first) { event.preventDefault();last.focus(); }
|
||||
else if (!event.shiftKey && active===last) { event.preventDefault();first.focus(); }
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown',function(event) {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase()!=='k') return;
|
||||
if (event.target && event.target.closest('input, textarea, select, [contenteditable]') && !dialog.contains(event.target)) return;
|
||||
if (trigger.hidden || document.body.classList.contains('modal-overlay-active') || Array.from(document.querySelectorAll('dialog[open]')).some(function(other) { return other!==dialog; })) return;event.preventDefault();open();
|
||||
});
|
||||
new MutationObserver(function() { if (!scheduled) { scheduled=true;Promise.resolve().then(function() { scheduled=false;indexMenu(); }); } }).observe(menu,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['href','hidden','aria-hidden','aria-disabled']});
|
||||
indexMenu();
|
||||
}
|
||||
if (document.readyState==='loading') document.addEventListener('DOMContentLoaded',init,{once:true});else init();
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2026 Paweł Kuna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if ! uci -q get luci.themes.RMM >/dev/null 2>&1; then
|
||||
uci set luci.themes.RMM='/luci-static/rmm'
|
||||
uci commit luci
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,14 @@
|
||||
{# Adapted from LuCI Bootstrap. Licensed under the Apache License 2.0. -#}
|
||||
{% if (!blank_page): %}
|
||||
</div>
|
||||
<footer>
|
||||
<span>OpenWrt RMM / <a href="https://github.com/openwrt/luci" target="_blank" rel="noreferrer">{{ version.luciname }} {{ version.luciversion }}</a> / {{ version.distname }} {{ version.distversion }}</span>
|
||||
<ul class="breadcrumb pull-right" id="modemenu" style="display:none"></ul>
|
||||
</footer>
|
||||
<script>L.require('menu-bootstrap')</script>
|
||||
<script src="{{ media }}/navigation.js?v=0.9.0" defer></script>
|
||||
<script src="{{ media }}/components.js?v=0.9.0" defer></script>
|
||||
<script src="{{ media }}/search.js?v=0.9.0" defer></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
{#
|
||||
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||
Copyright 2012 David Menting <david@nut-bolt.nl>
|
||||
Copyright 2008-2022 Jo-Philipp Wich <jo@mein.io>
|
||||
Adapted for OpenWrt RMM.
|
||||
Licensed under the Apache License 2.0.
|
||||
-#}
|
||||
{%
|
||||
import { getuid, getspnam } from 'luci.core';
|
||||
const boardinfo = ubus.call('system', 'board') ?? {};
|
||||
http.prepare_content('text/html; charset=UTF-8');
|
||||
-%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ dispatcher.lang }}" data-darkmode="true">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ striptags(`${boardinfo.hostname ?? '?'}${node?.title ? ` | ${_(node.title)}` : ''}`) }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="darkreader-lock">
|
||||
<link rel="stylesheet" href="{{ media }}/cascade.css?v=0.9.0">
|
||||
<link rel="icon" href="/luci-static/bootstrap/logo.svg" sizes="any">
|
||||
{% 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) }}"></script>
|
||||
<script src="{{ resource }}/cbi.js"></script>
|
||||
</head>
|
||||
<body data-rmm-icons="{{ media }}/icons.svg?v=0.9.0" class="lang_{{ dispatcher.lang }}" data-rmm-route="{{ entityencode(dispatcher.build_url(...ctx.path), true) }}" data-page="{{ entityencode(join('-', length(ctx.request_path) ? ctx.request_path : ctx.path), true) }}">
|
||||
{% if (!blank_page): %}
|
||||
<header>
|
||||
<a class="brand" href="{{ dispatcher.build_url('admin') }}">RMM <span class="rmm-brand-separator">/</span> <span class="rmm-brand-host">{{ striptags(boardinfo.hostname ?? '?') }}</span></a>
|
||||
<nav class="rmm-global-nav" aria-label="{{ _('Main menu') }}"><ul class="nav" id="topmenu" style="display:none"></ul></nav>
|
||||
<div id="indicators" class="pull-right"></div>
|
||||
</header>
|
||||
<div id="maincontent" class="container">
|
||||
{% if (getuid() == 0 && getspnam('root')?.pwdp === ''): %}
|
||||
<div class="alert-message warning">
|
||||
<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 %}
|
||||
{% if (boardinfo.rootfs_type == 'initramfs'): %}
|
||||
<div class="alert-message warning">
|
||||
<h4>{{ _('System running in recovery (initramfs) mode.') }}</h4>
|
||||
<p>{{ _('No changes to settings will be stored and are lost after rebooting. This mode should only be used to install a firmware upgrade') }}</p>
|
||||
{% if (dispatcher.lookup('admin/system/flash')): %}
|
||||
<div class="right"><a class="btn" href="{{ dispatcher.build_url('admin/system/flash') }}">{{ _('Go to firmware upgrade...') }}</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<noscript><div class="alert-message warning"><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>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{# Adapted from LuCI Bootstrap. Licensed under the Apache License 2.0. -#}
|
||||
{% include('header', { blank_page: true }) %}
|
||||
{% const login_board = ubus.call('system', 'board') ?? {}; %}
|
||||
<section hidden>
|
||||
<form method="post" class="cbi-map">
|
||||
<p class="rmm-login-context">RMM / {{ entityencode(login_board.hostname ?? '?', true) }}</p>
|
||||
<div class="cbi-section"><div class="cbi-section-node">
|
||||
<div class="cbi-value"><label class="cbi-value-title" for="luci_username">{{ _('Username') }}</label><div class="cbi-value-field"><input name="luci_username" id="luci_username" type="text" autocomplete="username" value="{{ entityencode(duser, true) }}"></div></div>
|
||||
<div class="cbi-value"><label class="cbi-value-title" for="luci_password">{{ _('Password') }}</label><div class="cbi-value-field"><input name="luci_password" id="luci_password" type="password" autocomplete="current-password"></div></div>
|
||||
{% if (auth_fields): %}
|
||||
{% for (let field in auth_fields): %}
|
||||
<div class="cbi-value"><label class="cbi-value-title" for="{{ field.name }}">{{ _(field.label ?? field.name) }}</label><div class="cbi-value-field"><input name="{{ field.name }}" id="{{ field.name }}" type="{{ field.type ?? 'text' }}" {% if (field.placeholder): %}placeholder="{{ field.placeholder }}"{% endif %} {% if (field.inputmode): %}inputmode="{{ field.inputmode }}"{% endif %} {% if (field.pattern): %}pattern="{{ field.pattern }}"{% endif %} {% if (field.maxlength): %}maxlength="{{ field.maxlength }}"{% endif %} {% if (field.autocomplete): %}autocomplete="{{ field.autocomplete }}"{% endif %} {% if (field.required): %}required{% endif %}></div></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if (auth_html): %}<div class="cbi-value">{{ auth_html }}</div>{% endif %}
|
||||
{% if (auth_assets): %}{% for (let asset in auth_assets): %}{% if (asset.type == 'script'): %}<script src="{{ asset.src }}"></script>{% endif %}{% endfor %}{% endif %}
|
||||
</div></div>
|
||||
</form>
|
||||
<hr>
|
||||
{% if (auth_message): %}<div class="alert-message{% if (auth_plugin): %} warning{% endif %}">{{ auth_message }}</div>
|
||||
{% elif (fuser): %}<div class="alert-message error">{{ _('Invalid username and/or password! Please try again.') }}</div>{% endif %}
|
||||
<button class="btn cbi-button-positive important">{{ _('Log in') }}</button>
|
||||
</section>
|
||||
<div id="view"><div class="spinning">{{ _('Loading view…') }}</div><script>L.require('ui').then(function(ui) { ui.instantiateView('bootstrap.sysauth'); });</script></div>
|
||||
{% include('footer', { blank_page: true }) %}
|
||||
Reference in new issue
Block a user