mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 10:53:56 +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,64 @@
|
||||
#
|
||||
# Copyright (C) 2026 bsatou
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=shadcn/ui Theme for LuCI
|
||||
LUCI_DEPENDS:=
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-theme-shadcnui
|
||||
PKG_VERSION:=0.3.7
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=bsatou
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# Post-install: replace the vanilla sysauth.ut with ours so our login layout
|
||||
# (sidebar-less centered card) is used instead of LuCI's default. Save the
|
||||
# original alongside it so we can restore on uninstall.
|
||||
#
|
||||
# luci-base owns /usr/share/ucode/luci/template/sysauth.ut, so we cannot ship
|
||||
# the override directly inside the package — apk would refuse with a file
|
||||
# overwrite conflict. Instead we ship our copy under the theme's own dir and
|
||||
# do the swap here at install time.
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0 # skip when building image, only run on device
|
||||
|
||||
THEME_SYSAUTH=/usr/share/ucode/luci/template/themes/shadcnui/sysauth.ut
|
||||
LIVE_SYSAUTH=/usr/share/ucode/luci/template/sysauth.ut
|
||||
BACKUP=$${LIVE_SYSAUTH}.shadcnui-backup
|
||||
|
||||
if [ -f "$${THEME_SYSAUTH}" ]; then
|
||||
if [ -f "$${LIVE_SYSAUTH}" ] && [ ! -f "$${BACKUP}" ]; then
|
||||
cp -a "$${LIVE_SYSAUTH}" "$${BACKUP}"
|
||||
fi
|
||||
cp -af "$${THEME_SYSAUTH}" "$${LIVE_SYSAUTH}"
|
||||
fi
|
||||
|
||||
# Drop LuCI caches so the new template is picked up immediately.
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
LIVE_SYSAUTH=/usr/share/ucode/luci/template/sysauth.ut
|
||||
BACKUP=$${LIVE_SYSAUTH}.shadcnui-backup
|
||||
|
||||
if [ -f "$${BACKUP}" ]; then
|
||||
mv -f "$${BACKUP}" "$${LIVE_SYSAUTH}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
exit 0
|
||||
endef
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,335 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require ui';
|
||||
|
||||
/*
|
||||
* luci-theme-shadcnui — menu-shadcnui.js
|
||||
*
|
||||
* Drop-in replacement for menu-bootstrap / menu-argon. Loads the menu tree via
|
||||
* ui.menu.load() and renders it into the sidebar shell that header.ut prepared:
|
||||
*
|
||||
* #shadcn-sidebar
|
||||
* .sidebar-header — brand (rendered server-side)
|
||||
* #shadcn-mainmenu — primary nav (we fill this)
|
||||
* #shadcn-modemenu — secondary "mode" tabs (we fill this when there are >1)
|
||||
* #shadcn-topbar
|
||||
* #shadcn-breadcrumb — current path (we fill this)
|
||||
* #tabmenu — page-local sub-tabs (we fill this when active)
|
||||
*
|
||||
* The menu has 3 conceptual layers:
|
||||
* level 0 — modes (Status / System / Network / Services / ...)
|
||||
* In bootstrap these go into #modemenu; in our layout we only
|
||||
* switch the sidebar's main pane based on the active mode.
|
||||
* level 1 — sections of the active mode (e.g. under "System": System /
|
||||
* Administration / Software / Startup / ...)
|
||||
* level 2 — leaves; clicking them dispatches a view
|
||||
* level 3+ — page-local sub-tabs, rendered into #tabmenu by tabmenu code.
|
||||
*
|
||||
* Layout:
|
||||
* - The first mode's children become collapsible sections in the sidebar.
|
||||
* - Other modes are listed at the bottom of the sidebar (#shadcn-modemenu)
|
||||
* as small chips so the user can switch.
|
||||
*/
|
||||
|
||||
/* --------------- Lucide icon paths by section/page name --------------- */
|
||||
var ICONS = {
|
||||
/* Top-level modes (activeMode children = these section.names) */
|
||||
'status': 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
'system': 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z',
|
||||
'services': 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01',
|
||||
'network': 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064',
|
||||
'tools': 'M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z',
|
||||
'logout': 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
|
||||
|
||||
/* Status submenu items */
|
||||
'overview': 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
|
||||
'routes': 'M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7',
|
||||
'firewall': 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
'iptables': 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
'nftables': 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
'realtime': 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
'processes': 'M4 6h16M4 10h16M4 14h16M4 18h16',
|
||||
'logs': 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
'syslog': 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
'bandwidth': 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6',
|
||||
'mwan3': 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
|
||||
/* System submenu */
|
||||
'admin': 'M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'software': 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4',
|
||||
'packages': 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4',
|
||||
'startup': 'M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664zM21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'crontab': 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'leds': 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z',
|
||||
'flash': 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12',
|
||||
'reboot': 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
|
||||
'poweroff': 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636',
|
||||
|
||||
/* Network submenu */
|
||||
'interfaces': 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
'wireless': 'M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0',
|
||||
'dhcp': 'M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18',
|
||||
'hosts': 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
||||
'diagnostics': 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z',
|
||||
'switch': 'M8 7h12m0 0l-4-4m4 4l-4 4m0 5H4m0 0l4 4m-4-4l4-4',
|
||||
|
||||
/* Services submenu */
|
||||
'upnp': 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
|
||||
'vpn': 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
||||
'ddns': 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
|
||||
'sqm': 'M13 10V3L4 14h7v7l9-11h-7z',
|
||||
'statistics': 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
'nlbw': 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
'amneziawg': 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
||||
'wireguard': 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
||||
'aria2': 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4',
|
||||
'samba': 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z',
|
||||
'docker': 'M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z',
|
||||
'ttyd': 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
'picoclaw': 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
|
||||
/* Fallback */
|
||||
'_chevron': 'M9 18l6-6-6-6',
|
||||
'_default': 'M4 6h16M4 12h16M4 18h16'
|
||||
};
|
||||
|
||||
function makeSvg(name, cls) {
|
||||
var d = ICONS[name] || ICONS['_default'];
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
var svg = document.createElementNS(NS, 'svg');
|
||||
svg.setAttribute('class', cls || 'icon');
|
||||
svg.setAttribute('viewBox', '0 0 24 24'); /* camelCase — required for SVG */
|
||||
svg.setAttribute('fill', 'none');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
svg.setAttribute('stroke-width', '1.75');
|
||||
svg.setAttribute('stroke-linecap', 'round');
|
||||
svg.setAttribute('stroke-linejoin', 'round');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
var path = document.createElementNS(NS, 'path');
|
||||
path.setAttribute('d', d);
|
||||
svg.appendChild(path);
|
||||
return svg;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
__init__: function () {
|
||||
ui.menu.load().then(L.bind(this.render, this));
|
||||
},
|
||||
|
||||
render: function (tree) {
|
||||
/* LuCI's handleSave/handleApply look for #maincontent to find forms.
|
||||
Wrap the rendered #view inside a #maincontent div if not present. */
|
||||
if (!document.getElementById('maincontent')) {
|
||||
var view = document.getElementById('view');
|
||||
if (view) {
|
||||
var mc = document.createElement('div');
|
||||
mc.id = 'maincontent';
|
||||
view.parentNode.insertBefore(mc, view);
|
||||
mc.appendChild(view);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderModeSwitcher(tree);
|
||||
this.renderSidebarForActiveMode(tree);
|
||||
this.renderBreadcrumb(tree);
|
||||
this.renderTabMenu(tree);
|
||||
this.fixApplyDropdown();
|
||||
},
|
||||
|
||||
/* Fix LuCI's apply dropdown */
|
||||
fixApplyDropdown: function () {},
|
||||
|
||||
/* --------------- Mode switcher (top-level) --------------- */
|
||||
|
||||
renderModeSwitcher: function (tree) {
|
||||
var container = document.querySelector('#shadcn-modemenu');
|
||||
if (!container) return;
|
||||
|
||||
var children = ui.menu.getChildren(tree);
|
||||
if (children.length <= 1) return;
|
||||
|
||||
var requestpath = L.env.requestpath || [];
|
||||
var activeName = requestpath.length ? requestpath[0] : (children[0] && children[0].name);
|
||||
|
||||
container.innerHTML = '';
|
||||
children.forEach(function (child) {
|
||||
var isActive = (child.name === activeName);
|
||||
var a = E('a', {
|
||||
'class': 'mode-chip' + (isActive ? ' active' : ''),
|
||||
'href': L.url(child.name)
|
||||
}, [ _(child.title) ]);
|
||||
container.appendChild(a);
|
||||
});
|
||||
},
|
||||
|
||||
/* --------------- Sidebar nav for the active mode --------------- */
|
||||
|
||||
renderSidebarForActiveMode: function (tree) {
|
||||
var container = document.querySelector('#shadcn-mainmenu');
|
||||
if (!container) return;
|
||||
|
||||
var topChildren = ui.menu.getChildren(tree);
|
||||
if (topChildren.length === 0) return;
|
||||
|
||||
var requestpath = L.env.requestpath || [];
|
||||
var dispatchpath = L.env.dispatchpath || [];
|
||||
|
||||
// Pick the active mode (or first if no path).
|
||||
var activeMode = null;
|
||||
if (requestpath.length) {
|
||||
for (var i = 0; i < topChildren.length; i++) {
|
||||
if (topChildren[i].name === requestpath[0]) {
|
||||
activeMode = topChildren[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!activeMode) activeMode = topChildren[0];
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
var sections = ui.menu.getChildren(activeMode);
|
||||
var self = this;
|
||||
|
||||
console.log('[shadcnui] activeMode.name:', activeMode.name);
|
||||
sections.forEach(function (section) {
|
||||
console.log('[shadcnui] section.name:', section.name, 'title:', section.title);
|
||||
var sectionUrl = activeMode.name + '/' + section.name;
|
||||
var leaves = ui.menu.getChildren(section);
|
||||
var isActiveSection = (dispatchpath[1] === section.name);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// Collapsible section
|
||||
var div = E('div', {
|
||||
'class': 'sidebar-section',
|
||||
'data-open': isActiveSection ? 'true' : 'false'
|
||||
});
|
||||
|
||||
var btn = E('button', {
|
||||
'class': 'sidebar-toggle' + (isActiveSection ? ' active' : ''),
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(self, 'handleSectionToggle')
|
||||
}, [
|
||||
makeSvg(section.name, 'icon'),
|
||||
E('span', {}, [ _(section.title) ]),
|
||||
makeSvg('_chevron', 'chevron')
|
||||
]);
|
||||
|
||||
var childrenWrap = E('div', { 'class': 'sidebar-children' });
|
||||
leaves.forEach(function (leaf) {
|
||||
var leafUrl = sectionUrl + '/' + leaf.name;
|
||||
var isActiveLeaf = (dispatchpath[1] === section.name && dispatchpath[2] === leaf.name);
|
||||
var a = E('a', {
|
||||
'class': 'sidebar-link' + (isActiveLeaf ? ' active' : ''),
|
||||
'href': L.url(leafUrl)
|
||||
}, [ E('span', {}, [ _(leaf.title) ]) ]);
|
||||
childrenWrap.appendChild(a);
|
||||
});
|
||||
|
||||
div.appendChild(btn);
|
||||
div.appendChild(childrenWrap);
|
||||
container.appendChild(div);
|
||||
}
|
||||
else {
|
||||
// Direct link
|
||||
var a = E('a', {
|
||||
'class': 'sidebar-link sidebar-link-top' + (isActiveSection ? ' active' : ''),
|
||||
'href': L.url(sectionUrl)
|
||||
}, [
|
||||
makeSvg(section.name, 'icon'),
|
||||
E('span', {}, [ _(section.title) ])
|
||||
]);
|
||||
container.appendChild(a);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleSectionToggle: function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
var section = btn.closest('.sidebar-section');
|
||||
if (!section) return;
|
||||
var open = section.getAttribute('data-open') === 'true';
|
||||
section.setAttribute('data-open', open ? 'false' : 'true');
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
/* --------------- Breadcrumb in the topbar --------------- */
|
||||
|
||||
renderBreadcrumb: function (tree) {
|
||||
var container = document.querySelector('#shadcn-breadcrumb');
|
||||
if (!container) return;
|
||||
|
||||
var dispatchpath = L.env.dispatchpath || [];
|
||||
if (dispatchpath.length === 0) return;
|
||||
|
||||
var node = tree;
|
||||
var crumbs = [];
|
||||
var url = '';
|
||||
|
||||
for (var i = 0; i < dispatchpath.length && node; i++) {
|
||||
var seg = dispatchpath[i];
|
||||
node = node.children && node.children[seg];
|
||||
if (!node) break;
|
||||
url = url + (url ? '/' : '') + seg;
|
||||
crumbs.push({ title: node.title, url: url });
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
crumbs.forEach(function (c, idx) {
|
||||
if (idx > 0) container.appendChild(E('span', { 'class': 'crumb-sep' }, [ '/' ]));
|
||||
if (idx === crumbs.length - 1) {
|
||||
container.appendChild(E('span', { 'class': 'crumb-current' }, [ _(c.title) ]));
|
||||
} else {
|
||||
container.appendChild(E('a', { 'href': L.url(c.url) }, [ _(c.title) ]));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* --------------- Page-local sub-tabs in #tabmenu --------------- */
|
||||
|
||||
renderTabMenu: function (tree) {
|
||||
var container = document.querySelector('#tabmenu');
|
||||
if (!container) return;
|
||||
|
||||
var dispatchpath = L.env.dispatchpath || [];
|
||||
if (dispatchpath.length < 3) return;
|
||||
|
||||
var node = tree;
|
||||
var url = '';
|
||||
for (var i = 0; i < 3 && node; i++) {
|
||||
node = node.children && node.children[dispatchpath[i]];
|
||||
url = url + (url ? '/' : '') + dispatchpath[i];
|
||||
}
|
||||
if (!node) return;
|
||||
|
||||
this._renderTabLevel(node, url, 0, container);
|
||||
},
|
||||
|
||||
_renderTabLevel: function (node, url, level, container) {
|
||||
var children = ui.menu.getChildren(node);
|
||||
if (children.length === 0) return;
|
||||
|
||||
var ul = E('ul', { 'class': 'cbi-tabmenu' });
|
||||
var activeNode = null;
|
||||
|
||||
var dispatchpath = L.env.dispatchpath || [];
|
||||
children.forEach(function (child) {
|
||||
var isActive = (dispatchpath[3 + level] === child.name);
|
||||
var li = E('li', { 'class': isActive ? 'cbi-tab' : 'cbi-tab-disabled' }, [
|
||||
E('a', { 'href': L.url(url, child.name) }, [ _(child.title) ])
|
||||
]);
|
||||
ul.appendChild(li);
|
||||
if (isActive) activeNode = child;
|
||||
});
|
||||
|
||||
container.appendChild(ul);
|
||||
container.style.display = '';
|
||||
|
||||
if (activeNode) {
|
||||
this._renderTabLevel(activeNode, url + '/' + activeNode.name, level + 1, container);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* shadcn/ui design tokens for LuCI
|
||||
* Values mirror ui.shadcn.com defaults (HSL, no commas inside hsl()).
|
||||
*
|
||||
* data-base on <html> picks neutral palette: zinc | slate | stone | gray | neutral
|
||||
* data-theme on <html> picks light/dark
|
||||
* --primary, --primary-foreground may be overridden inline by header.ut from UCI
|
||||
*/
|
||||
|
||||
/* ============================================================
|
||||
* BASE: ZINC (default)
|
||||
* ============================================================ */
|
||||
:root,
|
||||
:root[data-base="zinc"] {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* status / chart colors used by LuCI tables, network state, etc. */
|
||||
--success: 142 71% 45%;
|
||||
--warning: 38 92% 50%;
|
||||
--info: 199 89% 48%;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"][data-base="zinc"],
|
||||
:root[data-base="zinc"][data-theme="dark"] {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* BASE: SLATE
|
||||
* ============================================================ */
|
||||
:root[data-base="slate"] {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
}
|
||||
|
||||
:root[data-base="slate"][data-theme="dark"] {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* BASE: STONE
|
||||
* ============================================================ */
|
||||
:root[data-base="stone"] {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
}
|
||||
|
||||
:root[data-base="stone"][data-theme="dark"] {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* BASE: GRAY
|
||||
* ============================================================ */
|
||||
:root[data-base="gray"] {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 220.9 39.3% 11%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 224 71.4% 4.1%;
|
||||
}
|
||||
|
||||
:root[data-base="gray"][data-theme="dark"] {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 210 20% 98%;
|
||||
--primary-foreground: 220.9 39.3% 11%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 216 12.2% 83.9%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* BASE: NEUTRAL
|
||||
* ============================================================ */
|
||||
:root[data-base="neutral"] {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
}
|
||||
|
||||
:root[data-base="neutral"][data-theme="dark"] {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
|
||||
/* status colors stay consistent across all bases (only tweaked in dark) */
|
||||
:root[data-theme="dark"] {
|
||||
--success: 142 64% 50%;
|
||||
--warning: 35 91% 55%;
|
||||
--info: 199 89% 60%;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#18181b"/>
|
||||
<text x="16" y="22" text-anchor="middle"
|
||||
font-family="ui-sans-serif, system-ui, sans-serif"
|
||||
font-size="18" font-weight="700" fill="#fafafa">S</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* luci-theme-shadcnui — sidebar.js
|
||||
*
|
||||
* Sidebar is always visible on desktop. On mobile (≤768px) the burger button
|
||||
* in the topbar toggles a slide-in drawer; the dark overlay closes it.
|
||||
*
|
||||
* Section toggle and active-link highlighting are handled by menu-shadcnui.js
|
||||
* because that module is responsible for rendering the menu DOM.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var app = document.getElementById('shadcn-app');
|
||||
if (!app) return; // login page or other unrelated layout
|
||||
|
||||
function applyState(state) {
|
||||
// state: 'expanded' (mobile closed / desktop default) | 'open' (mobile drawer)
|
||||
app.setAttribute('data-sidebar', state);
|
||||
}
|
||||
|
||||
applyState('expanded');
|
||||
|
||||
var toggle = document.getElementById('shadcn-sidebar-toggle');
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var current = app.getAttribute('data-sidebar') || 'expanded';
|
||||
applyState(current === 'open' ? 'expanded' : 'open');
|
||||
});
|
||||
}
|
||||
|
||||
var overlay = document.getElementById('shadcn-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function () {
|
||||
applyState('expanded');
|
||||
});
|
||||
}
|
||||
|
||||
// Close the drawer if the viewport grows past the breakpoint.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.matchMedia('(min-width: 769px)').matches &&
|
||||
app.getAttribute('data-sidebar') === 'open') {
|
||||
applyState('expanded');
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* luci-theme-shadcnui — theme.js
|
||||
* Reads UCI-derived <html data-*> attributes, applies system preference when
|
||||
* needed, and persists user overrides to localStorage as a fast path before
|
||||
* the next page load (the source of truth stays /etc/config/shadcnui).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var root = document.documentElement;
|
||||
var LS_KEY = 'shadcnui:overrides';
|
||||
|
||||
function readOverrides() {
|
||||
try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); }
|
||||
catch (e) { return {}; }
|
||||
}
|
||||
function writeOverrides(o) {
|
||||
try { localStorage.setItem(LS_KEY, JSON.stringify(o || {})); }
|
||||
catch (e) { /* ignore quota / private mode */ }
|
||||
}
|
||||
|
||||
function applyMode(mode) {
|
||||
// mode: 'light' | 'dark' | 'system'
|
||||
if (mode === 'system') {
|
||||
var prefersDark = window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
root.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
||||
} else {
|
||||
root.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
|
||||
}
|
||||
root.setAttribute('data-mode', mode);
|
||||
}
|
||||
|
||||
function applyBase(base) {
|
||||
var allowed = ['zinc', 'slate', 'stone', 'gray', 'neutral'];
|
||||
if (allowed.indexOf(base) === -1) base = 'zinc';
|
||||
root.setAttribute('data-base', base);
|
||||
}
|
||||
|
||||
function applyAccent(hsl) {
|
||||
// hsl: "240 5.9% 10%" or null to reset
|
||||
if (!hsl) {
|
||||
root.style.removeProperty('--primary');
|
||||
root.style.removeProperty('--ring');
|
||||
return;
|
||||
}
|
||||
root.style.setProperty('--primary', hsl);
|
||||
root.style.setProperty('--ring', hsl);
|
||||
}
|
||||
|
||||
function applyLoginBg(url) {
|
||||
if (!url) {
|
||||
root.style.removeProperty('--login-bg');
|
||||
return;
|
||||
}
|
||||
root.style.setProperty('--login-bg', 'url("' + url.replace(/"/g, '\\"') + '")');
|
||||
}
|
||||
|
||||
// Public API exposed so config page can preview live.
|
||||
window.shadcnuiTheme = {
|
||||
setMode: function (mode) {
|
||||
var o = readOverrides(); o.mode = mode; writeOverrides(o);
|
||||
applyMode(mode);
|
||||
},
|
||||
setBase: function (base) {
|
||||
var o = readOverrides(); o.base = base; writeOverrides(o);
|
||||
applyBase(base);
|
||||
},
|
||||
setAccent: function (hsl) {
|
||||
var o = readOverrides(); o.accent = hsl; writeOverrides(o);
|
||||
applyAccent(hsl);
|
||||
},
|
||||
setLoginBg: function (url) {
|
||||
var o = readOverrides(); o.loginBg = url; writeOverrides(o);
|
||||
applyLoginBg(url);
|
||||
},
|
||||
getMode: function () { return root.getAttribute('data-mode') || 'system'; },
|
||||
getResolved: function () { return root.getAttribute('data-theme') || 'light'; }
|
||||
};
|
||||
|
||||
// Boot: server-rendered attrs are authoritative on first paint.
|
||||
// We only re-evaluate "system" against the live media query and listen for
|
||||
// OS-level changes so the UI follows the OS without a page reload.
|
||||
var initialMode = root.getAttribute('data-mode') || 'system';
|
||||
if (initialMode === 'system') applyMode('system');
|
||||
|
||||
if (window.matchMedia) {
|
||||
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var listener = function () {
|
||||
if ((root.getAttribute('data-mode') || 'system') === 'system') {
|
||||
applyMode('system');
|
||||
}
|
||||
};
|
||||
if (mql.addEventListener) mql.addEventListener('change', listener);
|
||||
else if (mql.addListener) mql.addListener(listener);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
config shadcnui 'main'
|
||||
option mode 'system'
|
||||
option base 'zinc'
|
||||
option accent ''
|
||||
option radius '0.5rem'
|
||||
option bg_source 'none'
|
||||
option bg_url ''
|
||||
|
||||
config bing 'bing'
|
||||
option enabled '0'
|
||||
option market 'auto'
|
||||
option cache_ttl '86400'
|
||||
|
||||
config unsplash 'unsplash'
|
||||
option enabled '0'
|
||||
option access_key ''
|
||||
option query 'nature'
|
||||
|
||||
config wallhaven 'wallhaven'
|
||||
option enabled '0'
|
||||
option api_key ''
|
||||
option query 'minimal'
|
||||
option purity '100'
|
||||
option categories '111'
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<EOF >/dev/null
|
||||
set luci.themes.shadcnui=/luci-static/shadcnui
|
||||
set luci.main.mediaurlbase=/luci-static/shadcnui
|
||||
commit luci
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
killall -HUP rpcd 2>/dev/null
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
{#
|
||||
luci-theme-shadcnui — footer.ut
|
||||
Closes the layout opened by header.ut.
|
||||
-#}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
<script>L.require('menu-shadcnui')</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{#
|
||||
luci-theme-shadcnui — footer_login.ut
|
||||
-#}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
{#
|
||||
luci-theme-shadcnui — header.ut
|
||||
Used for every authenticated LuCI page. The login screen is rendered by
|
||||
sysauth.ut (which overrides /usr/share/ucode/luci/template/sysauth.ut) and
|
||||
includes header_login.ut / footer_login.ut instead, so this file does NOT
|
||||
need to detect the login state.
|
||||
-#}
|
||||
|
||||
{%
|
||||
import { access } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const boardinfo = ubus.call('system', 'board');
|
||||
const hostname = striptags(boardinfo?.hostname ?? 'OpenWrt');
|
||||
|
||||
let cfg = cursor();
|
||||
|
||||
let mode = 'system';
|
||||
let base = 'zinc';
|
||||
let accent = '';
|
||||
let radius = '0.5rem';
|
||||
let bg_url = '';
|
||||
|
||||
if (access('/etc/config/shadcnui')) {
|
||||
mode = cfg.get('shadcnui', 'main', 'mode') ?? mode;
|
||||
base = cfg.get('shadcnui', 'main', 'base') ?? base;
|
||||
accent = cfg.get('shadcnui', 'main', 'accent') ?? '';
|
||||
radius = cfg.get('shadcnui', 'main', 'radius') ?? radius;
|
||||
bg_url = cfg.get('shadcnui', 'main', 'bg_url') ?? '';
|
||||
}
|
||||
|
||||
let resolved = (mode == 'dark') ? 'dark' : 'light';
|
||||
|
||||
let style_overrides = '';
|
||||
if (accent) style_overrides += `--primary:${accent};--ring:${accent};`;
|
||||
if (radius != '0.5rem') style_overrides += `--radius:${radius};`;
|
||||
if (bg_url) style_overrides += `--login-bg:url('${replace(bg_url, "'", "\\'")}');`;
|
||||
|
||||
http.prepare_content('text/html; charset=UTF-8');
|
||||
-%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ dispatcher.lang }}"
|
||||
data-base="{{ base }}"
|
||||
data-mode="{{ mode }}"
|
||||
data-theme="{{ resolved }}"
|
||||
style="{{ style_overrides }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="darkreader-lock">
|
||||
<title>{{ hostname }}{{ node?.title ? ` — ${striptags(node.title)}` : '' }} — LuCI</title>
|
||||
|
||||
<link rel="stylesheet" href="{{ media }}/css/shadcnui.css">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ media }}/icons/favicon.svg">
|
||||
|
||||
{% if (node?.css): %}
|
||||
<link rel="stylesheet" href="{{ resource }}/{{ node.css }}">
|
||||
{% endif -%}
|
||||
{% if (css): %}
|
||||
<style type="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>
|
||||
<script src="{{ media }}/js/theme.js" defer></script>
|
||||
<script src="{{ media }}/js/sidebar.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="lang_{{ dispatcher.lang }}"
|
||||
data-page="{{ entityencode(join('-', ctx.request_path), true) }}">
|
||||
|
||||
<div id="shadcn-app" data-sidebar="expanded">
|
||||
|
||||
<aside id="shadcn-sidebar" aria-label="Main navigation">
|
||||
<div class="sidebar-header">
|
||||
<a class="brand" href="/">
|
||||
<div class="logo" aria-hidden="true">{{ substr(uc(hostname), 0, 2) }}</div>
|
||||
<span>{{ hostname }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="sidebar-nav" id="shadcn-mainmenu" role="navigation"></nav>
|
||||
<div class="sidebar-footer" id="shadcn-modemenu"></div>
|
||||
</aside>
|
||||
|
||||
<div id="shadcn-overlay" class="darkMask"></div>
|
||||
|
||||
<header id="shadcn-topbar">
|
||||
<button class="topbar-toggle showSide" id="shadcn-sidebar-toggle"
|
||||
type="button" aria-label="Toggle sidebar">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="topbar-breadcrumb" id="shadcn-breadcrumb"></div>
|
||||
<div id="indicators" class="topbar-actions"></div>
|
||||
</header>
|
||||
|
||||
<main id="shadcn-main">
|
||||
<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" class="cbi-tabmenu" style="display:none"></div>
|
||||
@@ -0,0 +1,53 @@
|
||||
{#
|
||||
luci-theme-shadcnui — header_login.ut
|
||||
Renders the <head> and opens the <body> for the login page only. The actual
|
||||
login form and a closing <body>/<html> are provided by the global sysauth.ut
|
||||
(which we override) and by footer_login.ut.
|
||||
-#}
|
||||
|
||||
{%
|
||||
import { access } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const boardinfo = ubus.call('system', 'board');
|
||||
const hostname = striptags(boardinfo?.hostname ?? 'OpenWrt');
|
||||
|
||||
let cfg = cursor();
|
||||
let mode = 'system', base = 'zinc', accent = '', radius = '0.5rem', bg_url = '';
|
||||
|
||||
if (access('/etc/config/shadcnui')) {
|
||||
mode = cfg.get('shadcnui', 'main', 'mode') ?? mode;
|
||||
base = cfg.get('shadcnui', 'main', 'base') ?? base;
|
||||
accent = cfg.get('shadcnui', 'main', 'accent') ?? '';
|
||||
radius = cfg.get('shadcnui', 'main', 'radius') ?? radius;
|
||||
bg_url = cfg.get('shadcnui', 'main', 'bg_url') ?? '';
|
||||
}
|
||||
|
||||
let resolved = (mode == 'dark') ? 'dark' : 'light';
|
||||
|
||||
let style_overrides = '';
|
||||
if (accent) style_overrides += `--primary:${accent};--ring:${accent};`;
|
||||
if (radius != '0.5rem') style_overrides += `--radius:${radius};`;
|
||||
if (bg_url) style_overrides += `--login-bg:url('${replace(bg_url, "'", "\\'")}');`;
|
||||
|
||||
http.prepare_content('text/html; charset=UTF-8');
|
||||
-%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ dispatcher.lang }}"
|
||||
data-base="{{ base }}"
|
||||
data-mode="{{ mode }}"
|
||||
data-theme="{{ resolved }}"
|
||||
style="{{ style_overrides }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="darkreader-lock">
|
||||
<title>{{ hostname }} — LuCI</title>
|
||||
|
||||
<link rel="stylesheet" href="{{ media }}/css/shadcnui.css">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ media }}/icons/favicon.svg">
|
||||
|
||||
<script src="{{ media }}/js/theme.js" defer></script>
|
||||
</head>
|
||||
<body class="lang_{{ dispatcher.lang }} sysauth">
|
||||
@@ -0,0 +1,13 @@
|
||||
{#
|
||||
luci-theme-shadcnui — out_header_login.ut
|
||||
Idempotent header include used by sysauth.ut (the per-theme login page).
|
||||
Pattern copied from luci-theme-argon — guards against double-include via
|
||||
ctx.template_header_sent.
|
||||
-#}
|
||||
|
||||
{%
|
||||
if (!ctx.template_header_sent) {
|
||||
include("themes/" + theme + "/header_login");
|
||||
ctx.template_header_sent = true;
|
||||
}
|
||||
%}
|
||||
@@ -0,0 +1,82 @@
|
||||
{#
|
||||
luci-theme-shadcnui — global sysauth.ut override
|
||||
|
||||
Replaces /usr/share/ucode/luci/template/sysauth.ut from luci-base. Pattern
|
||||
copied from luci-theme-argon: include the theme's header_login wrapper,
|
||||
render the login form, then include footer_login.
|
||||
|
||||
The runtime exposes:
|
||||
theme — basename of mediaurlbase (e.g. 'shadcnui')
|
||||
media — full mediaurlbase URL (e.g. '/luci-static/shadcnui')
|
||||
duser — default username (usually 'root')
|
||||
fuser — non-empty if the previous attempt failed
|
||||
-#}
|
||||
|
||||
{% include("themes/" + theme + "/out_header_login") %}
|
||||
|
||||
{%
|
||||
import { access } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const boardinfo = ubus.call('system', 'board');
|
||||
const hostname = striptags(boardinfo?.hostname ?? 'OpenWrt');
|
||||
const initials = substr(uc(hostname), 0, 2);
|
||||
|
||||
let cfg = cursor();
|
||||
let bg_url = '';
|
||||
if (access('/etc/config/shadcnui')) {
|
||||
bg_url = cfg.get('shadcnui', 'main', 'bg_url') ?? '';
|
||||
}
|
||||
%}
|
||||
|
||||
<main id="login-root">
|
||||
|
||||
{% if (bg_url): %}
|
||||
<div class="login-bg" style="background-image:url('{{ bg_url }}')"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="sysauth-card">
|
||||
<div class="sysauth-brand">
|
||||
<div class="logo" aria-hidden="true">{{ initials }}</div>
|
||||
<div>
|
||||
<h2>{{ hostname }}</h2>
|
||||
<p class="muted">{{ _('Please sign in to manage this device.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ http.getenv('REQUEST_URI') }}">
|
||||
|
||||
{% if (fuser): %}
|
||||
<div class="alert-message error">
|
||||
{{ _('Invalid username and/or password! Please try again.') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="cbi-value">
|
||||
<label for="cbi-input-user">{{ _('Username') }}</label>
|
||||
<input id="cbi-input-user" type="text" name="luci_username"
|
||||
value="{{ entityencode(duser, true) }}"
|
||||
autocomplete="username" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value">
|
||||
<label for="cbi-input-password">{{ _('Password') }}</label>
|
||||
<input id="cbi-input-password" type="password" name="luci_password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="cbi-page-actions">
|
||||
<input type="submit" class="btn cbi-button cbi-button-apply"
|
||||
value="{{ _('Log in') }}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var pw = document.getElementsByName('luci_password')[0];
|
||||
if (pw) pw.focus();
|
||||
</script>
|
||||
|
||||
</main>
|
||||
|
||||
{% include("themes/" + theme + "/footer_login") %}
|
||||
Reference in new issue
Block a user