mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
🏅 Sync 2026-06-15 22:09:15
This commit is contained in:
1 parent
fae6bd4c8a
commit
d30e14c6e7
68 files changed
+2964
-918
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=autocore
|
||||
PKG_FLAGS:=nonshared
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_TARGET_bcm27xx \
|
||||
|
||||
@@ -9,11 +9,6 @@ start() {
|
||||
|
||||
sysctl -w net.core.rps_sock_flow_entries="$(( rfc * threads ))"
|
||||
|
||||
for fileRps in /sys/class/net/eth*/queues/rx-*/rps_cpus
|
||||
do
|
||||
echo "$threads" > "$fileRps"
|
||||
done
|
||||
|
||||
for fileRfc in /sys/class/net/eth*/queues/rx-*/rps_flow_cnt
|
||||
do
|
||||
echo "$rfc" > "$fileRfc"
|
||||
|
||||
+3
-2
@@ -6,11 +6,11 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudreve
|
||||
PKG_VERSION:=4.16.1
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git
|
||||
PKG_SOURCE_VERSION:=26b6b1044b0253c5ead7d5a90eaa10ff67ac2582
|
||||
PKG_SOURCE_VERSION:=ba2e870bbd17f1918dd2321de861e453f696d6a3
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
@@ -23,6 +23,7 @@ PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/cloudreve/Cloudreve/v4
|
||||
GO_PKG_EXCLUDES:=pkg/request/ssrftest
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/application/constants.BackendVersion=$(PKG_VERSION) \
|
||||
$(GO_PKG)/application/constants.LastCommit=$(PKG_VERSION)
|
||||
|
||||
+3
-3
@@ -3,10 +3,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.06.08
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=2026.06.14
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE:=dae-src-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
+3
-3
@@ -3,10 +3,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.06.08
|
||||
PKG_RELEASE:=7
|
||||
PKG_VERSION:=2026.06.14
|
||||
PKG_RELEASE:=8
|
||||
|
||||
PKG_SOURCE:=daed-src-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.12.0
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const CSS = [
|
||||
'.dd-up-icon{font-size:14px;text-align:center;line-height:1}',
|
||||
'.dd-up-ok{color:#3da66a}',
|
||||
'.dd-up-warn{color:#d39e00}',
|
||||
'.dd-up-new{color:#4a8cff}',
|
||||
'.dd-up-err{color:#d96d6d}',
|
||||
'.dd-up-name{font-weight:600;opacity:.85}',
|
||||
'.dd-up-meta{opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
@@ -101,17 +102,10 @@ function probePkg(pkg) {
|
||||
});
|
||||
}
|
||||
|
||||
function tailLog(path, into) {
|
||||
return L.resolveDefault(fs.read_direct(path, 'text'), '').then(function(content) {
|
||||
if (content) {
|
||||
into.textContent = content;
|
||||
into.classList.add('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
// background apk update so new versions show on next poll
|
||||
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {});
|
||||
return Promise.all([
|
||||
backend.detectBackend(),
|
||||
uci.load('daed').catch(function() {})
|
||||
@@ -145,35 +139,48 @@ return view.extend({
|
||||
]);
|
||||
};
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
const label = kind === 'geoip' ? 'GeoIP' : 'GeoSite';
|
||||
// run a backgrounded script, stream its log, then toast the result.
|
||||
// Scripts end their log with "done (rc=N)"; poll until that appears.
|
||||
const runJob = function(script, arg, btn, logPath, label) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-geo.sh', [kind]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.' + kind + '.log', logPane); }, 2000);
|
||||
}
|
||||
let tries = 0;
|
||||
const poll = function() {
|
||||
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
||||
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
|
||||
const m = c.match(/done \(rc=(\d+)\)/);
|
||||
if (m) {
|
||||
const ok = m[1] === '0';
|
||||
ui.addNotification(null, E('p', label + ' · ' + (ok ? _('done') : _('failed'))), ok ? null : 'danger');
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
if (tries++ > 90) return;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
||||
});
|
||||
};
|
||||
return fs.exec('/usr/share/luci-app-daede/' + script, [arg]).then(function(res) {
|
||||
if (res.code !== 0) { ui.addNotification(null, E('p', label + ' · ' + _('failed to start')), 'danger'); return; }
|
||||
return poll();
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Update');
|
||||
btn.textContent = orig;
|
||||
});
|
||||
};
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
return runJob('update-geo.sh', kind, btn,
|
||||
'/tmp/luci-app-daede.' + kind + '.log', kind === 'geoip' ? 'GeoIP' : 'GeoSite');
|
||||
};
|
||||
|
||||
// === Package updates (dae|daed / luci-app-daede) ===
|
||||
const upgradePkg = function(pkg, btn) {
|
||||
if (!confirm(_('Run "apk upgrade %s" now? This may restart the active backend.').format(pkg)))
|
||||
if (!confirm(_('Upgrade %s now? This may restart the active backend.').format(pkg)))
|
||||
return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-pkg.sh', [pkg]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.pkg-' + pkg + '.log', logPane); }, 3000);
|
||||
}
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Upgrade');
|
||||
});
|
||||
return runJob('update-pkg.sh', pkg, btn,
|
||||
'/tmp/luci-app-daede.pkg-' + pkg + '.log', pkg);
|
||||
};
|
||||
|
||||
const refresh = function() {
|
||||
@@ -256,8 +263,8 @@ return view.extend({
|
||||
btn.disabled = true;
|
||||
}
|
||||
pkgBody.appendChild(mkRow(
|
||||
updatable ? '⚠' : (entry.r.installed ? '✓' : '✗'),
|
||||
updatable ? 'dd-up-warn' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
|
||||
updatable ? '↑' : (entry.r.installed ? '✓' : '✗'),
|
||||
updatable ? 'dd-up-new' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Background apk/opkg update for the Updates view. Returns at once; throttled.
|
||||
|
||||
LOCK=/tmp/luci-app-daede.idx.lock
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
|
||||
fi
|
||||
: > "$LOCK"
|
||||
|
||||
(
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk update
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
opkg update
|
||||
fi
|
||||
) >/dev/null 2>&1 </dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -44,6 +44,7 @@ fi
|
||||
exec >"$LOG" 2>&1
|
||||
trap 'rm -f "$LOCK"' EXIT INT TERM
|
||||
|
||||
rc=0
|
||||
TMP="${DEST}.new"
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
echo "$(date '+%F %T') begin: $URL"
|
||||
@@ -51,18 +52,19 @@ fi
|
||||
if ! curl -fsSL --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
|
||||
echo "$(date '+%F %T') download failed"
|
||||
rm -f "$TMP"
|
||||
exit 1
|
||||
rc=1
|
||||
else
|
||||
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
|
||||
if [ "$size" -lt 102400 ]; then
|
||||
echo "$(date '+%F %T') file too small ($size bytes)"
|
||||
rm -f "$TMP"
|
||||
rc=2
|
||||
else
|
||||
mv "$TMP" "$DEST"
|
||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||
fi
|
||||
fi
|
||||
|
||||
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
|
||||
if [ "$size" -lt 102400 ]; then
|
||||
echo "$(date '+%F %T') file too small ($size bytes)"
|
||||
rm -f "$TMP"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mv "$TMP" "$DEST"
|
||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||
echo "$(date '+%F %T') done (rc=$rc)"
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
|
||||
@@ -40,10 +40,23 @@ fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo "--- apk update ---"
|
||||
apk update 2>&1
|
||||
echo "--- apk upgrade $PKG ---"
|
||||
# --no-self-upgrade so apk itself does not jump versions mid-op
|
||||
apk upgrade --no-self-upgrade "$PKG" 2>&1
|
||||
rc=$?
|
||||
echo "--- apk add $PKG ---"
|
||||
# Target only $PKG (not `apk upgrade`, which re-solves the whole world).
|
||||
apk add "$PKG" 2>&1
|
||||
# Don't trust apk's exit code: apk-tools 3 returns non-zero whenever ANY
|
||||
# unrelated installed package has an unavailable .apk in the configured
|
||||
# feeds, even when $PKG itself upgraded fine. Judge success by state —
|
||||
# $PKG must be installed and have no pending upgrade left.
|
||||
if ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
|
||||
echo "result: $PKG is not installed"
|
||||
rc=1
|
||||
elif apk list -u 2>/dev/null | grep -q "^${PKG}-"; then
|
||||
echo "result: $PKG still has a pending upgrade"
|
||||
rc=1
|
||||
else
|
||||
echo "result: $PKG is at the latest available version"
|
||||
rc=0
|
||||
fi
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
echo "--- opkg update ---"
|
||||
opkg update 2>&1
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
|
||||
@@ -53,9 +53,9 @@ const methods = {
|
||||
}
|
||||
|
||||
const type = req.args?.type;
|
||||
let cmd = `generate ${type}`;
|
||||
let result = {};
|
||||
|
||||
let cmd = `generate ${type}`;
|
||||
if (type === 'age-x25519') {
|
||||
cmd = 'age keygen';
|
||||
} else if (type === 'age-mlkem768-x25519') {
|
||||
@@ -63,7 +63,8 @@ const methods = {
|
||||
} else if (type === 'age-convert') {
|
||||
cmd = 'age convert';
|
||||
}
|
||||
const fd = popen(`/usr/bin/mihomo ${cmd}` + (req.args?.params ? ' ' + shellQuote(req.args.params) : ''));
|
||||
|
||||
const fd = popen(`/usr/bin/mihomo ${cmd} ${shellQuote(req.args.params ?? '')}`);
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
||||
if (type === 'uuid')
|
||||
@@ -140,7 +141,7 @@ const methods = {
|
||||
infd.write(content);
|
||||
|
||||
infd.seek();
|
||||
const out = executeCommand(infd, '/usr/bin/mihomo age', req.args?.action, req.args?.key, '-', '-');
|
||||
const out = executeCommand(infd, '/usr/bin/mihomo age', req.args?.action, shellQuote(req.args?.key), '-', '-');
|
||||
infd.close();
|
||||
|
||||
return out.exitcode === 0 ? { result: out.stdout } : { result: false, error: out.stderr };
|
||||
@@ -162,7 +163,7 @@ const methods = {
|
||||
if ((!req.args?.output) || match(req.args?.output, /\.\.\//))
|
||||
return { result: false, error: 'illegal output' };
|
||||
|
||||
const out = executeCommand(null, '/usr/bin/mihomo age', req.args?.action, req.args?.key, req.args?.input, req.args?.output);
|
||||
const out = executeCommand(null, '/usr/bin/mihomo age', req.args?.action, shellQuote(req.args?.key), shellQuote(req.args?.input), shellQuote(req.args?.output));
|
||||
|
||||
return out.exitcode === 0 ? { result: true } : { result: false, error: out.stderr };
|
||||
}
|
||||
@@ -228,7 +229,7 @@ const methods = {
|
||||
if (!req.args?.url)
|
||||
return { httpcode: null, error: 'illegal url' };
|
||||
|
||||
const httpcode = trim(popen(`wget --spider -t1 -ST3 '${req.args?.url}' 2>&1 | awk '/^\\s*HTTP\\//{print $2}'`).read('all')) || '-1';
|
||||
const httpcode = trim(popen(`wget --spider -t1 -ST3 ${shellQuote(req.args?.url)} 2>&1 | awk '/^\\s*HTTP\\//{print $2}'`).read('all')) || '-1';
|
||||
|
||||
return { httpcode: httpcode };
|
||||
}
|
||||
@@ -239,8 +240,10 @@ const methods = {
|
||||
call: function(req) {
|
||||
if (req.args?.type == 'resources') {
|
||||
system(`sed -i "/${replace(EXE_DIR, "/", "\\/")}\\/update_resources.sh/d" /etc/crontabs/root`);
|
||||
if (req.args?.expr)
|
||||
if ((req.args?.expr && match(req.args?.expr, /^[0-9 ,\*\/\?-]+$/)))
|
||||
system(`echo -e "` + req.args?.expr + ` ${EXE_DIR}/update_resources.sh ALL" >> /etc/crontabs/root`);
|
||||
else
|
||||
return { result: false, error: 'illegal expr' };
|
||||
} else
|
||||
return { result: false, error: 'illegal type' };
|
||||
|
||||
@@ -396,7 +399,7 @@ const methods = {
|
||||
if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//))
|
||||
return { result: false, error: 'illegal filename' };
|
||||
|
||||
system(`rm -f ${HM_DIR}/${req.args?.type}/${req.args?.filename}`);
|
||||
system(`rm -f ${HM_DIR}/${req.args?.type}/${shellQuote(req.args?.filename)}`);
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function yqRead(flags, command, content) {
|
||||
};
|
||||
|
||||
export function yqReadFile(flags, command, filepath) {
|
||||
const out = executeCommand(null, 'yq', flags, shellQuote(command), filepath);
|
||||
const out = executeCommand(null, 'yq', flags, shellQuote(command), shellQuote(filepath));
|
||||
|
||||
return out.stdout;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.6.3
|
||||
PKG_RELEASE:=53
|
||||
PKG_RELEASE:=54
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -1518,11 +1518,6 @@ function gen_config(var)
|
||||
end
|
||||
end)
|
||||
|
||||
table.insert(rules, {
|
||||
outboundTag = "direct",
|
||||
ip = { "geoip:private" }
|
||||
})
|
||||
|
||||
if default_outboundTag then
|
||||
local rule = {
|
||||
ruleTag = "default",
|
||||
|
||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wechatpush
|
||||
PKG_VERSION:=3.6.12
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_MAINTAINER:=tty228 <tty228@yeah.net>
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -2293,8 +2293,8 @@ monitor_logins() {
|
||||
trap cleanup_child SIGINT SIGTERM
|
||||
|
||||
(
|
||||
# 监听系统日志,-f 表示跟随实时日志,-p 表示日志级别为 notice
|
||||
run_with_tag logread -f -p notice | while IFS= read -r line; do
|
||||
# 监听系统日志,-f 表示跟随实时日志
|
||||
run_with_tag logread -f | while IFS= read -r line; do
|
||||
[ -n "$web_logged" ] && {
|
||||
web_login_ip=$(echo "$line" | grep -i "accepted login" | awk '{print $NF}')
|
||||
[ -n "$web_login_ip" ] && process_login "$web_login_ip" $(echo "$line" | awk '{print $4}') web_login_counts
|
||||
@@ -2317,7 +2317,7 @@ monitor_logins() {
|
||||
# 如果未能提取到 IP,从日志标识符提取失败用户的 ID,并再次提取 IP
|
||||
if [ -z "$ssh_failed_ip" ]; then
|
||||
ssh_failed_num=$(echo "$line" | sed -n 's/.*authpriv\.warn dropbear\[\([0-9]\+\)\]: Login attempt for nonexistent user/\1/p')
|
||||
[ -n "$ssh_failed_num" ] && ssh_failed_ip=$(logread notice | grep "authpriv\.info dropbear\[${ssh_failed_num}\].*Child connection from" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//' | tail -n 1)
|
||||
[ -n "$ssh_failed_num" ] && ssh_failed_ip=$(logread | grep "authpriv\.info dropbear\[${ssh_failed_num}\].*Child connection from" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//' | tail -n 1)
|
||||
fi
|
||||
|
||||
# 如果成功提取到 IP 地址,调用 process_login 处理
|
||||
@@ -2578,13 +2578,13 @@ login_send() {
|
||||
# 登录方式
|
||||
if [[ "$log_type" == "web"* ]]; then
|
||||
# Web 登录、非法登录
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $13}' | tail -n 1)
|
||||
local login_mode=$(logread | grep -E ".* $login_time.*$login_ip.*" | awk '{print $13}' | tail -n 1)
|
||||
[ "$login_mode" = "/" ] && login_mode="$(translate "/ (Homepage login)")"
|
||||
elif [[ "$log_type" == "ssh_login"* ]]; then
|
||||
# SSH 登录
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $8}' | tail -n 1)
|
||||
local login_mode=$(logread | grep -E ".* $login_time.*$login_ip.*" | awk '{print $8}' | tail -n 1)
|
||||
else
|
||||
local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{for(i=8;i<NF;i++) if($i=="from") break; else printf $i " "}' | tail -n 1)
|
||||
local login_mode=$(logread | grep -E ".* $login_time.*$login_ip.*" | awk '{for(i=8;i<NF;i++) if($i=="from") break; else printf $i " "}' | tail -n 1)
|
||||
fi
|
||||
|
||||
if [ -z "$login_disturb" ] || [ "$login_disturb" -ne "1" ]; then
|
||||
|
||||
@@ -87,7 +87,7 @@ This will be compiled to standard CSS that works in all browsers.
|
||||
|
||||
The theme has two independent Tailwind CSS v4 entry points, both sourced from `.dev/src/media/`:
|
||||
|
||||
- **`main.css`** — the LuCI admin UI. It contains no rules of its own; it's a pure import manifest that pulls in (in order) `_tokens.css` (OKLCH theme tokens, mapped via `@theme inline`), `_base.css`, `_layout.css`, every file in `components/` (one partial per UI component — buttons, cards, modals, tables, etc.), `_utilities.css`, and `_patches.css`.
|
||||
- **`main.css`** — the LuCI admin UI. It contains no rules of its own; it's a pure import manifest that pulls in (in order) `_tokens.css` (OKLCH theme tokens, mapped via `@theme inline`), `_base.css`, `_elements.css`, `_layout.css`, every file in `components/` (one partial per UI component — buttons, cards, modals, tables, etc.), `_utilities.css`, and `_patches.css`.
|
||||
- **`login.css`** — the standalone login page (`sysauth.ut`). Self-contained: imports Tailwind and `_tokens.css` directly.
|
||||
|
||||
**Adding new styles:**
|
||||
@@ -97,6 +97,21 @@ The theme has two independent Tailwind CSS v4 entry points, both sourced from `.
|
||||
|
||||
All rules use `@apply` with Tailwind utilities and CSS Nesting — no raw CSS properties.
|
||||
|
||||
### Design Tokens
|
||||
|
||||
`src/media/_tokens.css` is **generated** — its header says "DO NOT EDIT". The source of truth is `.dev/tokens/`:
|
||||
|
||||
- **`defaults.js`** — the 10 editable input colors (`bg`, `surface`, `text`, `brand`, `on_brand`, `link`, `info`, `warning`, `success`, `danger`) for light and dark mode, as OKLCH strings.
|
||||
- **`spec.js`** — `DERIVATIONS` (how every other token — `text_muted`, `surface_sunken`, `hairline`, `brand_hover`, `brand_subtle`, `focus_ring`, `progress_start`/`progress_end`, `*_surface`, `scrim`, `mega_menu_bg`, …) is computed from the inputs via `mix`/`shade`/`set`/`alpha`/`const` operators, and `FIXED` (mode-specific literals such as shadows that bypass derivation).
|
||||
- **`engine.js`** — the OKLCH/OKLAB color math behind those operators, via [colorjs.io](https://colorjs.io/).
|
||||
- **`resolve.js`** — `resolveMode(mode)` walks `DERIVATIONS` and returns a flat `{token: oklchString}` map with no `color-mix()`/`var()` left in it.
|
||||
|
||||
**Changing a color:**
|
||||
|
||||
1. Edit `tokens/defaults.js` (base input colors) and/or `tokens/spec.js` (derivation rules, fixed literals).
|
||||
2. Run `pnpm gen:tokens` (also runs automatically as part of `pnpm build`) to rewrite `src/media/_tokens.css` — it emits `:root` (light) and `[data-darkmode="true"]` (dark) blocks plus the `@theme inline` mapping, in that order.
|
||||
3. Run `pnpm test` to check the color-math operators and derived-token invariants (`tests/engine.test.js`, `tests/resolve.test.js`, `tests/surfaces.test.js`) — e.g. hue families, lightness ordering between `bg`/`surface_sunken`/`surface`, and translucency of menu backgrounds.
|
||||
|
||||
### LuCI JavaScript API
|
||||
|
||||
For LuCI-specific JavaScript development, refer to the official API documentation:
|
||||
@@ -174,10 +189,11 @@ htdocs/luci-static/
|
||||
|
||||
**Build Process:**
|
||||
|
||||
1. Vite builds the CSS entry points (`src/media/main.css` and `src/media/login.css`)
|
||||
2. Custom PostCSS plugin removes `@layer` at-rules for OpenWrt compatibility
|
||||
3. Custom Vite plugin (`luci-js-compress`) minifies JS files via Terser
|
||||
4. Static assets copied from `.dev/public/aurora/`
|
||||
1. `pnpm gen:tokens` regenerates `src/media/_tokens.css` from `tokens/` (see [Design Tokens](#design-tokens))
|
||||
2. Vite builds the CSS entry points (`src/media/main.css` and `src/media/login.css`)
|
||||
3. Custom PostCSS plugin removes `@layer` at-rules for OpenWrt compatibility
|
||||
4. Custom Vite plugin (`luci-js-compress`) minifies JS files via Terser
|
||||
5. Static assets copied from `.dev/public/aurora/`
|
||||
|
||||
## Package Compilation
|
||||
|
||||
@@ -219,20 +235,33 @@ luci-theme-aurora/
|
||||
│ │ ├── fonts/ # Web fonts (Lato)
|
||||
│ │ └── images/ # Theme images + PWA icons
|
||||
│ ├── scripts/ # Build scripts
|
||||
│ │ └── clean.js # Build cleanup utility
|
||||
│ │ ├── clean.js # Build cleanup utility
|
||||
│ │ └── gen-tokens.js # Regenerates src/media/_tokens.css from tokens/
|
||||
│ ├── src/ # Source code
|
||||
│ │ ├── assets/icons/ # SVG icons
|
||||
│ │ ├── media/ # CSS source (Tailwind CSS v4)
|
||||
│ │ │ ├── main.css # Admin UI entry point (import manifest)
|
||||
│ │ │ ├── login.css # Login page entry point
|
||||
│ │ │ ├── _tokens.css # OKLCH theme tokens (@theme inline)
|
||||
│ │ │ ├── _base.css # Base element styles
|
||||
│ │ │ ├── _tokens.css # OKLCH theme tokens -- GENERATED, see tokens/
|
||||
│ │ │ ├── _base.css # Document foundation (html/body viewport bg)
|
||||
│ │ │ ├── _elements.css # Base element styles (headings, links, …)
|
||||
│ │ │ ├── _layout.css # Page layout/structure
|
||||
│ │ │ ├── _utilities.css # Custom utility classes
|
||||
│ │ │ ├── _patches.css # Third-party LuCI app/page overrides
|
||||
│ │ │ └── components/ # One partial per UI component
|
||||
│ │ └── resource/ # JavaScript resources
|
||||
│ │ └── menu-aurora.js # Menu logic
|
||||
│ ├── tokens/ # Design token source (-> src/media/_tokens.css)
|
||||
│ │ ├── defaults.js # 10 editable input colors (light/dark)
|
||||
│ │ ├── spec.js # Derivation rules (DERIVATIONS) + fixed literals
|
||||
│ │ ├── engine.js # OKLCH/OKLAB color math (mix/shade/set/alpha)
|
||||
│ │ └── resolve.js # Resolves spec into a flat token map
|
||||
│ ├── tests/ # All test suites (pnpm test)
|
||||
│ │ ├── engine.test.js # Color-math operators
|
||||
│ │ ├── resolve.test.js # Resolved token invariants
|
||||
│ │ ├── surfaces.test.js # Surface/hue layering invariants
|
||||
│ │ ├── overlay.test.js # Overlay/layout CSS assertions
|
||||
│ │ └── navigation-*.test.js # Navigation model/rendering/styles
|
||||
│ ├── .env.example # Environment variables template
|
||||
│ ├── .prettierrc # Prettier configuration
|
||||
│ ├── package.json # Node.js dependencies
|
||||
@@ -270,6 +299,7 @@ luci-theme-aurora/
|
||||
- **[Vite](https://vitejs.dev/)** - Build tool and development server
|
||||
- **[pnpm](https://pnpm.io/)** - Fast, disk space efficient package manager
|
||||
- **[lightningcss](https://lightningcss.dev/)** - CSS minifier
|
||||
- **[colorjs.io](https://colorjs.io/)** - OKLCH/OKLAB color math for design token generation (`.dev/tokens/`)
|
||||
- **[Terser](https://terser.org/)** - JavaScript minifier
|
||||
- **[Prettier](https://prettier.io/)** - Code formatter
|
||||
- **[prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)** - Tailwind class sorting
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run clean && vite build",
|
||||
"clean": "node scripts/clean.js"
|
||||
"build": "npm run clean && npm run gen:tokens && vite build",
|
||||
"clean": "node scripts/clean.js",
|
||||
"gen:tokens": "node scripts/gen-tokens.js",
|
||||
"test": "node --test tests/*.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "^24.0.0",
|
||||
"browserslist": "^4.28.2",
|
||||
"colorjs.io": "^0.6.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.7.0",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
|
||||
Generated
+198
-6
@@ -7,13 +7,23 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
colorjs.io:
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))
|
||||
version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0))
|
||||
'@types/node':
|
||||
specifier: ^24.0.0
|
||||
version: 24.10.9
|
||||
browserslist:
|
||||
specifier: ^4.28.2
|
||||
version: 4.28.2
|
||||
lightningcss:
|
||||
specifier: ^1.32.0
|
||||
version: 1.32.0
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.8.1
|
||||
@@ -34,7 +44,7 @@ importers:
|
||||
version: 1.4.0
|
||||
vite:
|
||||
specifier: ^7.1.1
|
||||
version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -459,13 +469,29 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
baseline-browser-mapping@2.10.37:
|
||||
resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
browserslist@4.28.2:
|
||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
caniuse-lite@1.0.30001799:
|
||||
resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
colorjs.io@0.6.1:
|
||||
resolution: {integrity: sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
@@ -473,6 +499,9 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
electron-to-chromium@1.5.372:
|
||||
resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==}
|
||||
|
||||
enhanced-resolve@5.18.4:
|
||||
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -482,6 +511,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -509,30 +542,60 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lightningcss-darwin-arm64@1.30.2:
|
||||
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.30.2:
|
||||
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.30.2:
|
||||
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -540,6 +603,13 @@ packages:
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -547,6 +617,13 @@ packages:
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -554,6 +631,13 @@ packages:
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -561,22 +645,45 @@ packages:
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.30.2:
|
||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lightningcss@1.32.0:
|
||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -585,6 +692,10 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
node-releases@2.0.47:
|
||||
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -709,6 +820,12 @@ packages:
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -989,12 +1106,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
|
||||
|
||||
'@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0))':
|
||||
'@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.18
|
||||
'@tailwindcss/oxide': 4.1.18
|
||||
tailwindcss: 4.1.18
|
||||
vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
@@ -1006,14 +1123,30 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
baseline-browser-mapping@2.10.37: {}
|
||||
|
||||
browserslist@4.28.2:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.37
|
||||
caniuse-lite: 1.0.30001799
|
||||
electron-to-chromium: 1.5.372
|
||||
node-releases: 2.0.47
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
caniuse-lite@1.0.30001799: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
colorjs.io@0.6.1: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
electron-to-chromium@1.5.372: {}
|
||||
|
||||
enhanced-resolve@5.18.4:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -1048,6 +1181,8 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -1062,36 +1197,69 @@ snapshots:
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-arm64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.30.2:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
@@ -1108,12 +1276,30 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.30.2
|
||||
lightningcss-win32-x64-msvc: 1.30.2
|
||||
|
||||
lightningcss@1.32.0:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64: 1.32.0
|
||||
lightningcss-darwin-arm64: 1.32.0
|
||||
lightningcss-darwin-x64: 1.32.0
|
||||
lightningcss-freebsd-x64: 1.32.0
|
||||
lightningcss-linux-arm-gnueabihf: 1.32.0
|
||||
lightningcss-linux-arm64-gnu: 1.32.0
|
||||
lightningcss-linux-arm64-musl: 1.32.0
|
||||
lightningcss-linux-x64-gnu: 1.32.0
|
||||
lightningcss-linux-x64-musl: 1.32.0
|
||||
lightningcss-win32-arm64-msvc: 1.32.0
|
||||
lightningcss-win32-x64-msvc: 1.32.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
node-releases@2.0.47: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
@@ -1205,7 +1391,13 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0):
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
browserslist: 4.28.2
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0):
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -1217,5 +1409,5 @@ snapshots:
|
||||
'@types/node': 24.10.9
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
lightningcss: 1.32.0
|
||||
terser: 5.46.0
|
||||
@@ -0,0 +1,80 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { resolveMode } from "../tokens/resolve.js";
|
||||
import { FIXED } from "../tokens/spec.js";
|
||||
|
||||
const kebab = (s) => s.replace(/_/g, "-");
|
||||
|
||||
function block(selector, colors, fixed) {
|
||||
const lines = [];
|
||||
for (const [k, v] of Object.entries(colors)) {
|
||||
lines.push(` --${kebab(k)}: ${v};`);
|
||||
}
|
||||
for (const [k, v] of Object.entries(fixed)) {
|
||||
lines.push(` --${kebab(k)}: ${v};`);
|
||||
}
|
||||
return `${selector} {\n${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
const light = resolveMode("light");
|
||||
const dark = resolveMode("dark");
|
||||
|
||||
const STRUCTURE = `
|
||||
--font-sans: "Lato", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
--spacing: 0.25rem;
|
||||
--container-max-width: 80rem;
|
||||
--radius-base: 0.5rem;
|
||||
`;
|
||||
|
||||
const THEME = `@theme inline {
|
||||
${Object.keys(light)
|
||||
.map((k) => ` --color-${kebab(k)}: var(--${kebab(k)});`)
|
||||
.join("\n")}
|
||||
|
||||
--shadow-sm: var(--app-shadow-sm);
|
||||
--shadow-md: var(--app-shadow-md);
|
||||
--shadow-lg: var(--app-shadow-md);
|
||||
--shadow-xl: var(--app-shadow-lg);
|
||||
--shadow-2xl: var(--app-shadow-lg);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--spacing: var(--spacing);
|
||||
--container-max-width: var(--container-max-width);
|
||||
|
||||
--radius-sm: calc(var(--radius-base) * 0.25);
|
||||
--radius: calc(var(--radius-base) * 0.5);
|
||||
--radius-md: calc(var(--radius-base) * 0.75);
|
||||
--radius-lg: var(--radius-base);
|
||||
--radius-xl: calc(var(--radius-base) * 1.5);
|
||||
--radius-2xl: calc(var(--radius-base) * 2);
|
||||
--radius-3xl: calc(var(--radius-base) * 3);
|
||||
--radius-4xl: calc(var(--radius-base) * 4);
|
||||
}
|
||||
`;
|
||||
|
||||
const HEADER = `/**
|
||||
* luci-theme-aurora: design tokens -- GENERATED, DO NOT EDIT.
|
||||
* Run \`pnpm gen:tokens\`. Source: tokens/spec.js + tokens/defaults.js
|
||||
* All color values are flat; lightningcss adds legacy fallbacks.
|
||||
* ORDER MATTERS: [data-darkmode="true"] must stay after :root.
|
||||
*/
|
||||
`;
|
||||
|
||||
const css =
|
||||
HEADER +
|
||||
"\n" +
|
||||
block(":root", light, FIXED.light) +
|
||||
STRUCTURE +
|
||||
"}\n\n" +
|
||||
block('[data-darkmode="true"]', dark, FIXED.dark) +
|
||||
"}\n\n" +
|
||||
THEME;
|
||||
|
||||
await writeFile(
|
||||
resolve(import.meta.dirname, "../src/media/_tokens.css"),
|
||||
css,
|
||||
"utf-8",
|
||||
);
|
||||
console.log("gen-tokens: wrote src/media/_tokens.css");
|
||||
@@ -1,88 +1,12 @@
|
||||
*:not(html) {
|
||||
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-transparent scrollbar-thumb-content-muted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document foundation — shared by main.css and login.css.
|
||||
* Solid bg on html/body — one paint path with header (no body::before;
|
||||
* fixed pseudo made the same var(--bg) look mismatched).
|
||||
*/
|
||||
html {
|
||||
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-surface scrollbar-thumb-content-muted;
|
||||
@apply bg-canvas relative h-full font-sans;
|
||||
@apply bg-bg min-h-dvh font-sans;
|
||||
|
||||
body {
|
||||
@apply text-content relative flex flex-col bg-transparent text-sm leading-relaxed font-normal;
|
||||
&.modal-overlay-active {
|
||||
@apply h-screen overflow-hidden;
|
||||
}
|
||||
@apply bg-bg min-h-dvh;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-content font-sans leading-tight font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply mb-4 pl-6 text-3xl max-md:pl-4 max-md:text-2xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply mb-4 pl-5 text-2xl max-md:mb-2 max-md:pl-4 max-md:text-xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply mb-4 text-xl font-semibold tracking-tight max-md:mb-2 max-md:text-lg;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply mb-2 text-lg max-md:text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply mb-2 text-base max-md:text-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply mb-2 text-sm max-md:text-xs;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
abbr {
|
||||
@apply cursor-help;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-content text-sm leading-relaxed;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply text-content-muted border-border-subtle bg-surface-muted block rounded-xl border px-3 py-2 font-mono text-sm leading-relaxed wrap-break-word whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply text-content-muted bg-surface-muted rounded px-2 py-1 font-mono text-sm;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-link no-underline hover:underline;
|
||||
}
|
||||
|
||||
var {
|
||||
@apply text-link font-mono;
|
||||
}
|
||||
|
||||
small {
|
||||
@apply text-content text-xs font-normal;
|
||||
}
|
||||
|
||||
em {
|
||||
@apply text-content-muted font-medium italic;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
*:not(html) {
|
||||
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-transparent scrollbar-thumb-text-muted;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply scrollbar-thumb-rounded-full scrollbar-thin scrollbar-track-surface scrollbar-thumb-text-muted;
|
||||
|
||||
body {
|
||||
@apply text-text relative flex min-h-dvh flex-col text-sm leading-relaxed font-normal;
|
||||
|
||||
&.modal-overlay-active {
|
||||
@apply h-screen overflow-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-text font-sans leading-tight font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply mb-4 pl-6 text-3xl max-md:pl-4 max-md:text-2xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply mb-4 pl-5 text-2xl max-md:mb-2 max-md:pl-4 max-md:text-xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply mb-4 text-xl font-semibold tracking-tight max-md:mb-2 max-md:text-lg;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply mb-2 text-lg max-md:text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply mb-2 text-base max-md:text-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply mb-2 text-sm max-md:text-xs;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
abbr {
|
||||
@apply cursor-help;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-text text-sm leading-relaxed;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply text-text-muted border-hairline bg-surface-sunken block rounded-xl border px-3 py-2 font-mono text-sm leading-relaxed wrap-break-word whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply text-text-muted bg-surface-sunken rounded px-2 py-1 font-mono text-sm;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-link no-underline hover:underline;
|
||||
}
|
||||
|
||||
var {
|
||||
@apply text-link font-mono;
|
||||
}
|
||||
|
||||
small {
|
||||
@apply text-text text-xs font-normal;
|
||||
}
|
||||
|
||||
em {
|
||||
@apply text-text-muted font-medium italic;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
header {
|
||||
@apply bg-header-bg sticky top-0 z-60 mb-2;
|
||||
@apply bg-bg sticky top-0 z-60 mb-2;
|
||||
|
||||
& .header-content {
|
||||
@apply relative flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2;
|
||||
@@ -7,7 +7,7 @@ header {
|
||||
|
||||
[data-nav-type="mega-menu"] & {
|
||||
& .desktop-menu-container {
|
||||
@apply bg-header-glass-bg pointer-events-none absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden opacity-0 backdrop-blur-xl transition-[clip-path,opacity] duration-200 ease-out [will-change:clip-path,opacity] [clip-path:inset(0_0_100%_0)] max-md:hidden;
|
||||
@apply border-hairline bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 z-30 h-(--mega-menu-height,0) w-full overflow-hidden border-b opacity-0 shadow-xl backdrop-blur-lg backdrop-saturate-150 transition-[clip-path,opacity] duration-[250ms] ease-out [will-change:clip-path,opacity] [clip-path:inset(0_0_100%_0)] max-md:hidden;
|
||||
|
||||
&.active {
|
||||
@apply pointer-events-auto opacity-100 [clip-path:inset(0)];
|
||||
@@ -22,7 +22,7 @@ header {
|
||||
fixed slot left of them. The entering panel floats up 6px —
|
||||
transform + opacity only. */
|
||||
& .desktop-nav {
|
||||
@apply pointer-events-none absolute inset-x-0 top-14 min-h-60 translate-y-1.5 pt-7 pb-8 pl-[calc(50%-13rem)] opacity-0 transition-[opacity,transform] duration-200 ease-out;
|
||||
@apply pointer-events-none absolute inset-x-0 top-14 min-h-60 translate-y-1.5 pt-7 pb-8 pl-[calc(50%-13rem)] opacity-0 transition-[opacity,transform] duration-[250ms] ease-out;
|
||||
|
||||
&.active {
|
||||
@apply pointer-events-auto translate-y-0 opacity-100;
|
||||
@@ -32,7 +32,7 @@ header {
|
||||
@apply absolute top-7 left-[calc(50%-30rem)] w-56 max-lg:hidden;
|
||||
|
||||
& .desktop-nav-title {
|
||||
@apply text-content text-2xl font-semibold tracking-tight;
|
||||
@apply text-text text-2xl font-semibold tracking-tight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ header {
|
||||
@apply m-0 list-none;
|
||||
|
||||
& > a {
|
||||
@apply text-content block w-fit py-2.5 text-sm whitespace-nowrap no-underline hover:underline hover:decoration-2 hover:underline-offset-4;
|
||||
@apply text-text block w-fit py-2.5 text-sm whitespace-nowrap no-underline hover:underline hover:decoration-2 hover:underline-offset-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ header {
|
||||
@apply absolute top-34 left-[calc(50%-30rem)] flex w-56 flex-col gap-2 max-lg:hidden;
|
||||
|
||||
& .board-line {
|
||||
@apply text-content-muted flex max-w-full items-center gap-2.5 text-xs;
|
||||
@apply text-text-muted flex max-w-full items-center gap-2.5 text-xs;
|
||||
|
||||
&::before {
|
||||
@apply size-4 shrink-0 bg-current content-[''];
|
||||
@@ -95,7 +95,7 @@ header {
|
||||
}
|
||||
|
||||
.brand {
|
||||
@apply hover:text-brand text-content inline-block shrink-0 text-xl font-semibold tracking-tight no-underline transition-[color,transform] duration-200 hover:-translate-y-0.5 max-md:flex-1 max-md:text-lg;
|
||||
@apply hover:text-brand text-text inline-block shrink-0 text-xl font-semibold tracking-tight no-underline transition-[color,transform] duration-150 hover:-translate-y-0.5 max-md:flex-1 max-md:text-lg;
|
||||
}
|
||||
|
||||
.nav {
|
||||
@@ -105,15 +105,15 @@ header {
|
||||
@apply relative;
|
||||
|
||||
.menu {
|
||||
@apply text-content block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-colors duration-150;
|
||||
@apply text-text block rounded-xl px-3.5 py-1.5 font-medium no-underline transition-colors duration-150;
|
||||
|
||||
&.menu-active {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
[data-nav-type="mega-menu"] & {
|
||||
@apply underline decoration-2 underline-offset-4;
|
||||
}
|
||||
[data-nav-type="boxed-dropdown"] & {
|
||||
@apply bg-header-interactive-bg;
|
||||
@apply bg-brand-subtle text-brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,16 +131,16 @@ header {
|
||||
|
||||
& .desktop-nav-list {
|
||||
[data-nav-type="boxed-dropdown"] & {
|
||||
@apply border-border-subtle bg-header-glass-bg flex flex-col gap-y-1 rounded-3xl border py-2 shadow-lg backdrop-blur-md backdrop-saturate-150;
|
||||
@apply border-hairline bg-surface-overlay flex flex-col gap-y-1 rounded-3xl border py-2 shadow-lg backdrop-blur-md backdrop-saturate-150;
|
||||
}
|
||||
& > li {
|
||||
@apply m-0 list-none;
|
||||
|
||||
& > a {
|
||||
@apply text-content block whitespace-nowrap no-underline;
|
||||
@apply text-text block whitespace-nowrap no-underline;
|
||||
|
||||
[data-nav-type="boxed-dropdown"] & {
|
||||
@apply hover:bg-header-interactive-bg mx-2 rounded-xl px-4 py-2;
|
||||
@apply hover:bg-hover-faint mx-2 rounded-xl px-4 py-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,15 +157,15 @@ header {
|
||||
}
|
||||
|
||||
& .navigation-toggle {
|
||||
@apply cursor-pointer rounded-none border-0 bg-transparent p-0 shadow-none transition-transform duration-200 hover:scale-105 active:scale-95;
|
||||
@apply cursor-pointer rounded-none border-0 bg-transparent p-0 shadow-none transition-transform duration-150 hover:scale-105 active:scale-95;
|
||||
|
||||
& svg {
|
||||
@apply text-content size-5;
|
||||
@apply text-text size-5;
|
||||
}
|
||||
|
||||
& .navigation-toggle-line {
|
||||
transform-box: fill-box;
|
||||
@apply origin-center transition-[transform,opacity] duration-300 ease-in-out;
|
||||
@apply origin-center transition-[transform,opacity] duration-150 ease-in-out;
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
@@ -189,7 +189,7 @@ header {
|
||||
@apply flex shrink-0 flex-row-reverse gap-5 md:gap-8;
|
||||
|
||||
& span[data-indicator] {
|
||||
@apply before:text-content size-5 cursor-pointer text-[0px] before:absolute before:size-5 before:bg-current;
|
||||
@apply before:text-text size-5 cursor-pointer text-[0px] before:absolute before:size-5 before:bg-current;
|
||||
|
||||
&[data-indicator="media_error"] {
|
||||
@apply before:[mask:url('@assets/icons/error.svg')_center/cover_no-repeat];
|
||||
@@ -223,14 +223,14 @@ body[data-nav-type="sidebar"] {
|
||||
so collapsing the sidebar never stretches it — lets the sidebar column
|
||||
animate to zero, and lets #maincontent fluidly reclaim the freed space.
|
||||
overflow-x-clip keeps the sliding sidebar out of horizontal overflow. */
|
||||
@apply md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip md:transition-[grid-template-columns] md:duration-300 md:ease-out;
|
||||
@apply md:grid md:grid-cols-[17rem_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip md:transition-[grid-template-columns] md:duration-[250ms] md:ease-out;
|
||||
|
||||
&.sidebar-collapsed {
|
||||
@apply md:grid-cols-[0_minmax(0,1fr)];
|
||||
}
|
||||
|
||||
& > header {
|
||||
@apply md:border-border-subtle md:col-span-full md:row-start-1 md:mb-0 md:border-b;
|
||||
@apply md:border-hairline md:col-span-full md:row-start-1 md:mb-0 md:border-b;
|
||||
}
|
||||
|
||||
& > .sidebar-panel {
|
||||
@@ -242,7 +242,7 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
& .header-crumb {
|
||||
@apply text-content-muted my-0 mr-auto ml-6 hidden min-w-0 list-none items-center gap-2 overflow-hidden p-0 text-sm md:flex;
|
||||
@apply text-text-muted my-0 mr-auto ml-6 hidden min-w-0 list-none items-center gap-2 overflow-hidden p-0 text-sm md:flex;
|
||||
|
||||
& li {
|
||||
@apply m-0 list-none whitespace-nowrap;
|
||||
@@ -253,12 +253,12 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
& .current {
|
||||
@apply text-content min-w-0 truncate font-medium;
|
||||
@apply text-text min-w-0 truncate font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-panel {
|
||||
@apply border-border-subtle sticky top-14 hidden h-[calc(100vh-3.5rem)] w-full overflow-hidden rounded-none border-r transition-[visibility] duration-300 md:block;
|
||||
@apply border-hairline sticky top-14 hidden h-[calc(100vh-3.5rem)] w-full overflow-hidden rounded-none border-r transition-[visibility] duration-[250ms] md:block;
|
||||
}
|
||||
|
||||
/* Visibility flips at the end of the slide so collapsed controls leave the
|
||||
@@ -268,7 +268,7 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
& .sidebar-panel-inner {
|
||||
@apply bg-header-bg flex h-full w-68 flex-col overflow-hidden transition-[translate,opacity] duration-300;
|
||||
@apply bg-bg flex h-full w-68 flex-col overflow-hidden transition-[translate,opacity] duration-[250ms];
|
||||
}
|
||||
|
||||
&.sidebar-collapsed .sidebar-panel-inner {
|
||||
@@ -283,46 +283,25 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-list .nav-link {
|
||||
& .sidebar-list .navigation-direct {
|
||||
@apply truncate text-lg;
|
||||
}
|
||||
|
||||
/* Selected-pill background for top-level leaf links. The shared
|
||||
.nav-link-active carries only brand text, so the sidebar owns its pill
|
||||
here (submenu links get theirs below). */
|
||||
& .sidebar-list > li > .nav-link-active {
|
||||
@apply bg-brand-faint hover:bg-brand-faint;
|
||||
}
|
||||
|
||||
& .sidebar-section {
|
||||
@apply grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-200 ease-out;
|
||||
}
|
||||
|
||||
& .sidebar-group-open > .sidebar-section {
|
||||
@apply grid-rows-[1fr] opacity-100;
|
||||
}
|
||||
|
||||
/* Child links sit indented under their category — hierarchy comes from
|
||||
spacing alone, no guide line, for a calmer look. */
|
||||
& .sidebar-submenu {
|
||||
@apply m-0 min-h-0 list-none space-y-0.5 overflow-hidden p-0 pl-3;
|
||||
@apply m-0 min-h-0 list-none space-y-0.5 overflow-hidden p-0 pl-4;
|
||||
}
|
||||
|
||||
& .sidebar-submenu .nav-link {
|
||||
@apply text-content-muted hover:text-content rounded-lg px-3 py-1.5 text-sm;
|
||||
}
|
||||
|
||||
& .sidebar-submenu .nav-link-active {
|
||||
@apply bg-brand-faint text-brand hover:text-brand;
|
||||
& .sidebar-submenu .navigation-sublink {
|
||||
@apply px-3 py-1.5 text-sm;
|
||||
}
|
||||
|
||||
& .sidebar-footer {
|
||||
@apply border-border-subtle shrink-0 border-t p-3;
|
||||
@apply border-hairline shrink-0 border-t p-3;
|
||||
|
||||
/* Logout: muted at rest; on hover the base .nav-link pill background
|
||||
plus error-colored text is the intended compound state (matches v1). */
|
||||
& .nav-link {
|
||||
@apply text-content-muted hover:text-danger-content;
|
||||
@apply text-text-muted hover:text-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +310,7 @@ body[data-nav-type="sidebar"] {
|
||||
@apply max-w-max-width mx-auto min-h-[calc(100vh-4rem)] w-23/24 px-4 max-md:w-full max-md:px-3;
|
||||
|
||||
#view {
|
||||
@apply animate-in fade-in-0 slide-in-from-top-2 fill-mode-both md:border-border-faint md:bg-surface mx-0 w-full p-0 shadow-none empty:hidden md:rounded-4xl md:border md:p-6 md:shadow-xl;
|
||||
@apply animate-in fade-in-0 slide-in-from-top-2 fill-mode-both mx-0 w-full bg-transparent p-0 shadow-none empty:hidden md:p-0;
|
||||
|
||||
.cbi-title-section {
|
||||
@apply mb-6 leading-relaxed max-md:mx-2 max-md:mb-3;
|
||||
@@ -365,7 +344,7 @@ body[data-nav-type="sidebar"] {
|
||||
@apply dark:!bg-surface;
|
||||
|
||||
& line[style] {
|
||||
@apply dark:!stroke-content;
|
||||
@apply dark:!stroke-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,18 +352,18 @@ body[data-nav-type="sidebar"] {
|
||||
|
||||
.cbi-map-descr,
|
||||
.cbi-section-descr {
|
||||
@apply text-content-muted mb-6 text-sm leading-relaxed max-md:mx-2 max-md:mb-3 max-md:text-sm;
|
||||
@apply text-text-muted mb-6 text-sm leading-relaxed max-md:mx-2 max-md:mb-3 max-md:text-sm;
|
||||
}
|
||||
|
||||
.cbi-page-actions {
|
||||
@apply border-border-subtle mt-6 flex flex-wrap items-center justify-end gap-3 border-t pt-4 max-md:mx-2 max-md:mt-4 max-md:gap-2 max-md:pt-3;
|
||||
@apply border-hairline mt-6 flex flex-wrap items-center justify-end gap-3 border-t pt-4 max-md:mx-2 max-md:mt-4 max-md:gap-2 max-md:pt-3;
|
||||
}
|
||||
|
||||
.zone-forwards {
|
||||
@apply flex items-start gap-3 leading-relaxed;
|
||||
|
||||
& > span {
|
||||
@apply text-content-muted mt-1.5;
|
||||
@apply text-text-muted mt-1.5;
|
||||
}
|
||||
|
||||
.zone-dest {
|
||||
@@ -398,12 +377,12 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
#syslog {
|
||||
@apply bg-terminal-bg text-terminal-content rounded-3xl border p-6 shadow-lg max-md:p-3;
|
||||
@apply bg-surface-sunken text-text rounded-3xl border p-6 shadow-lg max-md:p-3;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply text-content-muted mt-2 flex min-h-16 flex-wrap items-center justify-between text-xs;
|
||||
@apply text-text-muted mt-2 flex min-h-16 flex-wrap items-center justify-between text-xs;
|
||||
|
||||
& a {
|
||||
@apply text-brand;
|
||||
@@ -414,13 +393,13 @@ footer {
|
||||
}
|
||||
|
||||
& .breadcrumb {
|
||||
@apply bg-glass-surface hover:bg-surface flex items-center gap-1 rounded-full px-3 py-1.5 text-xs shadow-sm backdrop-blur-sm transition-colors duration-200;
|
||||
@apply bg-surface-overlay hover:bg-surface flex items-center gap-1 rounded-full px-3 py-1.5 text-xs shadow-sm backdrop-blur-md backdrop-saturate-150 transition-colors duration-150;
|
||||
|
||||
& li {
|
||||
@apply list-none;
|
||||
|
||||
&.active a {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,17 @@
|
||||
&.tr {
|
||||
@apply rounded-none;
|
||||
}
|
||||
@apply bg-glass-raised not-[.tr]:border-border-faint not-[.tr]:hover:border-border-subtle max-md:bg-glass-raised not-[.tr]:max-md:border-border-faint not-[.tr]:border not-[.tr]:shadow-lg not-[.tr]:hover:shadow-xl not-[.tr]:max-md:border;
|
||||
@apply bg-surface-overlay not-[.tr]:border-hairline not-[.tr]:hover:border-hairline max-md:bg-surface-overlay not-[.tr]:max-md:border-hairline not-[.tr]:border not-[.tr]:shadow-lg not-[.tr]:hover:shadow-xl not-[.tr]:max-md:border;
|
||||
|
||||
/* Dark mode: override custom.css hardcoded light-mode colors on all text nodes */
|
||||
& span {
|
||||
@apply dark:text-content!;
|
||||
@apply dark:text-text!;
|
||||
}
|
||||
& td {
|
||||
@apply dark:text-content!;
|
||||
@apply dark:text-text!;
|
||||
}
|
||||
& th {
|
||||
@apply dark:text-content!;
|
||||
@apply dark:text-text!;
|
||||
}
|
||||
|
||||
img[src*=".svg"] {
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
.title {
|
||||
h3 {
|
||||
@apply text-content border-0 pb-4 max-md:mx-0 max-md:pb-2;
|
||||
@apply text-text border-0 pb-4 max-md:mx-0 max-md:pb-2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,122 +1,44 @@
|
||||
/**
|
||||
* luci-theme-aurora: design tokens
|
||||
* Copyright 2025 eamonxg <eamonxiong@gmail.com>
|
||||
* Licensed to the public under the Apache License 2.0.
|
||||
*
|
||||
* Shared between main.css and login.css.
|
||||
*
|
||||
* Layer 1 contains broad theme inputs. Layer 2 names semantic and component
|
||||
* responsibilities. Components consume named roles and never apply numeric
|
||||
* color opacity modifiers.
|
||||
*
|
||||
* ORDER MATTERS: [data-darkmode="true"] ties with :root on specificity, so
|
||||
* the dark block must remain after :root.
|
||||
* luci-theme-aurora: design tokens -- GENERATED, DO NOT EDIT.
|
||||
* Run `pnpm gen:tokens`. Source: tokens/spec.js + tokens/defaults.js
|
||||
* All color values are flat; lightningcss adds legacy fallbacks.
|
||||
* ORDER MATTERS: [data-darkmode="true"] must stay after :root.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Layer 1: theme inputs */
|
||||
--canvas: oklch(0.968 0.007 247.896);
|
||||
--bg: oklch(0.967 0.003 264);
|
||||
--surface: oklch(1 0 0);
|
||||
--surface-raised: oklch(1 0 0);
|
||||
--content: oklch(0.208 0.042 265.755);
|
||||
--text: oklch(0.21 0.02 264);
|
||||
--brand: oklch(0.68 0.11 233);
|
||||
--on-brand: oklch(1 0 0);
|
||||
--link: oklch(0.74 0.238 322.16);
|
||||
--info: oklch(0.35 0.08 240);
|
||||
--info: oklch(0.43 0.2 255);
|
||||
--warning: oklch(0.35 0.08 60);
|
||||
--success: oklch(0.32 0.09 165);
|
||||
--danger: oklch(0.35 0.12 25);
|
||||
--text-muted: oklch(49.77% 0.0135 264);
|
||||
--text-subtle: oklch(60.36% 0.0112 264);
|
||||
--surface-sunken: oklch(95.7% 0.003 264);
|
||||
--surface-overlay: oklch(98.3% 0.003 264);
|
||||
--hairline: oklch(21% 0.02 264 / 0.08);
|
||||
--hover-faint: oklch(92.7% 0.003 264);
|
||||
--brand-hover: oklch(62% 0.11 233);
|
||||
--brand-subtle: oklch(93.26% 0.0155 238);
|
||||
--brand-subtle-hover: oklch(89.26% 0.0155 238);
|
||||
--focus-ring: oklch(68% 0.11 233 / 0.6);
|
||||
--progress-start: oklch(77.7% 0.0724 233.4);
|
||||
--progress-end: oklch(0.68 0.11 233);
|
||||
--info-surface: oklch(94% 0.05 255);
|
||||
--warning-surface: oklch(95% 0.05 60);
|
||||
--success-surface: oklch(94% 0.05 165);
|
||||
--danger-surface: oklch(94% 0.05 25);
|
||||
--danger-surface-hover: oklch(90% 0.05 25);
|
||||
--scrim: oklch(0% 0 0 / 0.6);
|
||||
--mega-menu-bg: oklch(98.3% 0.003 264 / 0.66);
|
||||
--app-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.04);
|
||||
--app-shadow-md: 0 4px 16px oklch(0 0 0 / 0.08), 0 1px 3px oklch(0 0 0 / 0.04);
|
||||
--app-shadow-lg: 0 12px 32px oklch(0 0 0 / 0.12);
|
||||
|
||||
/* Layer 2: semantic surface and content roles */
|
||||
--surface-subtle: oklch(from var(--surface-raised) calc(l - 0.02) c h);
|
||||
--surface-muted: oklch(from var(--surface-raised) calc(l - 0.05) c h);
|
||||
--content-muted: oklch(0.446 0.043 257.281);
|
||||
--content-subtle: color-mix(in oklab, var(--content) 52%, var(--surface-raised));
|
||||
|
||||
/* Layer 2: semantic brand roles */
|
||||
--brand-faint: color-mix(in oklab, var(--brand) 8%, var(--surface-raised));
|
||||
--brand-soft: color-mix(in oklab, var(--brand) 15%, var(--surface-raised));
|
||||
--brand-emphasis: color-mix(in oklab, var(--brand) 25%, var(--surface-raised));
|
||||
--brand-emphasis-hover: color-mix(
|
||||
in oklab,
|
||||
var(--brand) 30%,
|
||||
var(--surface-raised)
|
||||
);
|
||||
--brand-hover: color-mix(in oklab, var(--brand) 90%, var(--surface-raised));
|
||||
--focus-ring: var(--brand-emphasis);
|
||||
|
||||
/* Layer 2: semantic border and material roles */
|
||||
--border-faint: color-mix(in oklab, var(--content) 8%, transparent);
|
||||
--border-subtle: color-mix(in oklab, var(--content) 12%, transparent);
|
||||
--border-strong: color-mix(in oklab, var(--content) 28%, transparent);
|
||||
--glass-surface: color-mix(in oklab, var(--surface) 92%, transparent);
|
||||
--glass-raised: color-mix(in oklab, var(--surface-raised) 96%, transparent);
|
||||
--scrim: oklch(0 0 0 / 0.5);
|
||||
|
||||
/* Layer 2: semantic status families */
|
||||
--info-surface: oklch(from var(--info) 0.94 0.05 h);
|
||||
--info-content: var(--info);
|
||||
--info-border: color-mix(in oklab, var(--info) 28%, var(--surface-raised));
|
||||
--warning-surface: oklch(from var(--warning) 0.95 0.05 h);
|
||||
--warning-content: var(--warning);
|
||||
--warning-border: color-mix(in oklab, var(--warning) 28%, var(--surface-raised));
|
||||
--success-surface: oklch(from var(--success) 0.94 0.05 h);
|
||||
--success-content: var(--success);
|
||||
--success-border: color-mix(in oklab, var(--success) 28%, var(--surface-raised));
|
||||
--danger-surface: oklch(from var(--danger) 0.94 0.05 h);
|
||||
--danger-content: var(--danger);
|
||||
--danger-border: color-mix(in oklab, var(--danger) 28%, var(--surface-raised));
|
||||
|
||||
/* Layer 2: shell and navigation */
|
||||
--header-bg: var(--canvas);
|
||||
--header-interactive-bg: oklch(from var(--header-bg) calc(l - 0.12) c h);
|
||||
--header-glass-bg: color-mix(in oklab, var(--header-bg) 40%, transparent);
|
||||
|
||||
/* Layer 2: focused component responsibilities */
|
||||
--tooltip-bg: oklch(from var(--surface) calc(l - 0.015) c h);
|
||||
--terminal-bg: oklch(0.24 0.02 260);
|
||||
--terminal-content: oklch(1 0 0);
|
||||
--progress-track-bg: oklch(0.929 0.013 255.508);
|
||||
--progress-start: var(--brand);
|
||||
--progress-end: oklch(from var(--brand) calc(l + 0.07) c calc(h - 35));
|
||||
--input-bg: var(--surface);
|
||||
--input-checked-content: oklch(1 0 0);
|
||||
|
||||
/* Layer 2: buttons and neutral status */
|
||||
--button-secondary-bg: oklch(0.929 0.013 255.508);
|
||||
--button-secondary-hover-bg: color-mix(
|
||||
in oklab,
|
||||
var(--button-secondary-bg) 90%,
|
||||
var(--surface-raised)
|
||||
);
|
||||
--button-secondary-content: oklch(0.372 0.044 257.287);
|
||||
--button-secondary-border: var(--button-secondary-bg);
|
||||
--button-muted-bg: var(--surface-muted);
|
||||
--button-muted-hover-bg: color-mix(
|
||||
in oklab,
|
||||
var(--button-muted-bg) 90%,
|
||||
var(--surface-raised)
|
||||
);
|
||||
--button-muted-content: var(--content-muted);
|
||||
--button-muted-border: var(--button-muted-bg);
|
||||
--neutral-status-surface: oklch(0.97 0 0);
|
||||
--neutral-status-content: oklch(0.205 0 0);
|
||||
--neutral-status-border: var(--border-faint);
|
||||
|
||||
/* Layer 2: responsibilities formerly shared by label-surface */
|
||||
--card-action-bg: var(--surface-subtle);
|
||||
--interface-badge-bg: var(--surface-subtle);
|
||||
--segmented-control-bg: var(--surface-subtle);
|
||||
--ifacebox-header-bg: var(--surface-subtle);
|
||||
--table-header-bg: var(--surface-subtle);
|
||||
--table-row-hover-bg: var(--surface-subtle);
|
||||
--table-row-alternate-bg: color-mix(
|
||||
in oklab,
|
||||
var(--table-row-hover-bg) 50%,
|
||||
var(--surface-raised)
|
||||
);
|
||||
|
||||
/* Structure */
|
||||
--font-sans: "Lato", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
--spacing: 0.25rem;
|
||||
@@ -125,52 +47,44 @@
|
||||
}
|
||||
|
||||
[data-darkmode="true"] {
|
||||
/* Layer 1: theme inputs */
|
||||
--canvas: oklch(0.32 0.059 188.42);
|
||||
--surface: oklch(0.21 0.034 264.665);
|
||||
--surface-raised: oklch(0.279 0.041 260.031);
|
||||
--content: oklch(0.968 0.007 247.896);
|
||||
--bg: oklch(0.13 0.018 264);
|
||||
--surface: oklch(0.21 0.02 264);
|
||||
--text: oklch(0.985 0.002 264);
|
||||
--brand: oklch(0.6 0.13 188.745);
|
||||
--on-brand: oklch(1 0 0);
|
||||
--link: oklch(0.702 0.183 293.541);
|
||||
--info: oklch(0.88 0.06 230);
|
||||
--warning: oklch(0.924 0.12 95.746);
|
||||
--success: oklch(0.92 0.09 160);
|
||||
--danger: oklch(0.88 0.14 25);
|
||||
|
||||
/* Mode-sensitive semantic roles */
|
||||
--surface-subtle: oklch(from var(--surface-raised) calc(l + 0.04) c h);
|
||||
--surface-muted: oklch(from var(--surface-raised) calc(l + 0.09) c h);
|
||||
--content-muted: oklch(0.704 0.04 256.788);
|
||||
--info-surface: oklch(from var(--info) 0.34 0.07 h);
|
||||
--warning-surface: oklch(from var(--warning) 0.35 0.08 h);
|
||||
--success-surface: oklch(from var(--success) 0.33 0.06 h);
|
||||
--danger-surface: oklch(from var(--danger) 0.34 0.09 h);
|
||||
|
||||
/* Mode-sensitive component roles */
|
||||
--header-bg: var(--surface);
|
||||
--header-interactive-bg: oklch(from var(--header-bg) calc(l + 0.24) c h);
|
||||
--header-glass-bg: color-mix(in oklab, var(--header-bg) 90%, transparent);
|
||||
--tooltip-bg: oklch(from var(--surface) calc(l + 0.06) c h);
|
||||
--terminal-bg: oklch(0.18 0.03 260);
|
||||
--terminal-content: oklch(1 0 0);
|
||||
--progress-track-bg: oklch(0.372 0.044 257.287);
|
||||
--progress-start: oklch(0.4318 0.0865 166.91);
|
||||
--progress-end: oklch(0.621 0.145 189.632);
|
||||
--input-checked-content: oklch(1 0 0);
|
||||
--button-secondary-bg: oklch(0.372 0.044 257.287);
|
||||
--button-secondary-content: oklch(0.928 0.006 264.531);
|
||||
--button-muted-content: oklch(0.704 0.04 256.788);
|
||||
--neutral-status-surface: oklch(0.274 0.006 286.033);
|
||||
--neutral-status-content: oklch(0.985 0.01 285.805);
|
||||
--link: oklch(0.77 0.14 168);
|
||||
--info: oklch(0.8 0.11 255);
|
||||
--warning: oklch(0.82 0.13 80);
|
||||
--success: oklch(0.72 0.13 158);
|
||||
--danger: oklch(0.7 0.16 22);
|
||||
--text-muted: oklch(66.01% 0.0081 264);
|
||||
--text-subtle: oklch(48.91% 0.0113 264);
|
||||
--surface-sunken: oklch(16.5% 0.02 264);
|
||||
--surface-overlay: oklch(23% 0.02 264);
|
||||
--hairline: oklch(98.5% 0.002 264 / 0.1);
|
||||
--hover-faint: oklch(98.5% 0.002 264 / 0.05);
|
||||
--brand-hover: oklch(55% 0.13 188.7);
|
||||
--brand-subtle: oklch(20.52% 0.0287 219.4);
|
||||
--brand-subtle-hover: oklch(24.52% 0.0287 219.4);
|
||||
--focus-ring: oklch(60% 0.13 188.7 / 0.6);
|
||||
--progress-start: oklch(43.18% 0.0865 166.9);
|
||||
--progress-end: oklch(62.1% 0.145 189.6);
|
||||
--info-surface: oklch(32% 0.05 255);
|
||||
--warning-surface: oklch(33% 0.06 80);
|
||||
--success-surface: oklch(30% 0.05 158);
|
||||
--danger-surface: oklch(32% 0.08 22);
|
||||
--danger-surface-hover: oklch(36% 0.08 22);
|
||||
--scrim: oklch(0% 0 0 / 0.6);
|
||||
--mega-menu-bg: oklch(23% 0.02 264 / 0.62);
|
||||
--app-shadow-sm: 0 4px 12px oklch(0 0 0 / 0.3);
|
||||
--app-shadow-md: 0 10px 28px oklch(0 0 0 / 0.42);
|
||||
--app-shadow-lg: 0 20px 48px oklch(0 0 0 / 0.55);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* Layer 1 */
|
||||
--color-canvas: var(--canvas);
|
||||
--color-bg: var(--bg);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-raised: var(--surface-raised);
|
||||
--color-content: var(--content);
|
||||
--color-text: var(--text);
|
||||
--color-brand: var(--brand);
|
||||
--color-on-brand: var(--on-brand);
|
||||
--color-link: var(--link);
|
||||
@@ -178,72 +92,37 @@
|
||||
--color-warning: var(--warning);
|
||||
--color-success: var(--success);
|
||||
--color-danger: var(--danger);
|
||||
|
||||
/* Layer 2: semantic */
|
||||
--color-surface-subtle: var(--surface-subtle);
|
||||
--color-surface-muted: var(--surface-muted);
|
||||
--color-content-muted: var(--content-muted);
|
||||
--color-content-subtle: var(--content-subtle);
|
||||
--color-brand-faint: var(--brand-faint);
|
||||
--color-brand-soft: var(--brand-soft);
|
||||
--color-brand-emphasis: var(--brand-emphasis);
|
||||
--color-brand-emphasis-hover: var(--brand-emphasis-hover);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-text-subtle: var(--text-subtle);
|
||||
--color-surface-sunken: var(--surface-sunken);
|
||||
--color-surface-overlay: var(--surface-overlay);
|
||||
--color-hairline: var(--hairline);
|
||||
--color-hover-faint: var(--hover-faint);
|
||||
--color-brand-hover: var(--brand-hover);
|
||||
--color-brand-subtle: var(--brand-subtle);
|
||||
--color-brand-subtle-hover: var(--brand-subtle-hover);
|
||||
--color-focus-ring: var(--focus-ring);
|
||||
--color-border-faint: var(--border-faint);
|
||||
--color-border-subtle: var(--border-subtle);
|
||||
--color-border-strong: var(--border-strong);
|
||||
--color-glass-surface: var(--glass-surface);
|
||||
--color-glass-raised: var(--glass-raised);
|
||||
--color-scrim: var(--scrim);
|
||||
--color-info-surface: var(--info-surface);
|
||||
--color-info-content: var(--info-content);
|
||||
--color-info-border: var(--info-border);
|
||||
--color-warning-surface: var(--warning-surface);
|
||||
--color-warning-content: var(--warning-content);
|
||||
--color-warning-border: var(--warning-border);
|
||||
--color-success-surface: var(--success-surface);
|
||||
--color-success-content: var(--success-content);
|
||||
--color-success-border: var(--success-border);
|
||||
--color-danger-surface: var(--danger-surface);
|
||||
--color-danger-content: var(--danger-content);
|
||||
--color-danger-border: var(--danger-border);
|
||||
|
||||
/* Layer 2: component */
|
||||
--color-header-bg: var(--header-bg);
|
||||
--color-header-interactive-bg: var(--header-interactive-bg);
|
||||
--color-header-glass-bg: var(--header-glass-bg);
|
||||
--color-tooltip-bg: var(--tooltip-bg);
|
||||
--color-terminal-bg: var(--terminal-bg);
|
||||
--color-terminal-content: var(--terminal-content);
|
||||
--color-progress-track-bg: var(--progress-track-bg);
|
||||
--color-progress-start: var(--progress-start);
|
||||
--color-progress-end: var(--progress-end);
|
||||
--color-input-bg: var(--input-bg);
|
||||
--color-input-checked-content: var(--input-checked-content);
|
||||
--color-button-secondary-bg: var(--button-secondary-bg);
|
||||
--color-button-secondary-hover-bg: var(--button-secondary-hover-bg);
|
||||
--color-button-secondary-content: var(--button-secondary-content);
|
||||
--color-button-secondary-border: var(--button-secondary-border);
|
||||
--color-button-muted-bg: var(--button-muted-bg);
|
||||
--color-button-muted-hover-bg: var(--button-muted-hover-bg);
|
||||
--color-button-muted-content: var(--button-muted-content);
|
||||
--color-button-muted-border: var(--button-muted-border);
|
||||
--color-neutral-status-surface: var(--neutral-status-surface);
|
||||
--color-neutral-status-content: var(--neutral-status-content);
|
||||
--color-neutral-status-border: var(--neutral-status-border);
|
||||
--color-card-action-bg: var(--card-action-bg);
|
||||
--color-interface-badge-bg: var(--interface-badge-bg);
|
||||
--color-segmented-control-bg: var(--segmented-control-bg);
|
||||
--color-ifacebox-header-bg: var(--ifacebox-header-bg);
|
||||
--color-table-header-bg: var(--table-header-bg);
|
||||
--color-table-row-hover-bg: var(--table-row-hover-bg);
|
||||
--color-table-row-alternate-bg: var(--table-row-alternate-bg);
|
||||
--color-info-surface: var(--info-surface);
|
||||
--color-warning-surface: var(--warning-surface);
|
||||
--color-success-surface: var(--success-surface);
|
||||
--color-danger-surface: var(--danger-surface);
|
||||
--color-danger-surface-hover: var(--danger-surface-hover);
|
||||
--color-scrim: var(--scrim);
|
||||
--color-mega-menu-bg: var(--mega-menu-bg);
|
||||
|
||||
--shadow-sm: var(--app-shadow-sm);
|
||||
--shadow-md: var(--app-shadow-md);
|
||||
--shadow-lg: var(--app-shadow-md);
|
||||
--shadow-xl: var(--app-shadow-lg);
|
||||
--shadow-2xl: var(--app-shadow-lg);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--spacing: var(--spacing);
|
||||
--container-max-width: var(--container-max-width);
|
||||
|
||||
--radius-sm: calc(var(--radius-base) * 0.25);
|
||||
--radius: calc(var(--radius-base) * 0.5);
|
||||
--radius-md: calc(var(--radius-base) * 0.75);
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
@apply animate-in fade-in-0 fill-mode-backwards duration-400;
|
||||
@apply animate-in fade-in-0 fill-mode-backwards duration-[250ms];
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
@apply pointer-events-none opacity-0 transition-opacity duration-400;
|
||||
@apply pointer-events-none opacity-0 transition-opacity duration-[250ms];
|
||||
}
|
||||
|
||||
.spinning {
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
.label {
|
||||
@apply bg-neutral-status-surface text-neutral-status-content rounded-full px-3 py-1 text-xs font-bold uppercase;
|
||||
@apply bg-surface-sunken text-text rounded-full px-3 py-1 text-xs font-bold uppercase;
|
||||
|
||||
&.important {
|
||||
@apply bg-info-surface text-info-content;
|
||||
@apply bg-info-surface text-info;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-danger-surface text-danger-content;
|
||||
@apply bg-danger-surface text-danger;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-success-surface text-success-content;
|
||||
@apply bg-success-surface text-success;
|
||||
}
|
||||
|
||||
&.notice {
|
||||
@apply bg-warning-surface text-warning-content;
|
||||
@apply bg-warning-surface text-warning;
|
||||
}
|
||||
}
|
||||
|
||||
.zonebadge {
|
||||
@apply border-border-subtle bg-interface-badge-bg text-content inline-flex items-center gap-1.5 overflow-visible rounded-full border px-1 py-1.5 text-sm font-medium shadow-sm transition-[box-shadow] duration-200 hover:shadow-md max-md:gap-1 max-md:text-xs;
|
||||
@apply border-hairline bg-surface-sunken text-text inline-flex items-center gap-1.5 overflow-visible rounded-full border px-1 py-1.5 text-sm font-medium shadow-sm transition-[box-shadow] duration-150 hover:shadow-md max-md:gap-1 max-md:text-xs;
|
||||
|
||||
&[style*="--zone-color-rgb"] {
|
||||
@apply bg-[rgb(var(--zone-color-rgb),.7)]!;
|
||||
|
||||
& em {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.ifacebadge {
|
||||
@apply border-border-subtle bg-interface-badge-bg text-content hover:border-border-strong hover:bg-interface-badge-bg inline-flex cursor-default flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[box-shadow,border-color,background-color] duration-200 hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs;
|
||||
@apply border-hairline bg-surface-sunken text-text hover:border-hairline hover:bg-surface-sunken inline-flex cursor-default flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[box-shadow,border-color,background-color] duration-150 hover:shadow-md max-md:gap-1.5 max-md:px-2.5 max-md:py-1.5 max-md:text-xs;
|
||||
|
||||
.cbi-dropdown &,
|
||||
.cbi-tooltip & {
|
||||
|
||||
@@ -6,7 +6,7 @@ input[type="reset"],
|
||||
.cbi-button {
|
||||
/* transform stays in this list for the .cbi-button active:scale-95
|
||||
variant in _form.css, which relies on this base transition. */
|
||||
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-2xl border px-3 py-1.5 text-sm font-medium antialiased shadow-sm transition-[background-color,border-color,transform] duration-200 max-md:text-base max-md:font-medium;
|
||||
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-xl border px-3 py-1.5 text-sm font-medium antialiased transition-[background-color,border-color,transform] duration-150 max-md:text-base max-md:font-medium;
|
||||
&[disabled] {
|
||||
@apply cursor-not-allowed opacity-40 dark:opacity-30;
|
||||
}
|
||||
@@ -23,11 +23,11 @@ input[type="reset"],
|
||||
.cbi-button-link,
|
||||
.cbi-button-up,
|
||||
.cbi-button-down {
|
||||
@apply border-button-secondary-border bg-button-secondary-bg text-button-secondary-content hover:bg-button-secondary-hover-bg;
|
||||
@apply border-hairline bg-surface-sunken text-text hover:bg-hover-faint;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
@apply border-button-muted-border bg-button-muted-bg text-button-muted-content hover:bg-button-muted-hover-bg;
|
||||
@apply border-hairline bg-surface-sunken text-text-muted hover:bg-hover-faint;
|
||||
}
|
||||
|
||||
.btn.primary,
|
||||
@@ -42,12 +42,12 @@ input[type="reset"],
|
||||
.cbi-button-positive,
|
||||
.cbi-button-fieldadd,
|
||||
.cbi-button-save {
|
||||
@apply border-brand-emphasis bg-brand-emphasis text-brand hover:bg-brand-emphasis-hover;
|
||||
@apply border-transparent bg-brand-subtle text-brand hover:bg-brand-subtle-hover;
|
||||
}
|
||||
|
||||
.cbi-button-negative,
|
||||
.cbi-section-remove .cbi-button,
|
||||
.cbi-button-reset,
|
||||
.cbi-button-remove {
|
||||
@apply border-danger-border bg-danger-surface text-danger-content hover:bg-danger-border;
|
||||
@apply border-transparent bg-danger-surface text-danger hover:bg-danger-surface-hover;
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
#maincontent & {
|
||||
@apply border-border-faint bg-surface-raised hover:border-border-subtle border shadow-lg hover:shadow-xl;
|
||||
@apply bg-surface border-hairline border shadow-sm;
|
||||
}
|
||||
|
||||
& > .cbi-title {
|
||||
@@ -19,12 +19,12 @@
|
||||
@apply flex-1;
|
||||
|
||||
& .label[data-clickable] {
|
||||
@apply text-content bg-card-action-bg;
|
||||
@apply inline-flex size-7 cursor-pointer items-center justify-center p-0 before:size-4 before:bg-current before:transition-transform before:duration-300 hover:shadow-md active:scale-95 max-md:size-6;
|
||||
@apply text-text bg-surface-sunken;
|
||||
@apply inline-flex size-7 cursor-pointer items-center justify-center p-0 before:size-4 before:bg-current before:transition-transform before:duration-150 hover:shadow-md active:scale-95 max-md:size-6;
|
||||
}
|
||||
|
||||
& span[data-indicator="poll-status"] {
|
||||
@apply relative size-5 cursor-pointer text-[0px] before:absolute before:size-5 before:bg-current before:transition-transform before:duration-300 before:ease-out hover:before:scale-110 active:before:scale-95;
|
||||
@apply relative size-5 cursor-pointer text-[0px] before:absolute before:size-5 before:bg-current before:transition-transform before:duration-150 before:ease-out hover:before:scale-110 active:before:scale-95;
|
||||
|
||||
&[data-style="active"] {
|
||||
@apply before:rotate-0 before:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat];
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
& h3 {
|
||||
@apply text-content border-border-subtle border-b pb-4 max-md:mx-0 max-md:pb-2;
|
||||
@apply text-text border-hairline border-b pb-4 max-md:mx-0 max-md:pb-2;
|
||||
}
|
||||
|
||||
& div[style*="display:grid"] {
|
||||
@@ -63,14 +63,14 @@
|
||||
}
|
||||
|
||||
.ifacebox {
|
||||
@apply border-border-subtle bg-glass-raised hover:border-border-strong max-md:border-border-subtle relative inline-flex min-w-40 flex-col items-stretch overflow-visible rounded-2xl border text-center text-base leading-5 shadow-md hover:shadow-lg max-md:min-w-30 max-md:flex-1 max-md:rounded-3xl max-md:shadow-sm;
|
||||
@apply border-hairline bg-surface-overlay hover:border-hairline max-md:border-hairline relative inline-flex min-w-40 flex-col items-stretch overflow-visible rounded-2xl border text-center text-base leading-5 shadow-md hover:shadow-xl max-md:min-w-30 max-md:flex-1 max-md:rounded-3xl max-md:shadow-sm;
|
||||
|
||||
td & {
|
||||
@apply max-md:flex-row;
|
||||
}
|
||||
|
||||
& .ifacebox-head {
|
||||
@apply border-border-subtle bg-ifacebox-header-bg text-content w-full rounded-t-2xl border-b px-4 py-3 text-center font-semibold tracking-tight transition-[background-color,border-color,color] duration-200 max-md:rounded-t-3xl max-md:px-3 max-md:py-2.5 max-md:text-sm max-md:leading-5 max-md:font-medium;
|
||||
@apply border-hairline bg-surface-sunken text-text w-full rounded-t-2xl border-b px-4 py-3 text-center font-semibold tracking-tight transition-[background-color,border-color,color] duration-150 max-md:rounded-t-3xl max-md:px-3 max-md:py-2.5 max-md:text-sm max-md:leading-5 max-md:font-medium;
|
||||
|
||||
&[style*="--zone-color-rgb"] {
|
||||
@apply bg-[rgb(var(--zone-color-rgb),.75)]!;
|
||||
@@ -93,14 +93,14 @@
|
||||
}
|
||||
|
||||
& .ifacebox-body {
|
||||
@apply text-content flex w-full flex-1 flex-col items-center justify-around gap-2 rounded-b-2xl p-4 text-center max-md:rounded-b-3xl;
|
||||
@apply text-text flex w-full flex-1 flex-col items-center justify-around gap-2 rounded-b-2xl p-4 text-center max-md:rounded-b-3xl;
|
||||
|
||||
td & {
|
||||
@apply max-md:flex-row max-md:rounded-r-3xl max-md:rounded-bl-none max-md:py-2 max-md:pr-2 max-md:pl-4;
|
||||
}
|
||||
|
||||
& > span {
|
||||
@apply text-content space-y-1.5 max-md:space-y-1 max-md:text-sm max-md:leading-5;
|
||||
@apply text-text space-y-1.5 max-md:space-y-1 max-md:text-sm max-md:leading-5;
|
||||
|
||||
.network-status-table & {
|
||||
@apply w-full;
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
|
||||
& .cbi-tooltip-container {
|
||||
@apply text-content static inline-flex cursor-help items-center justify-center;
|
||||
@apply text-text static inline-flex cursor-help items-center justify-center;
|
||||
|
||||
& .cbi-tooltip {
|
||||
@apply bottom-0 md:left-0;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
&:not(.btn):not(.cbi-button) {
|
||||
@apply border-border-subtle bg-surface rounded-2xl border;
|
||||
@apply border-hairline bg-surface rounded-2xl border;
|
||||
& > ul > li {
|
||||
&[placeholder] {
|
||||
@apply hidden;
|
||||
@@ -50,10 +50,10 @@
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
@apply border-border-subtle bg-glass-surface absolute left-0 z-60 w-fit min-w-full overflow-y-auto rounded-lg border shadow-xl backdrop-blur-sm;
|
||||
@apply border-hairline bg-surface-overlay absolute left-0 z-60 w-fit min-w-full overflow-y-auto rounded-lg border shadow-xl backdrop-blur-md backdrop-saturate-150;
|
||||
|
||||
& > li {
|
||||
@apply text-content-muted hover:bg-brand-soft min-h-9 w-full cursor-pointer px-3 py-1.5 font-medium;
|
||||
@apply text-text-muted hover:bg-brand-subtle min-h-9 w-full cursor-pointer px-3 py-1.5 font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
@apply flex;
|
||||
|
||||
&[selected] {
|
||||
@apply bg-brand-emphasis text-brand;
|
||||
@apply bg-brand-subtle text-brand;
|
||||
}
|
||||
|
||||
&[unselectable] {
|
||||
@@ -127,7 +127,7 @@
|
||||
}
|
||||
|
||||
& > .open {
|
||||
@apply border-border-subtle flex flex-none shrink-0 cursor-pointer items-center justify-center border-l px-2.5 py-0.5 text-sm text-current;
|
||||
@apply border-hairline flex flex-none shrink-0 cursor-pointer items-center justify-center border-l px-2.5 py-0.5 text-sm text-current;
|
||||
}
|
||||
|
||||
& > .more {
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
@apply fixed right-4 bottom-4 z-40 flex flex-col items-center max-md:right-3 max-md:bottom-3;
|
||||
|
||||
.toolbar-list {
|
||||
@apply visible mb-2 grid origin-bottom grid-rows-[1fr] opacity-100 transition-[grid-template-rows,transform,opacity,visibility] duration-300 ease-in-out max-md:mb-1.5;
|
||||
@apply visible mb-2 grid origin-bottom grid-rows-[1fr] opacity-100 transition-[grid-template-rows,transform,opacity,visibility] duration-[250ms] ease-in-out max-md:mb-1.5;
|
||||
|
||||
.toolbar-list-inner {
|
||||
@apply border-border-subtle bg-glass-surface flex min-h-0 flex-col items-center gap-2 overflow-hidden rounded-full border p-1 shadow-sm max-md:gap-1.5 max-md:p-0.5;
|
||||
@apply border-hairline bg-surface-overlay flex min-h-0 flex-col items-center gap-2 overflow-hidden rounded-full border p-1 shadow-sm max-md:gap-1.5 max-md:p-0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
@apply flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full p-2 transition-[background-color,transform,opacity] duration-200 ease-in-out active:scale-95 max-md:h-9 max-md:w-9 max-md:p-1.5;
|
||||
@apply flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full p-2 transition-[background-color,transform,opacity] duration-150 ease-in-out active:scale-95 max-md:h-9 max-md:w-9 max-md:p-1.5;
|
||||
|
||||
.icon {
|
||||
@apply h-5 w-5 shrink-0 transition-transform duration-300 ease-in-out max-md:h-4 max-md:w-4;
|
||||
@apply h-5 w-5 shrink-0 transition-transform duration-150 ease-in-out max-md:h-4 max-md:w-4;
|
||||
}
|
||||
|
||||
&:not(.toggle) {
|
||||
@apply hover:bg-border-faint no-underline hover:scale-110;
|
||||
@apply hover:bg-hover-faint no-underline hover:scale-110;
|
||||
.icon {
|
||||
@apply object-contain dark:invert;
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
&.toggle {
|
||||
@apply bg-surface border-0 shadow-sm;
|
||||
.icon {
|
||||
@apply transition-transform duration-300 ease-out;
|
||||
@apply transition-transform duration-150 ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.cbi-input-invalid,
|
||||
.cbi-value-error {
|
||||
@apply !border-danger-border focus:!ring-danger-border;
|
||||
@apply !border-danger focus:!ring-danger;
|
||||
}
|
||||
|
||||
.cbi-value {
|
||||
@@ -25,10 +25,10 @@
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
& .cbi-value-description {
|
||||
@apply text-content-muted relative mt-1 pl-4 leading-none wrap-break-word;
|
||||
@apply text-text-muted relative mt-1 pl-4 leading-none wrap-break-word;
|
||||
|
||||
&:not(:empty) {
|
||||
@apply before:bg-info-content before:absolute before:-left-0.5 before:inline-block before:size-4 before:[mask:url('@assets/icons/info.svg')_center/cover_no-repeat];
|
||||
@apply before:bg-info before:absolute before:-left-0.5 before:inline-block before:size-4 before:[mask:url('@assets/icons/info.svg')_center/cover_no-repeat];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,15 +43,15 @@
|
||||
@apply inline-flex w-max max-w-120 flex-col items-start gap-3 max-md:max-w-full!;
|
||||
|
||||
& .item {
|
||||
@apply border-border-faint bg-surface-raised pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-2xl border py-3 pr-10 pl-4 break-all shadow-sm transition-[transform,box-shadow,opacity,border-color,background-color] duration-200 select-text max-md:w-full max-md:py-2.5 max-md:pr-8 max-md:pl-3;
|
||||
@apply after:bg-button-muted-bg after:text-button-muted-content after:absolute after:top-2.5 after:right-2 after:inline-flex after:h-6 after:w-6 after:shrink-0 after:cursor-pointer after:items-center after:justify-center after:rounded-xl after:text-sm after:font-medium after:content-['\00D7'];
|
||||
@apply border-hairline bg-surface pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-2xl border py-3 pr-10 pl-4 break-all shadow-sm transition-[transform,box-shadow,opacity,border-color,background-color] duration-150 select-text max-md:w-full max-md:py-2.5 max-md:pr-8 max-md:pl-3;
|
||||
@apply after:bg-surface-sunken after:text-text-muted after:absolute after:top-2.5 after:right-2 after:inline-flex after:h-6 after:w-6 after:shrink-0 after:cursor-pointer after:items-center after:justify-center after:rounded-xl after:text-sm after:font-medium after:content-['\00D7'];
|
||||
|
||||
&.dragging {
|
||||
@apply scale-95 cursor-grabbing opacity-50 shadow-2xl;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
@apply border-brand bg-brand-faint ring-brand-emphasis scale-105 shadow-lg ring-2;
|
||||
@apply border-brand bg-brand-subtle ring-brand-subtle scale-105 shadow-lg ring-2;
|
||||
}
|
||||
|
||||
& > span,
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
|
||||
& .add-item {
|
||||
@apply border-border-subtle hover:border-border-strong hover:bg-surface-subtle inline-flex items-center gap-1.5 self-stretch rounded-2xl border border-dashed bg-transparent px-1 py-1 transition-[border-color,background-color] duration-200;
|
||||
@apply border-hairline hover:border-hairline hover:bg-surface-sunken inline-flex items-center gap-1.5 self-stretch rounded-2xl border border-dashed bg-transparent px-1 py-1 transition-[border-color,background-color] duration-150;
|
||||
|
||||
& > input {
|
||||
@apply min-w-61 flex-1;
|
||||
|
||||
@@ -6,7 +6,7 @@ input[type="text"],
|
||||
input[type="password"],
|
||||
.cbi-input-text,
|
||||
.cbi-input {
|
||||
@apply text-content border-border-subtle bg-surface placeholder-content-muted focus:border-brand focus:ring-brand-emphasis relative rounded-2xl border px-3 py-1.5 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
|
||||
@apply text-text border-hairline bg-surface-sunken placeholder-text-muted focus:border-brand focus:ring-focus-ring relative rounded-2xl border px-3 py-1.5 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-150 focus:ring-2 focus:outline-none;
|
||||
|
||||
.table.cbi-section-table & {
|
||||
@apply w-full;
|
||||
@@ -23,7 +23,7 @@ input[type="password"],
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
@apply focus:before:border-brand focus:before:ring-brand-emphasis checked:before:border-brand checked:before:bg-brand before:border-border-subtle before:bg-surface after:bg-input-checked-content hover:before:border-border-strong relative mr-3 inline-block h-4 w-4 cursor-pointer appearance-none before:absolute before:top-0 before:left-0 before:h-4 before:w-4 before:border before:transition-[background-color,border-color,box-shadow] before:duration-200 after:absolute after:top-0.5 after:left-0.5 after:h-3 after:w-3 after:opacity-0 after:transition-opacity after:duration-200 checked:after:opacity-100 focus:before:ring-2 focus:before:outline-none disabled:cursor-not-allowed;
|
||||
@apply focus:before:border-brand focus:before:ring-focus-ring checked:before:border-brand checked:before:bg-brand before:border-hairline before:bg-surface-sunken after:bg-on-brand hover:before:border-hairline relative mr-3 inline-block h-4 w-4 cursor-pointer appearance-none before:absolute before:top-0 before:left-0 before:h-4 before:w-4 before:border before:transition-[background-color,border-color,box-shadow] before:duration-150 after:absolute after:top-0.5 after:left-0.5 after:h-3 after:w-3 after:opacity-0 after:transition-opacity after:duration-150 checked:after:opacity-100 focus:before:ring-2 focus:before:outline-none disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.alert-message {
|
||||
@apply bg-neutral-status-surface text-neutral-status-content border-neutral-status-border sticky top-14 z-40 mb-4 rounded-4xl border p-6 max-md:mx-0 max-md:mb-3 max-md:rounded-3xl max-md:p-4;
|
||||
@apply bg-surface-sunken text-text border-hairline sticky top-14 z-40 mb-4 rounded-4xl border p-6 max-md:mx-0 max-md:mb-3 max-md:rounded-3xl max-md:p-4;
|
||||
|
||||
&.modal {
|
||||
@apply static top-auto;
|
||||
@@ -14,21 +14,21 @@
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply border-success-border bg-success-surface text-success-content;
|
||||
@apply border-hairline bg-success-surface text-success;
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply border-info-border bg-info-surface text-info-content;
|
||||
@apply border-hairline bg-info-surface text-info;
|
||||
}
|
||||
|
||||
&.warning,
|
||||
&.notice {
|
||||
@apply border-warning-border bg-warning-surface text-warning-content;
|
||||
@apply border-hairline bg-warning-surface text-warning;
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.danger {
|
||||
@apply border-danger-border bg-danger-surface text-danger-content;
|
||||
@apply border-hairline bg-danger-surface text-danger;
|
||||
}
|
||||
|
||||
h4,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
@apply pointer-events-none fixed top-0 bottom-0 hidden overflow-auto bg-transparent;
|
||||
|
||||
.modal-overlay-active & {
|
||||
@apply bg-scrim pointer-events-auto inset-0 right-0 left-0 z-100 grid grid-cols-1 place-items-center backdrop-blur-sm;
|
||||
@apply bg-scrim pointer-events-auto inset-0 right-0 left-0 z-100 grid grid-cols-1 place-items-center backdrop-blur-md backdrop-saturate-150;
|
||||
}
|
||||
|
||||
& > .modal {
|
||||
@apply border-border-subtle bg-surface relative flex w-5xl flex-col gap-4 rounded-3xl border p-6 wrap-break-word whitespace-normal shadow-2xl max-md:w-full max-md:gap-3 max-md:p-4;
|
||||
@apply border-hairline bg-surface-overlay relative flex w-5xl flex-col gap-4 rounded-3xl border p-6 wrap-break-word whitespace-normal shadow-2xl max-md:w-full max-md:gap-3 max-md:p-4;
|
||||
|
||||
& h4 {
|
||||
@apply mb-0 text-center;
|
||||
@@ -16,26 +16,26 @@
|
||||
}
|
||||
|
||||
& h5 {
|
||||
@apply text-content my-3;
|
||||
@apply text-text my-3;
|
||||
}
|
||||
|
||||
& p {
|
||||
@apply text-content text-sm leading-relaxed;
|
||||
@apply text-text text-sm leading-relaxed;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
@apply border-border-subtle bg-surface-subtle overflow-auto rounded-2xl border p-3;
|
||||
@apply border-hairline overflow-auto rounded-2xl border p-3;
|
||||
}
|
||||
|
||||
& label.btn {
|
||||
@apply border-border-subtle bg-surface-subtle text-content hover:border-border-strong border;
|
||||
@apply border-hairline bg-surface-sunken text-text hover:border-hairline border;
|
||||
}
|
||||
|
||||
& pre {
|
||||
@apply overflow-auto;
|
||||
|
||||
&.errors {
|
||||
@apply border-danger-border bg-danger-surface text-danger-content;
|
||||
@apply border-hairline bg-danger-surface text-danger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
& .button-row,
|
||||
& div.left,
|
||||
& div.right {
|
||||
@apply border-border-subtle flex shrink-0 gap-3 border-t p-4 max-md:gap-1.5 max-md:p-2.5;
|
||||
@apply border-hairline flex shrink-0 gap-3 border-t p-4 max-md:gap-1.5 max-md:p-2.5;
|
||||
}
|
||||
|
||||
& div.left {
|
||||
@@ -66,22 +66,22 @@
|
||||
}
|
||||
|
||||
& ins {
|
||||
@apply border-success-border bg-success-surface text-success-content border;
|
||||
@apply border-hairline bg-success-surface text-success border;
|
||||
}
|
||||
|
||||
& del {
|
||||
@apply border-danger-border bg-danger-surface text-danger-content border;
|
||||
@apply border-hairline bg-danger-surface text-danger border;
|
||||
}
|
||||
|
||||
& var {
|
||||
@apply border-info-border bg-info-surface text-info-content border;
|
||||
@apply border-hairline bg-info-surface text-info border;
|
||||
}
|
||||
|
||||
& .uci-change-legend {
|
||||
@apply border-border-subtle bg-surface-subtle mt-4 grid grid-cols-2 gap-3 rounded-2xl border p-4 max-md:grid-cols-1 max-md:gap-2 max-md:p-3;
|
||||
@apply border-hairline bg-surface-sunken mt-4 grid grid-cols-2 gap-3 rounded-2xl border p-4 max-md:grid-cols-1 max-md:gap-2 max-md:p-3;
|
||||
|
||||
& .uci-change-legend-label {
|
||||
@apply text-content flex items-center gap-2 text-sm font-medium;
|
||||
@apply text-text flex items-center gap-2 text-sm font-medium;
|
||||
|
||||
& ins,
|
||||
& del,
|
||||
@@ -102,7 +102,7 @@
|
||||
@apply mt-4 space-y-3;
|
||||
|
||||
& h5 {
|
||||
@apply bg-neutral-status-surface text-neutral-status-content mt-6 mb-2 rounded-xl px-3 py-2 text-sm font-semibold first:mt-0;
|
||||
@apply bg-surface-sunken text-text mt-6 mb-2 rounded-xl px-3 py-2 text-sm font-semibold first:mt-0;
|
||||
}
|
||||
|
||||
& ins,
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
*/
|
||||
|
||||
.nav-link {
|
||||
@apply text-content hover:bg-header-interactive-bg block rounded-xl px-3 py-1.5 no-underline transition-all duration-150;
|
||||
@apply text-text hover:bg-hover-faint block rounded-xl px-3 py-1.5 no-underline transition-all duration-150;
|
||||
}
|
||||
|
||||
/* Mode-agnostic active state: brand text + weight only. The selected-pill
|
||||
background is a sidebar affordance (set in _layout.css); the mobile drawer
|
||||
deliberately omits it and uses an accent indicator bar instead (_overlay.css).
|
||||
Repeats text-brand on hover so the base link's hover color never bleeds through. */
|
||||
.nav-link-active {
|
||||
@apply text-brand hover:text-brand font-medium;
|
||||
.navigation-direct {
|
||||
@apply text-text hover:text-text;
|
||||
}
|
||||
|
||||
/* Direct destinations use the same selected state in both navigation modes:
|
||||
a filled brand pill, matching the active sublink. */
|
||||
.navigation-direct.is-active-page {
|
||||
@apply text-brand hover:text-brand bg-brand-subtle font-medium;
|
||||
}
|
||||
|
||||
/* Category rows are group labels, not buttons: no hover surface, just a
|
||||
@@ -24,28 +26,53 @@
|
||||
appearance-none + border-0 + shadow-none strip the theme's global
|
||||
<button> chrome (_button.css), which would otherwise box every row. */
|
||||
.nav-category {
|
||||
@apply text-content-muted hover:text-content focus-visible:ring-focus-ring flex w-full cursor-pointer appearance-none items-center gap-2 rounded-lg border-0 bg-transparent px-3 py-2 text-left text-lg font-semibold tracking-wide shadow-none transition-colors duration-150 select-none focus-visible:ring-2 focus-visible:outline-none;
|
||||
@apply text-text-muted hover:text-text focus-visible:ring-focus-ring flex w-full cursor-pointer appearance-none items-center gap-2 rounded-lg border-0 bg-transparent px-3 py-2 text-left text-lg font-semibold tracking-wide shadow-none transition-colors duration-150 select-none focus-visible:ring-2 focus-visible:outline-none;
|
||||
}
|
||||
|
||||
/* Trailing chevron shares arrow-right.svg with the collapsible cards:
|
||||
points right when closed, rotates to point down when open. */
|
||||
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-200 after:content-[''] after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] hover:after:opacity-100;
|
||||
.navigation-group-toggle {
|
||||
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-[250ms] after:content-[''] after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat] hover:after:opacity-100;
|
||||
}
|
||||
|
||||
.nav-category-label {
|
||||
@apply min-w-0 flex-1 truncate;
|
||||
}
|
||||
|
||||
/* Open group: label darkens to foreground; the active child's pill is the
|
||||
sole location anchor, so no primary tint on the label itself. */
|
||||
.sidebar-group-open > .nav-category {
|
||||
@apply text-content after:rotate-90;
|
||||
/* An expanded group's label turns brand and its arrow rotates open. */
|
||||
.navigation-group.is-expanded > .navigation-group-toggle {
|
||||
@apply text-brand after:rotate-90;
|
||||
}
|
||||
|
||||
/* The active group keeps its brand label even when the user manually
|
||||
collapses it — otherwise the active-page pill is hidden inside the inert
|
||||
region and no "you are here" marker survives. No arrow rotation here: a
|
||||
collapsed group's arrow must still point right. */
|
||||
.navigation-group.is-active-group > .navigation-group-toggle {
|
||||
@apply text-brand;
|
||||
}
|
||||
|
||||
.navigation-group-region {
|
||||
@apply grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-[250ms] ease-out;
|
||||
}
|
||||
|
||||
.navigation-group.is-expanded > .navigation-group-region {
|
||||
@apply grid-rows-[1fr] opacity-100;
|
||||
}
|
||||
|
||||
.navigation-submenu-list {
|
||||
@apply min-h-0 list-none overflow-hidden;
|
||||
}
|
||||
|
||||
.navigation-sublink {
|
||||
@apply text-text-muted hover:bg-hover-faint hover:text-text relative flex rounded-lg font-medium no-underline transition-colors duration-150;
|
||||
}
|
||||
|
||||
/* Active page: a filled brand pill, shared by the sidebar and the drawer. */
|
||||
.navigation-sublink.is-active-page {
|
||||
@apply text-brand hover:text-brand bg-brand-subtle font-semibold;
|
||||
}
|
||||
|
||||
/* The desktop sidebar no longer uses previews; the mobile drawer is the
|
||||
sole consumer and sets its own px/text-size (_overlay.css), so the base
|
||||
only carries the mode-agnostic bits. */
|
||||
.nav-category-preview {
|
||||
@apply text-content-subtle truncate pb-1;
|
||||
@apply text-text-subtle truncate pb-1;
|
||||
|
||||
& .nav-preview-current {
|
||||
@apply text-brand;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.mobile-menu-overlay {
|
||||
@apply max-md:invisible max-md:fixed max-md:inset-0 max-md:z-60 max-md:bg-(--header-bg)/45 max-md:opacity-0 max-md:backdrop-blur-2xl max-md:backdrop-saturate-200 max-md:transition-[visibility,opacity] max-md:duration-280 md:hidden;
|
||||
@apply max-md:bg-mega-menu-bg max-md:invisible max-md:fixed max-md:inset-0 max-md:z-60 max-md:opacity-0 max-md:backdrop-blur-lg max-md:backdrop-saturate-150 max-md:transition-[visibility,opacity] max-md:duration-[250ms] md:hidden;
|
||||
|
||||
&.mobile-menu-open {
|
||||
@apply max-md:visible max-md:opacity-100;
|
||||
@@ -12,70 +12,43 @@
|
||||
@apply max-md:flex max-md:min-h-0 max-md:flex-1 max-md:flex-col max-md:px-5 max-md:pt-6;
|
||||
|
||||
& .mobile-nav-label {
|
||||
@apply max-md:text-content-muted max-md:mb-3 max-md:text-sm max-md:font-medium;
|
||||
@apply max-md:text-text-muted max-md:mb-3 max-md:text-sm max-md:font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
& .mobile-nav-list {
|
||||
@apply max-md:m-0 max-md:flex max-md:min-h-0 max-md:flex-1 max-md:-translate-y-2 max-md:list-none max-md:flex-col max-md:overflow-y-auto max-md:overscroll-contain max-md:p-0 max-md:pb-5 max-md:opacity-0 max-md:transition-[translate,opacity] max-md:duration-280 max-md:ease-out;
|
||||
@apply max-md:m-0 max-md:flex max-md:min-h-0 max-md:flex-1 max-md:-translate-y-2 max-md:list-none max-md:flex-col max-md:overflow-y-auto max-md:overscroll-contain max-md:p-0 max-md:pb-5 max-md:opacity-0 max-md:transition-[translate,opacity] max-md:duration-[250ms] max-md:ease-out;
|
||||
|
||||
& .mobile-nav-item {
|
||||
@apply max-md:shrink-0;
|
||||
|
||||
& .mobile-nav-link {
|
||||
@apply max-md:text-content max-md:hover:text-brand max-md:flex max-md:min-h-13 max-md:w-full max-md:items-center max-md:justify-between max-md:gap-4 max-md:rounded-none max-md:border-0 max-md:bg-transparent max-md:px-0 max-md:py-2.5 max-md:text-left max-md:text-2xl max-md:leading-tight max-md:font-medium max-md:tracking-tight max-md:no-underline max-md:shadow-none max-md:transition-colors max-md:duration-200;
|
||||
|
||||
&.nav-link-active {
|
||||
@apply max-md:text-brand max-md:hover:text-brand;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-submenu > .mobile-nav-link {
|
||||
@apply max-md:after:size-4 max-md:after:shrink-0 max-md:after:bg-current max-md:after:opacity-55 max-md:after:transition-transform max-md:after:duration-200 max-md:after:content-[''] max-md:after:[mask:url('@assets/icons/arrow-right.svg')_center/cover_no-repeat];
|
||||
}
|
||||
|
||||
&.submenu-expanded > .mobile-nav-link {
|
||||
@apply max-md:after:rotate-90;
|
||||
@apply max-md:flex max-md:min-h-13 max-md:w-full max-md:items-center max-md:justify-between max-md:gap-4 max-md:rounded-none max-md:border-0 max-md:bg-transparent max-md:px-0 max-md:py-2.5 max-md:text-left max-md:text-2xl max-md:leading-tight max-md:font-medium max-md:tracking-tight max-md:no-underline max-md:shadow-none;
|
||||
}
|
||||
|
||||
& .mobile-nav-submenu {
|
||||
@apply max-md:grid max-md:grid-rows-[0fr] max-md:opacity-0 max-md:transition-[grid-template-rows,opacity] max-md:duration-300 max-md:ease-in-out;
|
||||
|
||||
& .mobile-nav-submenu-list {
|
||||
@apply max-md:before:bg-border-subtle max-md:relative max-md:mx-0 max-md:mt-0 max-md:mb-2 max-md:min-h-0 max-md:list-none max-md:overflow-hidden max-md:py-1 max-md:pr-0 max-md:pl-4 max-md:before:absolute max-md:before:top-1 max-md:before:bottom-1 max-md:before:left-0 max-md:before:w-px max-md:before:origin-top max-md:before:scale-y-0 max-md:before:transition-transform max-md:before:duration-300 max-md:before:ease-out max-md:before:content-[''];
|
||||
@apply max-md:mx-0 max-md:mt-0 max-md:mb-2 max-md:py-1 max-md:pr-0 max-md:pl-4;
|
||||
}
|
||||
|
||||
& .mobile-nav-subitem {
|
||||
& .mobile-nav-sublink {
|
||||
@apply max-md:text-content max-md:hover:text-brand max-md:relative max-md:flex max-md:min-h-10 max-md:w-full max-md:items-center max-md:py-2 max-md:text-[0.9375rem] max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200;
|
||||
|
||||
&.nav-link-active {
|
||||
@apply max-md:text-brand max-md:hover:text-brand max-md:font-semibold;
|
||||
@apply max-md:before:bg-brand max-md:before:absolute max-md:before:top-1.5 max-md:before:bottom-1.5 max-md:before:-left-4 max-md:before:w-0.5 max-md:before:rounded-full max-md:before:content-[''];
|
||||
}
|
||||
@apply max-md:min-h-10 max-md:w-full max-md:items-center max-md:px-3 max-md:py-2 max-md:text-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.submenu-expanded .mobile-nav-submenu {
|
||||
@apply max-md:grid-rows-[1fr] max-md:opacity-100;
|
||||
|
||||
& .mobile-nav-submenu-list {
|
||||
@apply max-md:before:scale-y-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .mobile-nav-footer {
|
||||
@apply max-md:border-border-faint max-md:flex max-md:shrink-0 max-md:items-center max-md:justify-between max-md:border-t max-md:px-5 max-md:pt-4 max-md:pb-[max(1rem,env(safe-area-inset-bottom))];
|
||||
@apply max-md:border-hairline max-md:flex max-md:shrink-0 max-md:items-center max-md:justify-between max-md:border-t max-md:px-5 max-md:pt-4 max-md:pb-[max(1rem,env(safe-area-inset-bottom))];
|
||||
|
||||
& .mobile-nav-footer-action {
|
||||
@apply max-md:flex max-md:items-center;
|
||||
}
|
||||
|
||||
& .mobile-nav-logout {
|
||||
@apply max-md:text-content max-md:hover:text-brand max-md:text-sm max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-200;
|
||||
@apply max-md:text-text max-md:hover:text-brand max-md:text-sm max-md:font-medium max-md:no-underline max-md:transition-colors max-md:duration-150;
|
||||
}
|
||||
|
||||
& .theme-switcher {
|
||||
@@ -97,10 +70,10 @@ body.mobile-navigation-open > header {
|
||||
|
||||
[data-nav-type="mega-menu"] {
|
||||
& .desktop-menu-overlay {
|
||||
@apply bg-scrim pointer-events-none fixed inset-0 z-50 opacity-0 transition-opacity duration-300 max-md:hidden;
|
||||
@apply bg-scrim pointer-events-none fixed inset-0 z-50 opacity-0 transition-opacity duration-[250ms] max-md:hidden;
|
||||
|
||||
&.active {
|
||||
@apply pointer-events-auto opacity-100 backdrop-blur-sm;
|
||||
@apply pointer-events-auto opacity-100 backdrop-blur-lg backdrop-saturate-150;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
.cbi-progressbar {
|
||||
@apply before:text-content bg-progress-track-bg relative h-3.5 w-full cursor-help overflow-hidden rounded-full before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-2xl before:text-xs before:whitespace-nowrap before:content-[attr(title)] max-md:h-4 max-md:rounded-2xl max-md:before:text-xs max-md:before:leading-normal;
|
||||
@apply before:text-text bg-surface-sunken relative h-3.5 w-full cursor-help overflow-hidden rounded-full before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-2xl before:text-xs before:whitespace-nowrap before:content-[attr(title)] max-md:h-4 max-md:rounded-2xl max-md:before:text-xs max-md:before:leading-normal;
|
||||
[data-page="admin-system-package-manager"] & {
|
||||
@apply max-sm:before:text-[10px];
|
||||
}
|
||||
|
||||
& > div {
|
||||
@apply from-progress-start to-progress-end h-full bg-linear-to-r transition-[width] duration-300;
|
||||
@apply from-progress-start to-progress-end h-full bg-linear-to-r transition-[width] duration-[250ms];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.cbi-tabmenu {
|
||||
@apply border-border-subtle bg-segmented-control-bg mb-1 flex items-center gap-1 overflow-x-auto rounded-4xl border p-1;
|
||||
@apply border-hairline bg-surface-sunken mb-1 flex items-center gap-1 overflow-x-auto rounded-4xl border p-1;
|
||||
|
||||
& li {
|
||||
@apply relative m-0 min-w-fit shrink-0 list-none;
|
||||
@@ -7,15 +7,15 @@
|
||||
@apply after:bg-danger after:text-on-brand after:absolute after:-top-0.5 after:-right-1.5 after:z-20 after:inline-flex after:min-h-4 after:min-w-4 after:items-center after:justify-center after:rounded-full after:px-1.5 after:py-0.5 after:text-xs after:font-semibold after:content-[attr(data-errors)];
|
||||
|
||||
& > a {
|
||||
@apply border-danger-border border;
|
||||
@apply border-hairline border;
|
||||
}
|
||||
}
|
||||
& > a {
|
||||
@apply text-content-muted hover:text-content block rounded-4xl px-4 py-2 text-center text-sm font-medium whitespace-nowrap no-underline;
|
||||
@apply text-text-muted hover:text-text block rounded-4xl px-4 py-2 text-center text-sm font-medium whitespace-nowrap no-underline;
|
||||
}
|
||||
|
||||
&.cbi-tab a {
|
||||
@apply bg-content text-surface font-semibold shadow-sm;
|
||||
@apply bg-text text-surface font-semibold shadow-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
select {
|
||||
@apply text-content border-border-subtle bg-surface focus:border-brand focus:ring-brand-emphasis appearance-none rounded-2xl border px-3 py-1.5 pr-10 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
|
||||
@apply text-text border-hairline bg-surface-sunken focus:border-brand focus:ring-focus-ring appearance-none rounded-2xl border px-3 py-1.5 pr-10 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-150 focus:ring-2 focus:outline-none;
|
||||
@apply bg-[url('@assets/icons/arrow-down.svg')] bg-size-[16px] bg-position-[right_0.75rem_center] bg-no-repeat dark:bg-[url('@assets/icons/arrow-down-dark.svg')];
|
||||
&[disabled] {
|
||||
@apply cursor-not-allowed opacity-40 dark:opacity-30;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
.theme-switcher {
|
||||
@apply bg-surface relative inline-flex items-center gap-0 rounded-full shadow-sm backdrop-blur-sm transition-transform duration-300 max-md:py-0.5;
|
||||
@apply border-border-subtle border;
|
||||
@apply before:bg-border-subtle before:ring-border-subtle before:absolute before:top-1 before:left-1 before:z-0 before:h-[calc(100%-0.5rem)] before:w-[calc(33.333%-0.5rem)] before:rounded-full before:shadow-md before:ring-1 before:transition-transform before:duration-300;
|
||||
@apply bg-surface relative inline-flex items-center gap-0 rounded-full shadow-sm backdrop-blur-md backdrop-saturate-150 transition-transform duration-150 max-md:py-0.5;
|
||||
@apply border-hairline border;
|
||||
@apply before:bg-hairline before:ring-hairline before:absolute before:top-1 before:left-1 before:z-0 before:h-[calc(100%-0.5rem)] before:w-[calc(33.333%-0.5rem)] before:rounded-full before:shadow-md before:ring-1 before:transition-transform before:duration-[250ms];
|
||||
|
||||
footer & {
|
||||
@apply max-md:hidden;
|
||||
}
|
||||
|
||||
& .theme-option {
|
||||
@apply relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-full px-3 py-1.5 transition-transform duration-300 hover:scale-105 active:scale-95;
|
||||
@apply relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-full px-3 py-1.5 transition-transform duration-150 hover:scale-105 active:scale-95;
|
||||
|
||||
& input[type="radio"] {
|
||||
@apply sr-only;
|
||||
}
|
||||
|
||||
& .theme-icon {
|
||||
@apply text-content-muted flex items-center justify-center transition-colors duration-300;
|
||||
@apply text-text-muted flex items-center justify-center transition-colors duration-150;
|
||||
|
||||
& svg {
|
||||
@apply size-4 transition-transform duration-300;
|
||||
@apply size-4 transition-transform duration-150;
|
||||
}
|
||||
}
|
||||
|
||||
& .theme-label {
|
||||
@apply text-content-muted hidden text-xs font-medium transition-colors duration-300;
|
||||
@apply text-text-muted hidden text-xs font-medium transition-colors duration-150;
|
||||
}
|
||||
|
||||
&.active {
|
||||
& .theme-icon {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
|
||||
& svg {
|
||||
@apply scale-110;
|
||||
@@ -36,17 +36,17 @@
|
||||
}
|
||||
|
||||
& .theme-label {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
& .theme-icon {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
}
|
||||
|
||||
& .theme-label {
|
||||
@apply text-content;
|
||||
@apply text-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@apply border-border-subtle relative flex items-center gap-0 overflow-x-auto border-b;
|
||||
@apply border-hairline relative flex items-center gap-0 overflow-x-auto border-b;
|
||||
|
||||
& a {
|
||||
@apply text-content-muted hover:text-content block px-8 py-4 text-center text-sm font-medium whitespace-nowrap transition-colors duration-200 hover:no-underline;
|
||||
@apply text-text-muted hover:text-text block px-8 py-4 text-center text-sm font-medium whitespace-nowrap transition-colors duration-150 hover:no-underline;
|
||||
}
|
||||
|
||||
& > li.active {
|
||||
@apply border-content border-b-2;
|
||||
@apply border-text border-b-2;
|
||||
& > a {
|
||||
@apply text-content font-semibold;
|
||||
@apply text-text font-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
table.table,
|
||||
.table {
|
||||
@apply border-border-subtle max-md:bg-glass-raised mb-3 w-full border-separate border-spacing-0 overflow-visible rounded-2xl border max-md:mx-0 max-md:block max-md:w-full max-md:overflow-visible max-md:border-0 max-md:shadow-none;
|
||||
@apply border-hairline max-md:bg-surface-overlay mb-3 w-full border border-separate border-spacing-0 overflow-visible rounded-2xl max-md:mx-0 max-md:block max-md:w-full max-md:overflow-visible max-md:border-0 max-md:shadow-none;
|
||||
|
||||
&[width="100%"] {
|
||||
@apply w-full;
|
||||
@@ -17,7 +17,7 @@ table.table,
|
||||
& .cbi-section-table-titles.named,
|
||||
& .cbi-section-table-descr.named,
|
||||
& .cbi-section-table-row[data-title] {
|
||||
@apply before:border-border-subtle before:table-cell before:px-3 before:py-3 before:align-middle before:text-base before:font-semibold before:content-[attr(data-title)] before:max-md:flex before:max-md:flex-[0_0_100%] before:max-md:px-3 before:max-md:py-2.5 before:max-md:text-sm before:md:border-b;
|
||||
@apply before:border-hairline before:table-cell before:px-3 before:py-3 before:align-middle before:text-base before:font-semibold before:content-[attr(data-title)] before:max-md:flex before:max-md:flex-[0_0_100%] before:max-md:px-3 before:max-md:py-2.5 before:max-md:text-sm before:md:border-b;
|
||||
&:not([data-title]),
|
||||
&:has(td.cbi-section-table-titles) {
|
||||
@apply before:content-none;
|
||||
@@ -25,7 +25,7 @@ table.table,
|
||||
}
|
||||
|
||||
& .cbi-section-table-titles.named {
|
||||
@apply before:bg-table-header-bg before:max-md:bg-table-header-bg before:md:rounded-tl-2xl;
|
||||
@apply before:text-text-subtle before:text-xs before:font-semibold before:tracking-wider before:uppercase;
|
||||
}
|
||||
|
||||
& tbody,
|
||||
@@ -35,30 +35,16 @@ table.table,
|
||||
|
||||
& tr,
|
||||
& .tr {
|
||||
@apply hover:bg-table-row-hover-bg transition-colors duration-200 max-md:border-border-subtle max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
|
||||
@apply max-md:border-hairline max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
|
||||
@apply has-[.cbi-progressbar]:max-md:flex has-[.cbi-progressbar]:max-md:items-center has-[.cbi-progressbar]:max-md:justify-center;
|
||||
|
||||
&:first-child {
|
||||
&:not([data-title]) {
|
||||
& .th,
|
||||
& .td {
|
||||
@apply first:md:rounded-tl-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
& .th,
|
||||
& .td {
|
||||
@apply last:md:rounded-tr-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply max-md:rounded-b-2xl;
|
||||
&[data-title] {
|
||||
@apply before:rounded-bl-2xl before:border-b-0;
|
||||
@apply before:border-b-0;
|
||||
}
|
||||
.td {
|
||||
@apply border-b-0 first:md:rounded-bl-2xl last:md:rounded-br-2xl;
|
||||
@apply border-b-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +65,9 @@ table.table,
|
||||
@apply max-md:rounded-t-2xl;
|
||||
}
|
||||
|
||||
&.cbi-rowstyle-1 {
|
||||
@apply bg-surface-raised;
|
||||
}
|
||||
|
||||
&.cbi-rowstyle-1,
|
||||
&.cbi-rowstyle-2 {
|
||||
@apply bg-table-row-alternate-bg;
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
& > .col-1 {
|
||||
@@ -130,7 +113,7 @@ table.table,
|
||||
|
||||
& th,
|
||||
& .th {
|
||||
@apply border-border-subtle bg-table-header-bg border-b px-3 py-3 text-left text-base font-medium max-md:flex-[1_1_50%] max-md:border-0 max-md:text-sm;
|
||||
@apply border-hairline bg-surface-sunken text-text-subtle border-b px-3 py-3 text-left text-xs font-semibold tracking-wider uppercase first:rounded-tl-2xl last:rounded-tr-2xl max-md:flex-[1_1_50%] max-md:border-0 max-md:bg-transparent;
|
||||
|
||||
#cbi-samba4 & {
|
||||
@apply px-0.5 py-1.5;
|
||||
@@ -145,7 +128,7 @@ table.table,
|
||||
}
|
||||
|
||||
&[data-sortable-row="true"] {
|
||||
@apply hover:bg-table-row-hover-bg cursor-pointer select-none after:ml-0.5 after:text-xs;
|
||||
@apply hover:bg-hover-faint cursor-pointer select-none after:ml-0.5 after:text-xs;
|
||||
&[data-sort-direction="asc"] {
|
||||
@apply after:content-['\25B2'];
|
||||
}
|
||||
@@ -163,7 +146,7 @@ table.table,
|
||||
|
||||
& td,
|
||||
& .td {
|
||||
@apply border-border-subtle table-cell border-b px-3 py-3 align-middle text-base wrap-break-word whitespace-normal max-md:flex-[1_1_50%] max-md:border-0 max-md:px-1 max-md:py-1 max-md:text-sm;
|
||||
@apply border-hairline table-cell border-b px-3 py-3 align-middle text-base wrap-break-word whitespace-normal max-md:flex-[1_1_50%] max-md:border-0 max-md:px-1 max-md:py-1 max-md:text-sm;
|
||||
@apply before:hidden before:content-[attr(data-title)] before:max-md:mb-1 before:max-md:block before:max-md:text-xs before:max-md:font-semibold before:max-md:tracking-wider;
|
||||
|
||||
#cbi-samba4 & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
textarea {
|
||||
@apply text-content border-border-subtle bg-surface placeholder-content-muted focus:border-brand focus:ring-brand-emphasis min-h-24 w-full resize-y rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-200 focus:ring-2 focus:outline-none;
|
||||
@apply text-text border-hairline bg-surface-sunken placeholder-text-muted focus:border-brand focus:ring-focus-ring min-h-24 w-full resize-y rounded-2xl border px-3 py-2 text-sm font-normal shadow-sm transition-[border-color,box-shadow] duration-150 focus:ring-2 focus:outline-none;
|
||||
&[disabled] {
|
||||
@apply cursor-not-allowed opacity-40 dark:opacity-30;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
.cbi-tooltip {
|
||||
@apply bg-tooltip-bg text-content border-border-subtle absolute z-110 max-w-xs scale-95 rounded-xl border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-lg transition-[visibility,opacity,scale] duration-150 ease-out;
|
||||
@apply bg-surface-overlay text-text border-hairline absolute z-110 max-w-xs scale-95 rounded-xl border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-xl transition-[visibility,opacity,scale] duration-150 ease-out;
|
||||
&.error {
|
||||
@apply border-danger-border bg-danger-surface text-danger-content;
|
||||
@apply border-hairline bg-danger-surface text-danger;
|
||||
}
|
||||
&.success {
|
||||
@apply border-success-border bg-success-surface text-success-content;
|
||||
@apply border-hairline bg-success-surface text-success;
|
||||
}
|
||||
&.info {
|
||||
@apply border-info-border bg-info-surface text-info-content;
|
||||
@apply border-hairline bg-info-surface text-info;
|
||||
}
|
||||
&.notice {
|
||||
@apply border-warning-border bg-warning-surface text-warning-content;
|
||||
@apply border-hairline bg-warning-surface text-warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,10 @@
|
||||
@import "tailwindcss";
|
||||
@import "./_tokens.css";
|
||||
@import "./_reduced-motion.css";
|
||||
@import "./_base.css";
|
||||
|
||||
@custom-variant dark (&:where([data-darkmode=true], [data-darkmode=true] *));
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply bg-canvas h-full font-sans;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply text-content bg-canvas min-h-full;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes divider-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -38,16 +29,14 @@
|
||||
|
||||
/* Fixed sibling layer (not form ancestor) on its own GPU surface so input
|
||||
paints don't re-sample the bg image. DOM order keeps the form on top. */
|
||||
/* LQIP shown immediately; falls back to full image when no LQIP stored */
|
||||
.login-bg {
|
||||
@apply pointer-events-none fixed inset-0 transform-[translateZ(0)] bg-cover bg-center bg-no-repeat;
|
||||
/* LQIP shown immediately; falls back to full image when no LQIP stored */
|
||||
background-image: var(--login-bg-lqip, var(--login-bg));
|
||||
@apply pointer-events-none fixed inset-0 transform-[translateZ(0)] bg-cover bg-center bg-no-repeat [background-image:var(--login-bg-lqip,var(--login-bg))];
|
||||
}
|
||||
|
||||
/* Full image fades in on top once loaded (compositor-only opacity change) */
|
||||
.login-bg::after {
|
||||
@apply pointer-events-none absolute inset-0 content-[''] bg-cover bg-center bg-no-repeat opacity-0 [will-change:opacity] [transition:opacity_0.5s_ease];
|
||||
background-image: var(--login-bg);
|
||||
@apply pointer-events-none absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 [will-change:opacity] content-[''] [transition:opacity_0.5s_ease] [background-image:var(--login-bg)];
|
||||
}
|
||||
|
||||
.login-bg.full-loaded::after {
|
||||
@@ -55,7 +44,7 @@
|
||||
}
|
||||
|
||||
.login-card {
|
||||
@apply bg-surface-raised border-border-subtle flex flex-col gap-7 rounded-4xl border p-14 max-md:rounded-3xl max-md:p-9;
|
||||
@apply bg-surface flex flex-col gap-7 rounded-4xl p-14 shadow-lg max-md:rounded-3xl max-md:p-9;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
@@ -63,11 +52,11 @@
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
@apply via-border-subtle h-px w-full border-0 bg-linear-to-r from-transparent to-transparent animate-[divider-in_0.7s_cubic-bezier(0.16,1,0.3,1)_0.1s_both];
|
||||
@apply via-hairline h-px w-full animate-[divider-in_0.7s_cubic-bezier(0.16,1,0.3,1)_0.1s_both] border-0 bg-linear-to-r from-transparent to-transparent;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
@apply bg-brand-soft ring-brand-emphasis flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl ring-1;
|
||||
@apply bg-brand-subtle ring-brand-subtle flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl ring-1;
|
||||
|
||||
& img {
|
||||
@apply h-6 w-6;
|
||||
@@ -79,11 +68,11 @@
|
||||
}
|
||||
|
||||
.login-hostname {
|
||||
@apply text-content text-xl font-semibold tracking-tight;
|
||||
@apply text-text text-xl font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
@apply text-content text-sm;
|
||||
@apply text-text text-sm;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
@@ -99,11 +88,11 @@
|
||||
}
|
||||
|
||||
.login-label {
|
||||
@apply text-content text-[0.6875rem] font-semibold tracking-widest uppercase;
|
||||
@apply text-text text-xs font-semibold tracking-widest uppercase;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
@apply border-border-subtle bg-surface-raised relative rounded-2xl border [contain:layout_style];
|
||||
@apply border-hairline bg-surface-sunken relative rounded-2xl border [contain:layout_style];
|
||||
}
|
||||
|
||||
/* Focus ring: opacity-only transition runs on compositor thread, not main thread —
|
||||
@@ -117,11 +106,11 @@
|
||||
}
|
||||
|
||||
.login-input {
|
||||
@apply text-content placeholder:text-content-subtle w-full appearance-none rounded-2xl border-0 bg-transparent px-3 py-2.5 text-base outline-none;
|
||||
@apply text-text placeholder:text-text-subtle w-full appearance-none rounded-2xl border-0 bg-transparent px-3 py-2.5 text-base outline-none;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
@apply bg-danger-surface text-danger-content border-danger-border rounded-2xl border px-3 py-2 text-center text-sm;
|
||||
@apply bg-danger-surface text-danger border-hairline rounded-2xl border px-3 py-2 text-center text-sm;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
@@ -133,7 +122,7 @@
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
@apply text-content-muted absolute right-0 bottom-5 left-0 px-5 text-center text-xs;
|
||||
@apply text-text-muted absolute right-0 bottom-5 left-0 px-5 text-center text-xs;
|
||||
|
||||
& p {
|
||||
@apply whitespace-normal;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
@import "./_tokens.css";
|
||||
|
||||
@import "./_base.css";
|
||||
@import "./_elements.css";
|
||||
@import "./_layout.css";
|
||||
|
||||
@import "./components/_nav.css";
|
||||
|
||||
@@ -27,24 +27,13 @@ return baseclass.extend({
|
||||
|
||||
if (!navigationToggle || !overlay) return;
|
||||
|
||||
const mobileList = overlay.querySelector("#mobile-nav-list");
|
||||
const desktop = window.matchMedia("(min-width: 768px)");
|
||||
const SIDEBAR_COLLAPSED_KEY = "aurora.sidebarCollapsed";
|
||||
|
||||
const isDesktopSidebar = () =>
|
||||
desktop.matches && document.body.dataset.navType === "sidebar";
|
||||
|
||||
const resetMobileSubmenus = () => {
|
||||
overlay
|
||||
.querySelectorAll(".mobile-nav-item.submenu-expanded")
|
||||
.forEach((item) => this.setMobileSubmenuExpanded(item, false));
|
||||
};
|
||||
|
||||
const expandActiveMobileGroup = () => {
|
||||
const activeGroup = overlay.querySelector(".mobile-nav-item.has-active");
|
||||
|
||||
if (activeGroup) this.setMobileSubmenuExpanded(activeGroup, true);
|
||||
};
|
||||
|
||||
const updateToggleState = (expanded) => {
|
||||
const desktopSidebar = isDesktopSidebar();
|
||||
|
||||
@@ -74,7 +63,7 @@ return baseclass.extend({
|
||||
overlay.classList.remove("mobile-menu-open");
|
||||
document.body.classList.remove("mobile-navigation-open");
|
||||
document.body.style.overflow = "";
|
||||
resetMobileSubmenus();
|
||||
this.resetNavigationGroups(mobileList);
|
||||
};
|
||||
|
||||
const getNavigationExpanded = () => {
|
||||
@@ -106,8 +95,8 @@ return baseclass.extend({
|
||||
document.body.classList.toggle("mobile-navigation-open", expanded);
|
||||
document.body.style.overflow = expanded ? "hidden" : "";
|
||||
|
||||
if (expanded) expandActiveMobileGroup();
|
||||
else resetMobileSubmenus();
|
||||
if (expanded) this.expandActiveNavigationGroup(mobileList);
|
||||
else this.resetNavigationGroups(mobileList);
|
||||
|
||||
updateToggleState(expanded);
|
||||
};
|
||||
@@ -151,31 +140,8 @@ return baseclass.extend({
|
||||
syncNavigationState();
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const toggle = e.target.closest(".mobile-nav-toggle");
|
||||
|
||||
if (toggle && overlay.contains(toggle)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const item = toggle.closest(".mobile-nav-item");
|
||||
if (!item) return;
|
||||
|
||||
const isExpanded = item.classList.contains("submenu-expanded");
|
||||
|
||||
overlay
|
||||
.querySelectorAll(".mobile-nav-item.submenu-expanded")
|
||||
.forEach((expandedItem) => {
|
||||
if (expandedItem !== item) {
|
||||
this.setMobileSubmenuExpanded(expandedItem, false);
|
||||
}
|
||||
});
|
||||
|
||||
this.setMobileSubmenuExpanded(item, !isExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
const destination = e.target.closest(
|
||||
".mobile-nav-link:not(.mobile-nav-toggle), .mobile-nav-sublink, .mobile-nav-logout",
|
||||
const destination = e.target?.closest?.(
|
||||
".navigation-direct, .navigation-sublink, .mobile-nav-logout",
|
||||
);
|
||||
|
||||
if (destination && overlay.contains(destination)) {
|
||||
@@ -184,105 +150,31 @@ return baseclass.extend({
|
||||
});
|
||||
},
|
||||
|
||||
renderMobileMenu(tree, url) {
|
||||
renderMobileMenu(items) {
|
||||
const list = document.querySelector("#mobile-nav-list");
|
||||
const footerAction = document.querySelector("#mobile-nav-footer-action");
|
||||
const children = ui.menu.getChildren(tree);
|
||||
|
||||
if (list) list.innerHTML = "";
|
||||
if (footerAction) footerAction.innerHTML = "";
|
||||
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = "";
|
||||
if (footerAction) footerAction.innerHTML = "";
|
||||
if (!children.length) return;
|
||||
|
||||
children.forEach((child) => {
|
||||
const submenu = ui.menu.getChildren(child);
|
||||
const hasSubmenu = submenu.length > 0;
|
||||
const isActive = this.isActivePath(child.name);
|
||||
|
||||
if (child.name === "logout") {
|
||||
items.forEach((item) => {
|
||||
if (item.isLogout) {
|
||||
if (footerAction) {
|
||||
footerAction.appendChild(
|
||||
E(
|
||||
"a",
|
||||
{
|
||||
class: "mobile-nav-logout",
|
||||
href: L.url(url, child.name),
|
||||
},
|
||||
[_(child.title)],
|
||||
),
|
||||
E("a", { class: "mobile-nav-logout", href: item.href }, [
|
||||
item.title,
|
||||
]),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const itemClasses = [
|
||||
"mobile-nav-item",
|
||||
hasSubmenu ? "has-submenu" : "",
|
||||
isActive && hasSubmenu ? "has-active" : "",
|
||||
].filter(Boolean);
|
||||
const submenuId = `mobile-submenu-${String(child.name).replace(
|
||||
/[^A-Za-z0-9_-]/g,
|
||||
"-",
|
||||
)}`;
|
||||
const control = hasSubmenu
|
||||
? E(
|
||||
"button",
|
||||
{
|
||||
class: `mobile-nav-link mobile-nav-toggle${
|
||||
isActive ? " nav-link-active" : ""
|
||||
}`,
|
||||
type: "button",
|
||||
"aria-expanded": "false",
|
||||
"aria-controls": submenuId,
|
||||
},
|
||||
[_(child.title)],
|
||||
)
|
||||
: E(
|
||||
"a",
|
||||
{
|
||||
class: `mobile-nav-link${isActive ? " nav-link-active" : ""}`,
|
||||
href: L.url(url, child.name),
|
||||
},
|
||||
[_(child.title)],
|
||||
);
|
||||
|
||||
const li = E("li", { class: itemClasses.join(" ") }, [control]);
|
||||
|
||||
if (hasSubmenu) {
|
||||
const wrap = E("div", {
|
||||
class: "mobile-nav-submenu",
|
||||
id: submenuId,
|
||||
"aria-hidden": "true",
|
||||
inert: "",
|
||||
});
|
||||
const ul = E("ul", { class: "mobile-nav-submenu-list" });
|
||||
|
||||
submenu.forEach((item) => {
|
||||
const subActive = this.isActivePath(child.name, item.name);
|
||||
|
||||
ul.appendChild(
|
||||
E("li", { class: "mobile-nav-subitem" }, [
|
||||
E(
|
||||
"a",
|
||||
{
|
||||
class: `mobile-nav-sublink${
|
||||
subActive ? " nav-link-active" : ""
|
||||
}`,
|
||||
href: L.url(url, child.name, item.name),
|
||||
},
|
||||
[_(item.title)],
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
wrap.appendChild(ul);
|
||||
li.appendChild(wrap);
|
||||
}
|
||||
|
||||
list.appendChild(li);
|
||||
list.appendChild(this.renderNavigationItem(item, "mobile"));
|
||||
});
|
||||
|
||||
this.bindNavigationAccordion(list);
|
||||
},
|
||||
|
||||
render(tree) {
|
||||
@@ -336,26 +228,32 @@ return baseclass.extend({
|
||||
return ul;
|
||||
},
|
||||
|
||||
renderMainMenu(tree, url, level = 0) {
|
||||
renderMainMenu(tree, url, level = 0, navigationItems = null) {
|
||||
const ul = level
|
||||
? E("ul", { class: "desktop-nav-list" })
|
||||
: document.querySelector("#topmenu");
|
||||
const children = ui.menu.getChildren(tree);
|
||||
|
||||
if (!children.length || level > 1) return E([]);
|
||||
if (level > 1) return E([]);
|
||||
|
||||
if (level === 0) {
|
||||
const navType = document.body?.dataset?.navType || "mega-menu";
|
||||
|
||||
if (navType === "sidebar") {
|
||||
this.renderSidebar(children, url);
|
||||
return ul;
|
||||
} else if (navType === "mega-menu") {
|
||||
this.renderSidebar(navigationItems || []);
|
||||
return ul || E([]);
|
||||
}
|
||||
|
||||
if (!ul || !children.length) return E([]);
|
||||
|
||||
if (navType === "mega-menu") {
|
||||
this.initMegaMenu(children, url, ul);
|
||||
} else {
|
||||
this.initBoxedDropdown(children, url, ul);
|
||||
}
|
||||
} else {
|
||||
if (!children.length) return E([]);
|
||||
|
||||
children.forEach((child) => {
|
||||
ul.appendChild(
|
||||
E("li", {}, [
|
||||
@@ -377,144 +275,208 @@ return baseclass.extend({
|
||||
return page == null || L.env.dispatchpath[2] === page;
|
||||
},
|
||||
|
||||
setMobileSubmenuExpanded(item, expanded) {
|
||||
const toggle = item.querySelector(".mobile-nav-toggle");
|
||||
const submenu = item.querySelector(".mobile-nav-submenu");
|
||||
buildNavigationModel(children, url) {
|
||||
return children
|
||||
.filter((child) => child?.name)
|
||||
.map((child) => {
|
||||
const pages = ui.menu
|
||||
.getChildren(child)
|
||||
.filter((page) => page?.name)
|
||||
.map((page) => ({
|
||||
name: page.name,
|
||||
title: _(page.title),
|
||||
href: L.url(url, child.name, page.name),
|
||||
isActivePage: this.isActivePath(child.name, page.name),
|
||||
}));
|
||||
const hasChildren = pages.length > 0;
|
||||
const isCurrentTopLevel = this.isActivePath(child.name);
|
||||
|
||||
item.classList.toggle("submenu-expanded", expanded);
|
||||
toggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
submenu?.setAttribute("aria-hidden", expanded ? "false" : "true");
|
||||
|
||||
if (expanded) submenu?.removeAttribute("inert");
|
||||
else submenu?.setAttribute("inert", "");
|
||||
return {
|
||||
name: child.name,
|
||||
title: _(child.title),
|
||||
href: L.url(url, child.name),
|
||||
hasChildren,
|
||||
isLogout: child.name === "logout",
|
||||
isActiveGroup: hasChildren && isCurrentTopLevel,
|
||||
isActivePage: !hasChildren && isCurrentTopLevel,
|
||||
activePage: pages.find((page) => page.isActivePage) || null,
|
||||
pages,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSidebarSectionExpanded(item, expanded) {
|
||||
const category = item.querySelector(".nav-category");
|
||||
const section = item.querySelector(".sidebar-section");
|
||||
navigationSubmenuId(mode, name) {
|
||||
return `${mode}-submenu-${encodeURIComponent(String(name))}`;
|
||||
},
|
||||
|
||||
item.classList.toggle("sidebar-group-open", expanded);
|
||||
category?.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
section?.setAttribute("aria-hidden", expanded ? "false" : "true");
|
||||
renderNavigationItem(item, mode) {
|
||||
const mobile = mode === "mobile";
|
||||
const itemClass = mobile ? "mobile-nav-item" : "";
|
||||
const directClass = mobile
|
||||
? "navigation-direct mobile-nav-link"
|
||||
: "navigation-direct nav-link";
|
||||
|
||||
if (!item.hasChildren) {
|
||||
const attributes = {
|
||||
class: `${directClass}${item.isActivePage ? " is-active-page" : ""}`,
|
||||
href: item.href,
|
||||
};
|
||||
if (item.isActivePage) attributes["aria-current"] = "page";
|
||||
return E("li", { class: itemClass }, [E("a", attributes, [item.title])]);
|
||||
}
|
||||
|
||||
const submenuId = this.navigationSubmenuId(mode, item.name);
|
||||
const groupClasses = [
|
||||
"navigation-group",
|
||||
mobile ? "mobile-nav-item" : "sidebar-group",
|
||||
item.isActiveGroup ? "is-active-group" : "",
|
||||
item.isActiveGroup ? "is-expanded" : "",
|
||||
].filter(Boolean);
|
||||
const toggleAttributes = {
|
||||
class: mobile
|
||||
? "navigation-group-toggle mobile-nav-link mobile-nav-toggle"
|
||||
: "navigation-group-toggle nav-category",
|
||||
type: "button",
|
||||
"aria-expanded": item.isActiveGroup ? "true" : "false",
|
||||
"aria-controls": submenuId,
|
||||
};
|
||||
if (item.isActiveGroup) toggleAttributes["aria-current"] = "location";
|
||||
|
||||
const list = E("ul", {
|
||||
class: mobile
|
||||
? "navigation-submenu-list mobile-nav-submenu-list"
|
||||
: "navigation-submenu-list sidebar-submenu",
|
||||
});
|
||||
item.pages.forEach((page) => {
|
||||
const linkAttributes = {
|
||||
class: [
|
||||
"navigation-sublink",
|
||||
mobile ? "mobile-nav-sublink" : "",
|
||||
page.isActivePage ? "is-active-page" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
href: page.href,
|
||||
};
|
||||
if (page.isActivePage) linkAttributes["aria-current"] = "page";
|
||||
list.appendChild(
|
||||
E("li", { class: mobile ? "mobile-nav-subitem" : "" }, [
|
||||
E("a", linkAttributes, [page.title]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
const regionAttributes = {
|
||||
class: mobile
|
||||
? "navigation-group-region mobile-nav-submenu"
|
||||
: "navigation-group-region sidebar-section",
|
||||
id: submenuId,
|
||||
"aria-hidden": item.isActiveGroup ? "false" : "true",
|
||||
};
|
||||
if (!item.isActiveGroup) regionAttributes.inert = "";
|
||||
|
||||
return E("li", { class: groupClasses.join(" ") }, [
|
||||
E("button", toggleAttributes, [
|
||||
E("span", { class: "nav-category-label" }, [item.title]),
|
||||
]),
|
||||
E("div", regionAttributes, [list]),
|
||||
]);
|
||||
},
|
||||
|
||||
setNavigationGroupExpanded(item, expanded) {
|
||||
const toggle = item.querySelector(".navigation-group-toggle");
|
||||
const region = item.querySelector(".navigation-group-region");
|
||||
|
||||
item.classList.toggle("is-expanded", expanded);
|
||||
toggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
region?.setAttribute("aria-hidden", expanded ? "false" : "true");
|
||||
|
||||
if (expanded) region?.removeAttribute("inert");
|
||||
else region?.setAttribute("inert", "");
|
||||
},
|
||||
|
||||
setExclusiveNavigationGroupExpanded(surface, item, expanded) {
|
||||
if (!surface || !item) return;
|
||||
|
||||
if (expanded) {
|
||||
section?.removeAttribute("inert");
|
||||
} else {
|
||||
section?.setAttribute("inert", "");
|
||||
surface
|
||||
.querySelectorAll(".navigation-group.is-expanded")
|
||||
.forEach((expandedItem) => {
|
||||
if (expandedItem !== item) {
|
||||
this.setNavigationGroupExpanded(expandedItem, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setNavigationGroupExpanded(item, expanded);
|
||||
},
|
||||
|
||||
resetNavigationGroups(surface) {
|
||||
surface
|
||||
?.querySelectorAll(".navigation-group.is-expanded")
|
||||
.forEach((item) => this.setNavigationGroupExpanded(item, false));
|
||||
},
|
||||
|
||||
expandActiveNavigationGroup(surface) {
|
||||
const activeGroup = surface?.querySelector(
|
||||
".navigation-group.is-active-group",
|
||||
);
|
||||
|
||||
if (activeGroup) {
|
||||
this.setExclusiveNavigationGroupExpanded(surface, activeGroup, true);
|
||||
}
|
||||
},
|
||||
|
||||
renderSidebar(children, url) {
|
||||
bindNavigationAccordion(surface) {
|
||||
if (!surface || surface.dataset.accordionBound === "true") return;
|
||||
|
||||
surface.dataset.accordionBound = "true";
|
||||
surface.addEventListener("click", (event) => {
|
||||
const toggle = event.target?.closest?.(".navigation-group-toggle");
|
||||
const item = toggle?.closest?.(".navigation-group");
|
||||
|
||||
if (!item || !surface.contains(item)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.setExclusiveNavigationGroupExpanded(
|
||||
surface,
|
||||
item,
|
||||
!item.classList.contains("is-expanded"),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
renderSidebar(items) {
|
||||
const list = document.querySelector("#sidebar-list");
|
||||
const footer = document.querySelector("#sidebar-footer");
|
||||
const crumbEl = document.querySelector("#header-crumb");
|
||||
|
||||
if (list) list.innerHTML = "";
|
||||
if (footer) footer.innerHTML = "";
|
||||
if (crumbEl) crumbEl.innerHTML = "";
|
||||
|
||||
if (!list) return;
|
||||
|
||||
const crumb = [];
|
||||
|
||||
const link = (href, title, active) =>
|
||||
E("a", { class: `nav-link${active ? " nav-link-active" : ""}`, href }, [
|
||||
_(title),
|
||||
]);
|
||||
|
||||
children.forEach((child) => {
|
||||
const submenu = ui.menu.getChildren(child);
|
||||
const inGroup = this.isActivePath(child.name);
|
||||
|
||||
if (inGroup) {
|
||||
crumb.push(_(child.title));
|
||||
|
||||
const page = submenu.find((item) =>
|
||||
this.isActivePath(child.name, item.name),
|
||||
);
|
||||
if (page) crumb.push(_(page.title));
|
||||
items.forEach((item) => {
|
||||
if (item.isActiveGroup || item.isActivePage) {
|
||||
crumb.push(item.title);
|
||||
if (item.activePage) crumb.push(item.activePage.title);
|
||||
}
|
||||
|
||||
if (child.name === "logout") {
|
||||
if (item.isLogout) {
|
||||
(footer || list).appendChild(
|
||||
link(L.url(url, child.name), child.title, false),
|
||||
E("a", { class: "nav-link", href: item.href }, [item.title]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!submenu.length) {
|
||||
list.appendChild(
|
||||
E("li", {}, [link(L.url(url, child.name), child.title, inGroup)]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionId = `sidebar-section-${String(child.name).replace(
|
||||
/[^A-Za-z0-9_-]/g,
|
||||
"-",
|
||||
)}`;
|
||||
const ul = E("ul", { class: "sidebar-submenu" });
|
||||
|
||||
submenu.forEach((item) => {
|
||||
ul.appendChild(
|
||||
E("li", {}, [
|
||||
link(
|
||||
L.url(url, child.name, item.name),
|
||||
item.title,
|
||||
this.isActivePath(child.name, item.name),
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
const sectionAttrs = {
|
||||
class: "sidebar-section",
|
||||
id: sectionId,
|
||||
"aria-hidden": inGroup ? "false" : "true",
|
||||
};
|
||||
|
||||
if (!inGroup) sectionAttrs.inert = "";
|
||||
|
||||
const groupClasses = [
|
||||
"sidebar-group",
|
||||
inGroup ? "sidebar-group-open" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
list.appendChild(
|
||||
E(
|
||||
"li",
|
||||
{
|
||||
class: groupClasses.join(" "),
|
||||
"data-section": child.name,
|
||||
},
|
||||
[
|
||||
E(
|
||||
"button",
|
||||
{
|
||||
class: "nav-category",
|
||||
type: "button",
|
||||
"aria-expanded": inGroup ? "true" : "false",
|
||||
"aria-controls": sectionId,
|
||||
},
|
||||
[E("span", { class: "nav-category-label" }, [_(child.title)])],
|
||||
),
|
||||
E("div", sectionAttrs, [ul]),
|
||||
],
|
||||
),
|
||||
);
|
||||
list.appendChild(this.renderNavigationItem(item, "sidebar"));
|
||||
});
|
||||
|
||||
list.addEventListener("click", (e) => {
|
||||
const category = e.target.closest(".nav-category");
|
||||
const item = category?.closest(".sidebar-group");
|
||||
|
||||
if (!item || !list.contains(item)) return;
|
||||
|
||||
const shouldOpen = !item.classList.contains("sidebar-group-open");
|
||||
|
||||
list.querySelectorAll(".sidebar-group-open").forEach((group) => {
|
||||
if (group !== item) this.setSidebarSectionExpanded(group, false);
|
||||
});
|
||||
|
||||
this.setSidebarSectionExpanded(item, shouldOpen);
|
||||
});
|
||||
|
||||
const crumbEl = document.querySelector("#header-crumb");
|
||||
this.bindNavigationAccordion(list);
|
||||
|
||||
crumb.forEach((title, i) => {
|
||||
if (i) crumbEl?.appendChild(E("li", { class: "crumb-sep" }, ["/"]));
|
||||
@@ -789,25 +751,31 @@ return baseclass.extend({
|
||||
? child.name === L.env.requestpath[0]
|
||||
: index === 0;
|
||||
|
||||
ul.appendChild(
|
||||
E(
|
||||
"li",
|
||||
{
|
||||
class: isActive ? "active" : "",
|
||||
},
|
||||
[E("a", { href: L.url(child.name) }, [_(child.title)])],
|
||||
),
|
||||
);
|
||||
if (ul) {
|
||||
ul.appendChild(
|
||||
E(
|
||||
"li",
|
||||
{
|
||||
class: isActive ? "active" : "",
|
||||
},
|
||||
[E("a", { href: L.url(child.name) }, [_(child.title)])],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isActive) activeChild = child;
|
||||
});
|
||||
|
||||
if (activeChild) {
|
||||
this.renderMainMenu(activeChild, activeChild.name);
|
||||
this.renderMobileMenu(activeChild, activeChild.name);
|
||||
const navigationItems = this.buildNavigationModel(
|
||||
ui.menu.getChildren(activeChild),
|
||||
activeChild.name,
|
||||
);
|
||||
this.renderMainMenu(activeChild, activeChild.name, 0, navigationItems);
|
||||
this.renderMobileMenu(navigationItems);
|
||||
}
|
||||
|
||||
if (ul.children.length > 1) {
|
||||
if (ul?.children.length > 1) {
|
||||
ul.style.display = "";
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import Color from "colorjs.io";
|
||||
import { mix, shade, set, alpha, konst, toOklch } from "../tokens/engine.js";
|
||||
|
||||
const l = (v) => new Color(toOklch(v)).to("oklch").coords[0];
|
||||
const aOf = (v) => {
|
||||
const c = new Color(toOklch(v));
|
||||
return c.alpha;
|
||||
};
|
||||
|
||||
test("mix: 50% white + black ≈ mid gray", () => {
|
||||
const L = l(mix("white", "black", 0.5));
|
||||
assert.ok(L > 0.4 && L < 0.6, `L=${L}`);
|
||||
});
|
||||
|
||||
test("shade: lowers lightness by delta", () => {
|
||||
const L = l(shade("oklch(0.5 0.1 200)", -0.1));
|
||||
assert.ok(Math.abs(L - 0.4) < 0.02, `L=${L}`);
|
||||
});
|
||||
|
||||
test("set: fixes L and C, keeps hue", () => {
|
||||
const c = new Color(
|
||||
toOklch(set("oklch(0.5 0.1 200)", 0.9, 0.05)),
|
||||
).to("oklch");
|
||||
assert.ok(Math.abs(c.coords[0] - 0.9) < 0.001);
|
||||
assert.ok(Math.abs(c.coords[1] - 0.05) < 0.001);
|
||||
assert.ok(Math.abs(c.coords[2] - 200) < 0.5);
|
||||
});
|
||||
|
||||
test("alpha: sets opacity", () => {
|
||||
assert.ok(
|
||||
Math.abs(aOf(alpha("oklch(0.6 0.1 200)", 0.5)) - 0.5) < 0.001,
|
||||
);
|
||||
});
|
||||
|
||||
test("toOklch: serializes without color-mix/var", () => {
|
||||
const s = toOklch(mix("oklch(0.68 0.11 233)", "white", 0.15));
|
||||
assert.ok(s.startsWith("oklch("));
|
||||
assert.ok(!s.includes("color-mix") && !s.includes("var("));
|
||||
});
|
||||
@@ -0,0 +1,569 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
const source = await readFile(
|
||||
new URL("../src/resource/menu-aurora.js", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const menuOrder = (item) => (Number.isFinite(item.order) ? item.order : 1000);
|
||||
|
||||
const compareMenuItems = (a, b) => {
|
||||
const orderDifference = menuOrder(a) - menuOrder(b);
|
||||
|
||||
if (orderDifference) return orderDifference;
|
||||
if (a.name === b.name) return 0;
|
||||
|
||||
return a.name < b.name ? -1 : 1;
|
||||
};
|
||||
|
||||
const getMenuChildren = (children = {}) =>
|
||||
Object.entries(children)
|
||||
.filter(([, child]) => child?.satisfied !== false && child?.title)
|
||||
.map(([name, child]) => ({ ...child, name }))
|
||||
.sort(compareMenuItems);
|
||||
|
||||
const loadMenuModule = (
|
||||
dispatchpath,
|
||||
getChildren = (item) => getMenuChildren(item?.children),
|
||||
) => {
|
||||
const baseclass = {
|
||||
extend(module) {
|
||||
return module;
|
||||
},
|
||||
};
|
||||
const ui = { menu: { getChildren } };
|
||||
const E = () => ({});
|
||||
const L = {
|
||||
env: { dispatchpath },
|
||||
url: (...segments) => `/${segments.join("/")}`,
|
||||
};
|
||||
const translate = (title) => `translated:${title}`;
|
||||
const document = {};
|
||||
const window = {};
|
||||
const localStorage = {};
|
||||
|
||||
return new Function(
|
||||
"baseclass",
|
||||
"ui",
|
||||
"E",
|
||||
"L",
|
||||
"_",
|
||||
"document",
|
||||
"window",
|
||||
"localStorage",
|
||||
source,
|
||||
)(baseclass, ui, E, L, translate, document, window, localStorage);
|
||||
};
|
||||
|
||||
test("normalizes, orders, and filters a mixed navigation model", () => {
|
||||
const menu = loadMenuModule(["admin", "network", "wireless"]);
|
||||
const tree = {
|
||||
children: {
|
||||
logout: { title: "Logout", order: 30 },
|
||||
titleless: { order: 5 },
|
||||
status: { title: "Status", order: 20 },
|
||||
unavailable: { title: "Unavailable", order: 4, satisfied: false },
|
||||
network: {
|
||||
title: "Network",
|
||||
order: 10,
|
||||
children: {
|
||||
wireless: { title: "Wireless", order: 10 },
|
||||
hidden: { title: "Hidden", order: 5, satisfied: false },
|
||||
titleless: { order: 6 },
|
||||
interfaces: { title: "Interfaces", order: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
menu.buildNavigationModel(getMenuChildren(tree.children), "admin"),
|
||||
[
|
||||
{
|
||||
name: "network",
|
||||
title: "translated:Network",
|
||||
href: "/admin/network",
|
||||
hasChildren: true,
|
||||
isLogout: false,
|
||||
isActiveGroup: true,
|
||||
isActivePage: false,
|
||||
activePage: {
|
||||
name: "wireless",
|
||||
title: "translated:Wireless",
|
||||
href: "/admin/network/wireless",
|
||||
isActivePage: true,
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
name: "interfaces",
|
||||
title: "translated:Interfaces",
|
||||
href: "/admin/network/interfaces",
|
||||
isActivePage: false,
|
||||
},
|
||||
{
|
||||
name: "wireless",
|
||||
title: "translated:Wireless",
|
||||
href: "/admin/network/wireless",
|
||||
isActivePage: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
title: "translated:Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: false,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
},
|
||||
{
|
||||
name: "logout",
|
||||
title: "translated:Logout",
|
||||
href: "/admin/logout",
|
||||
hasChildren: false,
|
||||
isLogout: true,
|
||||
isActiveGroup: false,
|
||||
isActivePage: false,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("marks a direct current destination as an active page, not a group", () => {
|
||||
const menu = loadMenuModule(["admin", "status"]);
|
||||
const tree = {
|
||||
children: {
|
||||
status: { title: "Status" },
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
menu.buildNavigationModel(getMenuChildren(tree.children), "admin"),
|
||||
[
|
||||
{
|
||||
name: "status",
|
||||
title: "translated:Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: true,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("omits invalid top-level items without a name", () => {
|
||||
const menu = loadMenuModule(["admin", "status"]);
|
||||
|
||||
assert.deepEqual(
|
||||
menu.buildNavigationModel(
|
||||
[
|
||||
{ title: "Invalid", children: {} },
|
||||
...getMenuChildren({
|
||||
status: { title: "Status" },
|
||||
}),
|
||||
],
|
||||
"admin",
|
||||
),
|
||||
[
|
||||
{
|
||||
name: "status",
|
||||
title: "translated:Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: true,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("omits pages without a name returned by the menu API", () => {
|
||||
const getChildren = () => [
|
||||
{ title: "Invalid" },
|
||||
{ name: "wireless", title: "Wireless" },
|
||||
];
|
||||
const menu = loadMenuModule(["admin", "network", "wireless"], getChildren);
|
||||
|
||||
assert.deepEqual(
|
||||
menu.buildNavigationModel(
|
||||
[{ name: "network", title: "Network" }],
|
||||
"admin",
|
||||
)[0].pages,
|
||||
[
|
||||
{
|
||||
name: "wireless",
|
||||
title: "translated:Wireless",
|
||||
href: "/admin/network/wireless",
|
||||
isActivePage: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("returns an empty navigation model for empty input", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
|
||||
assert.deepEqual(menu.buildNavigationModel([], "admin"), []);
|
||||
});
|
||||
|
||||
const createClassList = (...classes) => {
|
||||
const values = new Set(classes);
|
||||
|
||||
return {
|
||||
contains(className) {
|
||||
return values.has(className);
|
||||
},
|
||||
toggle(className, force) {
|
||||
const enabled = force ?? !values.has(className);
|
||||
|
||||
if (enabled) values.add(className);
|
||||
else values.delete(className);
|
||||
|
||||
return enabled;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createFakeElement = (...classes) => {
|
||||
const attributes = new Map();
|
||||
|
||||
const element = {
|
||||
classList: createClassList(...classes),
|
||||
parentElement: null,
|
||||
closest(selector) {
|
||||
if (!selector.startsWith(".")) return null;
|
||||
|
||||
const className = selector.slice(1);
|
||||
let current = element;
|
||||
|
||||
while (current) {
|
||||
if (current.classList?.contains(className)) return current;
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getAttribute(name) {
|
||||
return attributes.get(name) ?? null;
|
||||
},
|
||||
hasAttribute(name) {
|
||||
return attributes.has(name);
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
removeAttribute(name) {
|
||||
attributes.delete(name);
|
||||
},
|
||||
setAttribute(name, value) {
|
||||
attributes.set(name, String(value));
|
||||
},
|
||||
};
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
const createNavigationGroup = (...classes) => {
|
||||
const item = createFakeElement("navigation-group", ...classes);
|
||||
const toggle = createFakeElement("navigation-group-toggle");
|
||||
const region = createFakeElement("navigation-group-region");
|
||||
|
||||
toggle.parentElement = item;
|
||||
region.parentElement = item;
|
||||
item.querySelector = (selector) => {
|
||||
if (selector === ".navigation-group-toggle") return toggle;
|
||||
if (selector === ".navigation-group-region") return region;
|
||||
return null;
|
||||
};
|
||||
|
||||
return { item, region, toggle };
|
||||
};
|
||||
|
||||
const createNavigationSurface = (groups) => {
|
||||
const items = groups.map((group) => group.item);
|
||||
const listeners = new Map();
|
||||
|
||||
return {
|
||||
dataset: {},
|
||||
addEventListener(type, listener) {
|
||||
const typeListeners = listeners.get(type) ?? [];
|
||||
|
||||
typeListeners.push(listener);
|
||||
listeners.set(type, typeListeners);
|
||||
},
|
||||
contains(item) {
|
||||
return items.includes(item);
|
||||
},
|
||||
dispatch(type, event) {
|
||||
for (const listener of listeners.get(type) ?? []) listener(event);
|
||||
},
|
||||
listenerCount(type) {
|
||||
return listeners.get(type)?.length ?? 0;
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === ".navigation-group.is-active-group") {
|
||||
return (
|
||||
items.find((item) => item.classList.contains("is-active-group")) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === ".navigation-group.is-expanded") {
|
||||
return items.filter((item) => item.classList.contains("is-expanded"));
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test("sets one navigation group expanded while preserving active state", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const { item, region, toggle } = createNavigationGroup(
|
||||
"is-active-group",
|
||||
"custom-state",
|
||||
);
|
||||
|
||||
region.setAttribute("aria-hidden", "true");
|
||||
region.setAttribute("inert", "");
|
||||
|
||||
menu.setNavigationGroupExpanded(item, true);
|
||||
|
||||
assert.equal(item.classList.contains("is-expanded"), true);
|
||||
assert.equal(item.classList.contains("is-active-group"), true);
|
||||
assert.equal(item.classList.contains("custom-state"), true);
|
||||
assert.equal(toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(region.getAttribute("aria-hidden"), "false");
|
||||
assert.equal(region.hasAttribute("inert"), false);
|
||||
|
||||
menu.setNavigationGroupExpanded(item, false);
|
||||
|
||||
assert.equal(item.classList.contains("is-expanded"), false);
|
||||
assert.equal(item.classList.contains("is-active-group"), true);
|
||||
assert.equal(item.classList.contains("custom-state"), true);
|
||||
assert.equal(toggle.getAttribute("aria-expanded"), "false");
|
||||
assert.equal(region.getAttribute("aria-hidden"), "true");
|
||||
assert.equal(region.getAttribute("inert"), "");
|
||||
});
|
||||
|
||||
test("sets an expanded navigation group when controls or regions are missing", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const item = createFakeElement("navigation-group");
|
||||
|
||||
assert.doesNotThrow(() => menu.setNavigationGroupExpanded(item, true));
|
||||
assert.equal(item.classList.contains("is-expanded"), true);
|
||||
});
|
||||
|
||||
test("exclusively expands one navigation group", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const second = createNavigationGroup();
|
||||
const surface = createNavigationSurface([first, second]);
|
||||
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.setExclusiveNavigationGroupExpanded(surface, second.item, true);
|
||||
|
||||
assert.equal(first.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(first.toggle.getAttribute("aria-expanded"), "false");
|
||||
assert.equal(first.region.getAttribute("aria-hidden"), "true");
|
||||
assert.equal(first.region.hasAttribute("inert"), true);
|
||||
assert.equal(second.item.classList.contains("is-expanded"), true);
|
||||
assert.equal(second.toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(second.region.getAttribute("aria-hidden"), "false");
|
||||
assert.equal(second.region.hasAttribute("inert"), false);
|
||||
});
|
||||
|
||||
test("collapses only the requested navigation group", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const second = createNavigationGroup();
|
||||
const surface = createNavigationSurface([first, second]);
|
||||
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.setNavigationGroupExpanded(second.item, true);
|
||||
menu.setExclusiveNavigationGroupExpanded(surface, second.item, false);
|
||||
|
||||
assert.equal(first.item.classList.contains("is-expanded"), true);
|
||||
assert.equal(first.toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(second.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(second.toggle.getAttribute("aria-expanded"), "false");
|
||||
});
|
||||
|
||||
test("ignores missing exclusive navigation group arguments", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const group = createNavigationGroup();
|
||||
const surface = createNavigationSurface([group]);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
menu.setExclusiveNavigationGroupExpanded(null, group.item, true),
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
menu.setExclusiveNavigationGroupExpanded(surface, null, true),
|
||||
);
|
||||
assert.equal(group.item.classList.contains("is-expanded"), false);
|
||||
});
|
||||
|
||||
test("resets every expanded navigation group", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const second = createNavigationGroup();
|
||||
const collapsed = createNavigationGroup();
|
||||
const surface = createNavigationSurface([first, second, collapsed]);
|
||||
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.setNavigationGroupExpanded(second.item, true);
|
||||
menu.resetNavigationGroups(surface);
|
||||
|
||||
assert.equal(first.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(first.toggle.getAttribute("aria-expanded"), "false");
|
||||
assert.equal(first.region.hasAttribute("inert"), true);
|
||||
assert.equal(second.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(second.toggle.getAttribute("aria-expanded"), "false");
|
||||
assert.equal(second.region.hasAttribute("inert"), true);
|
||||
assert.equal(collapsed.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(collapsed.toggle.getAttribute("aria-expanded"), null);
|
||||
assert.doesNotThrow(() => menu.resetNavigationGroups(null));
|
||||
});
|
||||
|
||||
test("expands the active navigation group exclusively", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const active = createNavigationGroup("is-active-group");
|
||||
const surface = createNavigationSurface([first, active]);
|
||||
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.expandActiveNavigationGroup(surface);
|
||||
|
||||
assert.equal(first.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(active.item.classList.contains("is-expanded"), true);
|
||||
assert.equal(active.item.classList.contains("is-active-group"), true);
|
||||
assert.equal(active.toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(active.region.hasAttribute("inert"), false);
|
||||
});
|
||||
|
||||
test("ignores missing active navigation groups", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const surface = createNavigationSurface([createNavigationGroup()]);
|
||||
|
||||
assert.doesNotThrow(() => menu.expandActiveNavigationGroup(surface));
|
||||
assert.doesNotThrow(() => menu.expandActiveNavigationGroup(null));
|
||||
});
|
||||
|
||||
test("binds one delegated navigation accordion listener", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const second = createNavigationGroup();
|
||||
const surface = createNavigationSurface([first, second]);
|
||||
let prevented = 0;
|
||||
let stopped = 0;
|
||||
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.bindNavigationAccordion(surface);
|
||||
menu.bindNavigationAccordion(surface);
|
||||
|
||||
assert.equal(surface.dataset.accordionBound, "true");
|
||||
assert.equal(surface.listenerCount("click"), 1);
|
||||
|
||||
surface.dispatch("click", {
|
||||
target: second.toggle,
|
||||
preventDefault() {
|
||||
prevented += 1;
|
||||
},
|
||||
stopPropagation() {
|
||||
stopped += 1;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prevented, 1);
|
||||
assert.equal(stopped, 1);
|
||||
assert.equal(first.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(second.item.classList.contains("is-expanded"), true);
|
||||
|
||||
surface.dispatch("click", {
|
||||
target: createFakeElement("outside"),
|
||||
preventDefault() {
|
||||
prevented += 1;
|
||||
},
|
||||
stopPropagation() {
|
||||
stopped += 1;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prevented, 1);
|
||||
assert.equal(stopped, 1);
|
||||
assert.equal(second.item.classList.contains("is-expanded"), true);
|
||||
assert.doesNotThrow(() => menu.bindNavigationAccordion(null));
|
||||
});
|
||||
|
||||
test("ignores delegated clicks from targets without closest", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const group = createNavigationGroup();
|
||||
const surface = createNavigationSurface([group]);
|
||||
let prevented = 0;
|
||||
let stopped = 0;
|
||||
|
||||
menu.bindNavigationAccordion(surface);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
surface.dispatch("click", {
|
||||
target: {},
|
||||
preventDefault() {
|
||||
prevented += 1;
|
||||
},
|
||||
stopPropagation() {
|
||||
stopped += 1;
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(prevented, 0);
|
||||
assert.equal(stopped, 0);
|
||||
assert.equal(group.item.classList.contains("is-expanded"), false);
|
||||
});
|
||||
|
||||
test("handles delegated clicks from inside a navigation group toggle", () => {
|
||||
const menu = loadMenuModule(["admin"]);
|
||||
const first = createNavigationGroup();
|
||||
const second = createNavigationGroup();
|
||||
const surface = createNavigationSurface([first, second]);
|
||||
const label = createFakeElement("navigation-group-label");
|
||||
const span = createFakeElement("navigation-group-label-text");
|
||||
let prevented = 0;
|
||||
let stopped = 0;
|
||||
|
||||
label.parentElement = second.toggle;
|
||||
span.parentElement = label;
|
||||
menu.setNavigationGroupExpanded(first.item, true);
|
||||
menu.bindNavigationAccordion(surface);
|
||||
|
||||
surface.dispatch("click", {
|
||||
target: span,
|
||||
preventDefault() {
|
||||
prevented += 1;
|
||||
},
|
||||
stopPropagation() {
|
||||
stopped += 1;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prevented, 1);
|
||||
assert.equal(stopped, 1);
|
||||
assert.equal(first.item.classList.contains("is-expanded"), false);
|
||||
assert.equal(second.item.classList.contains("is-expanded"), true);
|
||||
});
|
||||
@@ -0,0 +1,723 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
const source = await readFile(
|
||||
new URL("../src/resource/menu-aurora.js", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const getMethodSource = (name) => {
|
||||
const start = source.indexOf(` ${name}(`);
|
||||
|
||||
if (start === -1) return "";
|
||||
|
||||
const rest = source.slice(start + 2);
|
||||
const nextMethod = rest.slice(name.length).search(/^ [A-Za-z_$][\w$]*\(/m);
|
||||
|
||||
return nextMethod === -1
|
||||
? source.slice(start)
|
||||
: source.slice(start, start + 2 + name.length + nextMethod);
|
||||
};
|
||||
|
||||
class FakeClassList {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.values = new Set();
|
||||
}
|
||||
|
||||
add(...classNames) {
|
||||
classNames.forEach((className) => this.values.add(className));
|
||||
this.sync();
|
||||
}
|
||||
|
||||
contains(className) {
|
||||
return this.values.has(className);
|
||||
}
|
||||
|
||||
remove(...classNames) {
|
||||
classNames.forEach((className) => this.values.delete(className));
|
||||
this.sync();
|
||||
}
|
||||
|
||||
replace(classNames) {
|
||||
this.values = new Set(classNames.split(/\s+/).filter(Boolean));
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.element.attributes.set("class", [...this.values].join(" "));
|
||||
}
|
||||
|
||||
toggle(className, force) {
|
||||
const enabled = force ?? !this.values.has(className);
|
||||
|
||||
if (enabled) this.values.add(className);
|
||||
else this.values.delete(className);
|
||||
this.sync();
|
||||
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeElement {
|
||||
constructor(tagName, attributes = {}, children = []) {
|
||||
this.tagName = tagName;
|
||||
this.attributes = new Map();
|
||||
this.children = [];
|
||||
this.classList = new FakeClassList(this);
|
||||
this.dataset = {};
|
||||
this.listeners = new Map();
|
||||
this.parentElement = null;
|
||||
this.style = {
|
||||
display: "",
|
||||
properties: new Map(),
|
||||
removeProperty: (name) => this.style.properties.delete(name),
|
||||
setProperty: (name, value) => this.style.properties.set(name, value),
|
||||
};
|
||||
|
||||
Object.entries(attributes).forEach(([name, value]) => {
|
||||
this.setAttribute(name, value);
|
||||
});
|
||||
children.forEach((child) => this.appendChild(child));
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
if (child instanceof FakeElement) child.parentElement = this;
|
||||
return child;
|
||||
}
|
||||
|
||||
addEventListener(type, listener) {
|
||||
const listeners = this.listeners.get(type) ?? [];
|
||||
|
||||
listeners.push(listener);
|
||||
this.listeners.set(type, listeners);
|
||||
}
|
||||
|
||||
closest(selector) {
|
||||
let current = this;
|
||||
|
||||
while (current) {
|
||||
if (current.matches(selector)) return current;
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
contains(element) {
|
||||
if (element === this) return true;
|
||||
|
||||
return this.children.some(
|
||||
(child) => child instanceof FakeElement && child.contains(element),
|
||||
);
|
||||
}
|
||||
|
||||
getAttribute(name) {
|
||||
return this.attributes.get(name) ?? null;
|
||||
}
|
||||
|
||||
hasAttribute(name) {
|
||||
return this.attributes.has(name);
|
||||
}
|
||||
|
||||
listenerCount(type) {
|
||||
return this.listeners.get(type)?.length ?? 0;
|
||||
}
|
||||
|
||||
matches(selector) {
|
||||
if (selector.startsWith("#")) {
|
||||
return this.getAttribute("id") === selector.slice(1);
|
||||
}
|
||||
|
||||
if (selector.startsWith(".")) {
|
||||
return selector
|
||||
.slice(1)
|
||||
.split(".")
|
||||
.every((className) => this.classList.contains(className));
|
||||
}
|
||||
|
||||
return this.tagName === selector;
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
for (const child of this.children) {
|
||||
if (!(child instanceof FakeElement)) continue;
|
||||
if (child.matches(selector)) return child;
|
||||
|
||||
const descendant = child.querySelector(selector);
|
||||
if (descendant) return descendant;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
querySelectorAll(selector) {
|
||||
const matches = [];
|
||||
|
||||
for (const child of this.children) {
|
||||
if (!(child instanceof FakeElement)) continue;
|
||||
if (child.matches(selector)) matches.push(child);
|
||||
matches.push(...child.querySelectorAll(selector));
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
removeAttribute(name) {
|
||||
this.attributes.delete(name);
|
||||
if (name === "class") this.classList.replace("");
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes.set(name, String(value));
|
||||
if (name === "class") this.classList.replace(String(value));
|
||||
if (name.startsWith("data-")) {
|
||||
const dataName = name
|
||||
.slice(5)
|
||||
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
this.dataset[dataName] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
set innerHTML(value) {
|
||||
assert.equal(value, "");
|
||||
this.children = [];
|
||||
}
|
||||
}
|
||||
|
||||
const E = (tagName, attributes, children) =>
|
||||
new FakeElement(tagName, attributes, children);
|
||||
|
||||
const getMenuChildren = (item) =>
|
||||
Object.entries(item?.children ?? {}).map(([name, child]) => ({
|
||||
...child,
|
||||
name,
|
||||
}));
|
||||
|
||||
const createFakeDocument = ({ elements = {}, navType = "mega-menu" } = {}) => ({
|
||||
body: {
|
||||
dataset: { navType },
|
||||
},
|
||||
querySelector(selector) {
|
||||
return elements[selector] ?? null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
const loadMenuModule = ({
|
||||
dispatchpath = [],
|
||||
document = createFakeDocument(),
|
||||
getChildren = getMenuChildren,
|
||||
requestpath = [],
|
||||
translate = (value) => value,
|
||||
} = {}) => {
|
||||
const baseclass = {
|
||||
extend(module) {
|
||||
return module;
|
||||
},
|
||||
};
|
||||
const ui = { menu: { getChildren } };
|
||||
const L = {
|
||||
env: { dispatchpath, requestpath },
|
||||
url: (...segments) => `/${segments.join("/")}`,
|
||||
};
|
||||
|
||||
return new Function(
|
||||
"baseclass",
|
||||
"ui",
|
||||
"E",
|
||||
"L",
|
||||
"_",
|
||||
"document",
|
||||
"window",
|
||||
"localStorage",
|
||||
source,
|
||||
)(baseclass, ui, E, L, translate, document, {}, {});
|
||||
};
|
||||
|
||||
const textContent = (element) =>
|
||||
element.children
|
||||
.map((child) =>
|
||||
child instanceof FakeElement ? textContent(child) : String(child),
|
||||
)
|
||||
.join("");
|
||||
|
||||
const directItem = (overrides = {}) => ({
|
||||
name: "status",
|
||||
title: "Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: false,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const groupItem = (overrides = {}) => ({
|
||||
name: "network",
|
||||
title: "Network",
|
||||
href: "/admin/network",
|
||||
hasChildren: true,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: false,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("defines the shared navigation renderer and emits its common contract", () => {
|
||||
const renderer = getMethodSource("renderNavigationItem");
|
||||
|
||||
assert.match(renderer, /renderNavigationItem\(item, mode\)/);
|
||||
[
|
||||
"navigation-group",
|
||||
"navigation-group-toggle",
|
||||
"navigation-group-region",
|
||||
"navigation-submenu-list",
|
||||
"navigation-sublink",
|
||||
"navigation-direct",
|
||||
"is-active-group",
|
||||
"is-expanded",
|
||||
"is-active-page",
|
||||
].forEach((className) => assert.match(renderer, new RegExp(className)));
|
||||
assert.match(renderer, /aria-current/);
|
||||
});
|
||||
|
||||
test("renders both navigation surfaces through the shared renderer and accordion", () => {
|
||||
const mobile = getMethodSource("renderMobileMenu");
|
||||
const sidebar = getMethodSource("renderSidebar");
|
||||
|
||||
assert.match(mobile, /renderNavigationItem\(item,\s*"mobile"\)/);
|
||||
assert.match(mobile, /bindNavigationAccordion\(list\)/);
|
||||
assert.match(sidebar, /renderNavigationItem\(item,\s*"sidebar"\)/);
|
||||
assert.match(sidebar, /bindNavigationAccordion\(list\)/);
|
||||
});
|
||||
|
||||
test("removes legacy navigation state helpers and class tokens", () => {
|
||||
[
|
||||
"setMobileSubmenuExpanded",
|
||||
"setSidebarSectionExpanded",
|
||||
"submenu-expanded",
|
||||
"sidebar-group-open",
|
||||
"has-active",
|
||||
].forEach((token) => assert.doesNotMatch(source, new RegExp(token)));
|
||||
});
|
||||
|
||||
test("builds the navigation model once and passes the same items to both surfaces", () => {
|
||||
const renderModeMenu = getMethodSource("renderModeMenu");
|
||||
|
||||
assert.equal(source.match(/\bbuildNavigationModel\(/g)?.length, 2);
|
||||
assert.match(
|
||||
renderModeMenu,
|
||||
/const navigationItems = this\.buildNavigationModel\(\s*ui\.menu\.getChildren\(activeChild\),\s*activeChild\.name,\s*\)/,
|
||||
);
|
||||
assert.match(
|
||||
renderModeMenu,
|
||||
/this\.renderMainMenu\(activeChild,\s*activeChild\.name,\s*0,\s*navigationItems\)/,
|
||||
);
|
||||
assert.match(renderModeMenu, /this\.renderMobileMenu\(navigationItems\)/);
|
||||
});
|
||||
|
||||
test("renders an active direct item with shared and mode-specific state", () => {
|
||||
const menu = loadMenuModule();
|
||||
const item = menu.renderNavigationItem(
|
||||
{
|
||||
name: "status",
|
||||
title: "Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isActivePage: true,
|
||||
},
|
||||
"mobile",
|
||||
);
|
||||
const anchor = item.children[0];
|
||||
|
||||
assert.equal(item.getAttribute("class"), "mobile-nav-item");
|
||||
assert.equal(
|
||||
anchor.getAttribute("class"),
|
||||
"navigation-direct mobile-nav-link is-active-page",
|
||||
);
|
||||
assert.equal(anchor.getAttribute("href"), "/admin/status");
|
||||
assert.equal(anchor.getAttribute("aria-current"), "page");
|
||||
});
|
||||
|
||||
test("renders an active group expanded with active page semantics", () => {
|
||||
const menu = loadMenuModule();
|
||||
const item = menu.renderNavigationItem(
|
||||
{
|
||||
name: "network",
|
||||
title: "Network",
|
||||
hasChildren: true,
|
||||
isActiveGroup: true,
|
||||
pages: [
|
||||
{
|
||||
title: "Wireless",
|
||||
href: "/admin/network/wireless",
|
||||
isActivePage: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
"sidebar",
|
||||
);
|
||||
const [toggle, region] = item.children;
|
||||
const list = region.children[0];
|
||||
const activeLink = list.children[0].children[0];
|
||||
|
||||
assert.equal(
|
||||
item.getAttribute("class"),
|
||||
"navigation-group sidebar-group is-active-group is-expanded",
|
||||
);
|
||||
assert.equal(toggle.getAttribute("aria-controls"), "sidebar-submenu-network");
|
||||
assert.equal(toggle.getAttribute("aria-current"), "location");
|
||||
assert.equal(toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(region.getAttribute("aria-hidden"), "false");
|
||||
assert.equal(region.hasAttribute("inert"), false);
|
||||
assert.equal(
|
||||
activeLink.getAttribute("class"),
|
||||
"navigation-sublink is-active-page",
|
||||
);
|
||||
assert.equal(activeLink.getAttribute("aria-current"), "page");
|
||||
});
|
||||
|
||||
test("renders an inactive group collapsed and inert", () => {
|
||||
const menu = loadMenuModule();
|
||||
const item = menu.renderNavigationItem(
|
||||
{
|
||||
name: "system/tools",
|
||||
title: "System",
|
||||
hasChildren: true,
|
||||
isActiveGroup: false,
|
||||
pages: [],
|
||||
},
|
||||
"mobile",
|
||||
);
|
||||
const [toggle, region] = item.children;
|
||||
|
||||
assert.equal(item.getAttribute("class"), "navigation-group mobile-nav-item");
|
||||
assert.equal(
|
||||
toggle.getAttribute("aria-controls"),
|
||||
"mobile-submenu-system%2Ftools",
|
||||
);
|
||||
assert.equal(toggle.getAttribute("aria-expanded"), "false");
|
||||
assert.equal(toggle.hasAttribute("aria-current"), false);
|
||||
assert.equal(region.getAttribute("aria-hidden"), "true");
|
||||
assert.equal(region.getAttribute("inert"), "");
|
||||
});
|
||||
|
||||
test("renders mode and mobile navigation without desktop containers", () => {
|
||||
const mobileList = new FakeElement("ul");
|
||||
const mobileFooter = new FakeElement("div");
|
||||
const document = createFakeDocument({
|
||||
elements: {
|
||||
"#mobile-nav-footer-action": mobileFooter,
|
||||
"#mobile-nav-list": mobileList,
|
||||
},
|
||||
});
|
||||
const menu = loadMenuModule({
|
||||
dispatchpath: ["admin", "status"],
|
||||
document,
|
||||
requestpath: ["admin"],
|
||||
translate: (value) => `translated:${value}`,
|
||||
});
|
||||
const tree = {
|
||||
children: {
|
||||
admin: {
|
||||
title: "Administration",
|
||||
children: {
|
||||
status: { title: "Status" },
|
||||
},
|
||||
},
|
||||
services: {
|
||||
title: "Services",
|
||||
children: {
|
||||
dns: { title: "DNS" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const originalBuildNavigationModel = menu.buildNavigationModel.bind(menu);
|
||||
const originalRenderMainMenu = menu.renderMainMenu.bind(menu);
|
||||
const originalRenderMobileMenu = menu.renderMobileMenu.bind(menu);
|
||||
const desktopCalls = [];
|
||||
const mobileCalls = [];
|
||||
let buildCount = 0;
|
||||
|
||||
menu.buildNavigationModel = (...args) => {
|
||||
buildCount += 1;
|
||||
return originalBuildNavigationModel(...args);
|
||||
};
|
||||
menu.renderMainMenu = (...args) => {
|
||||
desktopCalls.push(args);
|
||||
return originalRenderMainMenu(...args);
|
||||
};
|
||||
menu.renderMobileMenu = (...args) => {
|
||||
mobileCalls.push(args);
|
||||
return originalRenderMobileMenu(...args);
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => menu.renderModeMenu(tree));
|
||||
assert.equal(buildCount, 1);
|
||||
assert.equal(desktopCalls.length, 1);
|
||||
assert.equal(desktopCalls[0][0].name, "admin");
|
||||
assert.equal(mobileCalls.length, 1);
|
||||
assert.strictEqual(desktopCalls[0][3], mobileCalls[0][0]);
|
||||
assert.deepEqual(mobileCalls[0][0], [
|
||||
{
|
||||
name: "status",
|
||||
title: "translated:Status",
|
||||
href: "/admin/status",
|
||||
hasChildren: false,
|
||||
isLogout: false,
|
||||
isActiveGroup: false,
|
||||
isActivePage: true,
|
||||
activePage: null,
|
||||
pages: [],
|
||||
},
|
||||
]);
|
||||
assert.equal(mobileList.children.length, 1);
|
||||
});
|
||||
|
||||
test("skips mega-menu initialization when the top menu is missing", () => {
|
||||
const menu = loadMenuModule({
|
||||
document: createFakeDocument({ navType: "mega-menu" }),
|
||||
});
|
||||
const tree = {
|
||||
children: {
|
||||
status: { title: "Status" },
|
||||
},
|
||||
};
|
||||
let calls = 0;
|
||||
let result;
|
||||
|
||||
menu.initMegaMenu = () => {
|
||||
calls += 1;
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
result = menu.renderMainMenu(tree, "admin");
|
||||
});
|
||||
assert.equal(calls, 0);
|
||||
assert.ok(result instanceof FakeElement);
|
||||
});
|
||||
|
||||
test("skips boxed-dropdown initialization when the top menu is missing", () => {
|
||||
const menu = loadMenuModule({
|
||||
document: createFakeDocument({ navType: "boxed-dropdown" }),
|
||||
});
|
||||
const tree = {
|
||||
children: {
|
||||
status: { title: "Status" },
|
||||
},
|
||||
};
|
||||
let calls = 0;
|
||||
let result;
|
||||
|
||||
menu.initBoxedDropdown = () => {
|
||||
calls += 1;
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
result = menu.renderMainMenu(tree, "admin");
|
||||
});
|
||||
assert.equal(calls, 0);
|
||||
assert.ok(result instanceof FakeElement);
|
||||
});
|
||||
|
||||
test("renders sidebar items when the top menu is missing", () => {
|
||||
const menu = loadMenuModule({
|
||||
document: createFakeDocument({ navType: "sidebar" }),
|
||||
});
|
||||
const tree = {
|
||||
children: {
|
||||
status: { title: "Status" },
|
||||
},
|
||||
};
|
||||
const items = [directItem()];
|
||||
let renderedItems = null;
|
||||
let result;
|
||||
|
||||
menu.renderSidebar = (navigationItems) => {
|
||||
renderedItems = navigationItems;
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
result = menu.renderMainMenu(tree, "admin", 0, items);
|
||||
});
|
||||
assert.strictEqual(renderedItems, items);
|
||||
assert.ok(result instanceof FakeElement);
|
||||
});
|
||||
|
||||
test("generates stable collision-safe submenu IDs", () => {
|
||||
const menu = loadMenuModule();
|
||||
|
||||
assert.equal(
|
||||
menu.navigationSubmenuId("mobile", "system/tools"),
|
||||
"mobile-submenu-system%2Ftools",
|
||||
);
|
||||
assert.equal(
|
||||
menu.navigationSubmenuId("mobile", "system-tools"),
|
||||
"mobile-submenu-system-tools",
|
||||
);
|
||||
assert.equal(
|
||||
menu.navigationSubmenuId("mobile", "system.tools"),
|
||||
"mobile-submenu-system.tools",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders unique submenu controls for punctuation-distinct group names", () => {
|
||||
const menu = loadMenuModule();
|
||||
const names = ["system/tools", "system-tools", "system.tools"];
|
||||
const rendered = names.map((name) =>
|
||||
menu.renderNavigationItem(groupItem({ name }), "mobile"),
|
||||
);
|
||||
const controlIds = rendered.map((item) =>
|
||||
item.children[0].getAttribute("aria-controls"),
|
||||
);
|
||||
const regionIds = rendered.map((item) => item.children[1].getAttribute("id"));
|
||||
|
||||
assert.deepEqual(controlIds, [
|
||||
"mobile-submenu-system%2Ftools",
|
||||
"mobile-submenu-system-tools",
|
||||
"mobile-submenu-system.tools",
|
||||
]);
|
||||
assert.deepEqual(regionIds, controlIds);
|
||||
assert.equal(new Set(controlIds).size, names.length);
|
||||
});
|
||||
|
||||
test("renders mobile items and logout repeatedly without duplicate listeners", () => {
|
||||
const list = new FakeElement("ul", {}, [new FakeElement("li")]);
|
||||
const footer = new FakeElement("div", {}, [new FakeElement("a")]);
|
||||
const document = createFakeDocument({
|
||||
elements: {
|
||||
"#mobile-nav-footer-action": footer,
|
||||
"#mobile-nav-list": list,
|
||||
},
|
||||
});
|
||||
const menu = loadMenuModule({ document });
|
||||
const items = [
|
||||
directItem(),
|
||||
groupItem(),
|
||||
directItem({
|
||||
name: "logout",
|
||||
title: "Logout",
|
||||
href: "/admin/logout",
|
||||
isLogout: true,
|
||||
}),
|
||||
];
|
||||
|
||||
menu.renderMobileMenu(items);
|
||||
menu.renderMobileMenu(items);
|
||||
|
||||
assert.equal(list.children.length, 2);
|
||||
assert.equal(
|
||||
list.children[0].children[0].getAttribute("class"),
|
||||
"navigation-direct mobile-nav-link",
|
||||
);
|
||||
assert.equal(
|
||||
list.children[1].getAttribute("class"),
|
||||
"navigation-group mobile-nav-item",
|
||||
);
|
||||
assert.equal(footer.children.length, 1);
|
||||
assert.equal(footer.children[0].getAttribute("class"), "mobile-nav-logout");
|
||||
assert.equal(footer.children[0].getAttribute("href"), "/admin/logout");
|
||||
assert.equal(textContent(footer.children[0]), "Logout");
|
||||
assert.equal(list.dataset.accordionBound, "true");
|
||||
assert.equal(list.listenerCount("click"), 1);
|
||||
});
|
||||
|
||||
test("renders sidebar items, logout, and translated crumbs without duplication", () => {
|
||||
const list = new FakeElement("ul", {}, [new FakeElement("li")]);
|
||||
const footer = new FakeElement("div", {}, [new FakeElement("a")]);
|
||||
const crumb = new FakeElement("ol", {}, [new FakeElement("li")]);
|
||||
const document = createFakeDocument({
|
||||
elements: {
|
||||
"#header-crumb": crumb,
|
||||
"#sidebar-footer": footer,
|
||||
"#sidebar-list": list,
|
||||
},
|
||||
navType: "sidebar",
|
||||
});
|
||||
const menu = loadMenuModule({
|
||||
dispatchpath: ["admin", "network", "wireless"],
|
||||
document,
|
||||
translate: (value) => `translated:${value}`,
|
||||
});
|
||||
const tree = {
|
||||
children: {
|
||||
network: {
|
||||
title: "Network",
|
||||
children: {
|
||||
wireless: { title: "Wireless" },
|
||||
},
|
||||
},
|
||||
status: { title: "Status" },
|
||||
logout: { title: "Logout" },
|
||||
},
|
||||
};
|
||||
const items = menu.buildNavigationModel(getMenuChildren(tree), "admin");
|
||||
|
||||
menu.renderSidebar(items);
|
||||
menu.renderSidebar(items);
|
||||
|
||||
assert.equal(list.children.length, 2);
|
||||
assert.equal(
|
||||
list.children[0].getAttribute("class"),
|
||||
"navigation-group sidebar-group is-active-group is-expanded",
|
||||
);
|
||||
assert.equal(
|
||||
list.children[1].children[0].getAttribute("class"),
|
||||
"navigation-direct nav-link",
|
||||
);
|
||||
assert.equal(footer.children.length, 1);
|
||||
assert.equal(footer.children[0].getAttribute("class"), "nav-link");
|
||||
assert.equal(footer.children[0].getAttribute("href"), "/admin/logout");
|
||||
assert.equal(textContent(footer.children[0]), "translated:Logout");
|
||||
assert.equal(crumb.children.length, 3);
|
||||
assert.deepEqual(
|
||||
crumb.children.map((child) => textContent(child)),
|
||||
["translated:Network", "/", "translated:Wireless"],
|
||||
);
|
||||
assert.equal(crumb.children[2].getAttribute("class"), "current");
|
||||
assert.equal(list.dataset.accordionBound, "true");
|
||||
assert.equal(list.listenerCount("click"), 1);
|
||||
});
|
||||
|
||||
test("renders an active group expanded when an open mobile list was initially empty", () => {
|
||||
const list = new FakeElement("ul");
|
||||
const overlay = new FakeElement("div", { class: "mobile-menu-open" }, [list]);
|
||||
const document = createFakeDocument({
|
||||
elements: {
|
||||
"#mobile-menu-overlay": overlay,
|
||||
"#mobile-nav-list": list,
|
||||
},
|
||||
});
|
||||
const menu = loadMenuModule({ document });
|
||||
|
||||
menu.renderMobileMenu([
|
||||
groupItem({
|
||||
isActiveGroup: true,
|
||||
pages: [
|
||||
{
|
||||
title: "Wireless",
|
||||
href: "/admin/network/wireless",
|
||||
isActivePage: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const item = list.children[0];
|
||||
const [toggle, region] = item.children;
|
||||
|
||||
assert.equal(overlay.classList.contains("mobile-menu-open"), true);
|
||||
assert.equal(item.classList.contains("is-expanded"), true);
|
||||
assert.equal(toggle.getAttribute("aria-expanded"), "true");
|
||||
assert.equal(region.getAttribute("aria-hidden"), "false");
|
||||
assert.equal(region.hasAttribute("inert"), false);
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
const readStylesheet = (path) =>
|
||||
readFile(new URL(`../src/media/${path}`, import.meta.url), "utf8");
|
||||
|
||||
const [navigationStyles, layoutStyles, overlayStyles] = await Promise.all([
|
||||
readStylesheet("components/_nav.css"),
|
||||
readStylesheet("_layout.css"),
|
||||
readStylesheet("components/_overlay.css"),
|
||||
]);
|
||||
|
||||
const getBlock = (source, selector) => {
|
||||
const selectorIndex = source.indexOf(selector);
|
||||
|
||||
assert.notEqual(selectorIndex, -1, `Missing selector: ${selector}`);
|
||||
|
||||
const blockStart = source.indexOf("{", selectorIndex);
|
||||
|
||||
assert.notEqual(blockStart, -1, `Missing block for selector: ${selector}`);
|
||||
|
||||
let depth = 1;
|
||||
|
||||
for (let index = blockStart + 1; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
if (source[index] === "}") depth -= 1;
|
||||
|
||||
if (depth === 0) return source.slice(blockStart + 1, index);
|
||||
}
|
||||
|
||||
assert.fail(`Unclosed block for selector: ${selector}`);
|
||||
};
|
||||
|
||||
const assertIncludesUtilities = (block, utilities) => {
|
||||
for (const utility of utilities) {
|
||||
assert.match(
|
||||
block,
|
||||
new RegExp(
|
||||
`(^|\\s)${utility.replaceAll("[", "\\[").replaceAll("]", "\\]")}($|\\s|;)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
test("shared navigation styles define active and expanded states", () => {
|
||||
const direct = getBlock(navigationStyles, ".navigation-direct");
|
||||
const directActive = getBlock(
|
||||
navigationStyles,
|
||||
".navigation-direct.is-active-page",
|
||||
);
|
||||
const expandedToggle = getBlock(
|
||||
navigationStyles,
|
||||
".navigation-group.is-expanded > .navigation-group-toggle",
|
||||
);
|
||||
const activeGroupToggle = getBlock(
|
||||
navigationStyles,
|
||||
".navigation-group.is-active-group > .navigation-group-toggle",
|
||||
);
|
||||
const activeSublink = getBlock(
|
||||
navigationStyles,
|
||||
".navigation-sublink.is-active-page",
|
||||
);
|
||||
const sublink = getBlock(navigationStyles, ".navigation-sublink");
|
||||
|
||||
assertIncludesUtilities(direct, ["text-text", "hover:text-text"]);
|
||||
// Active page is a filled brand pill on both direct links and sublinks.
|
||||
assertIncludesUtilities(directActive, [
|
||||
"text-brand",
|
||||
"hover:text-brand",
|
||||
"font-medium",
|
||||
"bg-brand-subtle",
|
||||
]);
|
||||
// An expanded group's label turns brand and rotates its arrow open.
|
||||
assertIncludesUtilities(expandedToggle, ["after:rotate-90", "text-brand"]);
|
||||
// The active group keeps a brand label even when manually collapsed, so the
|
||||
// current section stays marked while its pill is hidden — but it must not
|
||||
// rotate the arrow open in that collapsed state.
|
||||
assertIncludesUtilities(activeGroupToggle, ["text-brand"]);
|
||||
assert.doesNotMatch(activeGroupToggle, /after:rotate-90/);
|
||||
// The pill shape lives with the pill fill in the shared recipe, so the
|
||||
// hover/active background is rounded the same way on desktop and mobile.
|
||||
assertIncludesUtilities(sublink, [
|
||||
"font-medium",
|
||||
"hover:bg-hover-faint",
|
||||
"rounded-lg",
|
||||
]);
|
||||
assertIncludesUtilities(activeSublink, [
|
||||
"text-brand",
|
||||
"hover:text-brand",
|
||||
"font-semibold",
|
||||
"bg-brand-subtle",
|
||||
]);
|
||||
// The left accent bar is gone — no before:* rail on the active sublink.
|
||||
assert.doesNotMatch(activeSublink, /before:/);
|
||||
});
|
||||
|
||||
test("shared navigation styles own accordion animation without a guide rail", () => {
|
||||
const toggle = getBlock(navigationStyles, ".navigation-group-toggle");
|
||||
const region = getBlock(navigationStyles, ".navigation-group-region");
|
||||
const expandedRegion = getBlock(
|
||||
navigationStyles,
|
||||
".navigation-group.is-expanded > .navigation-group-region",
|
||||
);
|
||||
const submenu = getBlock(navigationStyles, ".navigation-submenu-list");
|
||||
|
||||
assertIncludesUtilities(toggle, [
|
||||
"after:transition-[transform,opacity]",
|
||||
"after:duration-[250ms]",
|
||||
]);
|
||||
assert.match(toggle, /arrow-right\.svg/);
|
||||
assertIncludesUtilities(region, [
|
||||
"grid",
|
||||
"grid-rows-[0fr]",
|
||||
"opacity-0",
|
||||
"transition-[grid-template-rows,opacity]",
|
||||
"duration-[250ms]",
|
||||
]);
|
||||
assertIncludesUtilities(expandedRegion, ["grid-rows-[1fr]", "opacity-100"]);
|
||||
// The vertical guide rail is removed: the submenu list carries no before:*
|
||||
// hairline anymore.
|
||||
assert.doesNotMatch(submenu, /before:bg-hairline/);
|
||||
});
|
||||
|
||||
test("desktop sidebar styles only provide desktop navigation density", () => {
|
||||
const sidebar = getBlock(layoutStyles, 'body[data-nav-type="sidebar"]');
|
||||
const direct = getBlock(sidebar, "& .sidebar-list .navigation-direct");
|
||||
const submenu = getBlock(sidebar, "& .sidebar-submenu");
|
||||
const sublink = getBlock(sidebar, "& .sidebar-submenu .navigation-sublink");
|
||||
|
||||
assertIncludesUtilities(direct, ["truncate", "text-lg"]);
|
||||
assertIncludesUtilities(submenu, ["pl-4"]);
|
||||
assertIncludesUtilities(sublink, ["px-3", "py-1.5", "text-sm"]);
|
||||
assert.doesNotMatch(
|
||||
sidebar,
|
||||
/sidebar-section|sidebar-group-open|nav-link-active|has-active/,
|
||||
);
|
||||
assert.doesNotMatch(sidebar, /bg-brand-subtle/);
|
||||
});
|
||||
|
||||
test("mobile drawer styles only provide mobile navigation density", () => {
|
||||
const drawer = getBlock(overlayStyles, ".mobile-menu-overlay");
|
||||
const submenu = getBlock(drawer, "& .mobile-nav-submenu-list");
|
||||
const sublink = getBlock(drawer, "& .mobile-nav-sublink");
|
||||
|
||||
assertIncludesUtilities(submenu, ["max-md:pl-4"]);
|
||||
assertIncludesUtilities(sublink, [
|
||||
"max-md:min-h-10",
|
||||
"max-md:px-3",
|
||||
"max-md:py-2",
|
||||
"max-md:text-base",
|
||||
]);
|
||||
assert.doesNotMatch(sublink, /max-md:font-(?:normal|medium|semibold|bold)/);
|
||||
assert.doesNotMatch(
|
||||
drawer,
|
||||
/has-submenu|submenu-expanded|nav-link-active|has-active/,
|
||||
);
|
||||
assert.doesNotMatch(drawer, /bg-brand-subtle/);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const overlayCss = await readFile(
|
||||
new URL("../src/media/components/_overlay.css", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const layoutCss = await readFile(
|
||||
new URL("../src/media/_layout.css", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("mobile and mega navigation share the translucent menu background", () => {
|
||||
const overlayRule = overlayCss.match(
|
||||
/\.mobile-menu-overlay\s*\{([\s\S]*?)\n\s*\.mobile-nav\s*\{/,
|
||||
)?.[1];
|
||||
const navRule = overlayCss.match(
|
||||
/\.mobile-nav\s*\{\s*@apply\s+([^;]+);/,
|
||||
)?.[1];
|
||||
|
||||
assert.ok(overlayRule?.includes("max-md:bg-mega-menu-bg"));
|
||||
assert.ok(!overlayRule?.includes("max-md:bg-scrim"));
|
||||
assert.ok(!overlayRule?.includes("max-md:p-1"));
|
||||
assert.ok(!navRule?.includes("max-md:bg-"));
|
||||
assert.match(layoutCss, /\bbg-mega-menu-bg\b/);
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveMode } from "../tokens/resolve.js";
|
||||
|
||||
test("resolveMode(light) returns flat oklch for all tokens", () => {
|
||||
const out = resolveMode("light");
|
||||
// 10 inputs + 19 derived = 29 color tokens.
|
||||
for (const k of [
|
||||
"bg",
|
||||
"surface",
|
||||
"text",
|
||||
"brand",
|
||||
"text_muted",
|
||||
"surface_sunken",
|
||||
"hairline",
|
||||
"brand_subtle",
|
||||
"focus_ring",
|
||||
"progress_start",
|
||||
"progress_end",
|
||||
"info_surface",
|
||||
"scrim",
|
||||
"mega_menu_bg",
|
||||
]) {
|
||||
assert.ok(out[k], `missing ${k}`);
|
||||
assert.ok(
|
||||
!out[k].includes("color-mix") && !out[k].includes("var("),
|
||||
`${k} not flat: ${out[k]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("progress_end aliases brand", () => {
|
||||
const out = resolveMode("light");
|
||||
assert.equal(out.progress_end, out.brand);
|
||||
});
|
||||
|
||||
test("scrim uses one 60% alpha value in both modes", () => {
|
||||
assert.equal(resolveMode("light").scrim, "oklch(0% 0 0 / 0.6)");
|
||||
assert.equal(resolveMode("dark").scrim, "oklch(0% 0 0 / 0.6)");
|
||||
});
|
||||
|
||||
test("menu backgrounds are translucent Gray glass (softer than 0.8)", () => {
|
||||
const l = resolveMode("light");
|
||||
const d = resolveMode("dark");
|
||||
assert.ok(l.mega_menu_bg.includes("/ 0.66"), `light: ${l.mega_menu_bg}`);
|
||||
assert.ok(d.mega_menu_bg.includes("/ 0.62"), `dark: ${d.mega_menu_bg}`);
|
||||
});
|
||||
|
||||
test("light bg is the Gray canvas; surface stays pure white", () => {
|
||||
const light = resolveMode("light");
|
||||
assert.equal(light.bg, "oklch(0.967 0.003 264)");
|
||||
assert.equal(light.surface, "oklch(1 0 0)");
|
||||
});
|
||||
|
||||
test("dark brand and progress colors match the mobile navigation design", () => {
|
||||
const dark = resolveMode("dark");
|
||||
|
||||
assert.equal(dark.brand, "oklch(0.6 0.13 188.745)");
|
||||
assert.equal(dark.on_brand, "oklch(1 0 0)");
|
||||
assert.equal(dark.progress_start, "oklch(43.18% 0.0865 166.9)");
|
||||
assert.equal(dark.progress_end, "oklch(62.1% 0.145 189.6)");
|
||||
});
|
||||
|
||||
test("dark differs from light", () => {
|
||||
assert.notEqual(
|
||||
resolveMode("light").surface_sunken,
|
||||
resolveMode("dark").surface_sunken,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import Color from "colorjs.io";
|
||||
import { resolveMode } from "../tokens/resolve.js";
|
||||
|
||||
// [L, C, H] in oklch; alpha separate.
|
||||
const lch = (s) => {
|
||||
const c = new Color(s).to("oklch");
|
||||
return { L: c.coords[0], C: c.coords[1], H: c.coords[2] || 0, a: c.alpha };
|
||||
};
|
||||
const nearHue = (s, h, tol = 8) => {
|
||||
const H = lch(s).H;
|
||||
const d = Math.abs(((H - h + 540) % 360) - 180);
|
||||
return d <= tol;
|
||||
};
|
||||
const read = (p) => readFileSync(new URL(p, import.meta.url), "utf8");
|
||||
|
||||
test("light low-emphasis surfaces carry the Gray tint (chroma > 0)", () => {
|
||||
const l = resolveMode("light");
|
||||
assert.ok(lch(l.surface_sunken).C > 0.001, `sunken flat: ${l.surface_sunken}`);
|
||||
assert.ok(lch(l.surface_overlay).C > 0.001, `overlay flat: ${l.surface_overlay}`);
|
||||
});
|
||||
|
||||
test("light bg, sunken and overlay share one hue family (~264)", () => {
|
||||
const l = resolveMode("light");
|
||||
for (const k of ["bg", "surface_sunken", "surface_overlay"]) {
|
||||
assert.ok(nearHue(l[k], 264), `${k} hue off: ${l[k]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("light sunken is darker than bg, overlay is lighter (toward white)", () => {
|
||||
const l = resolveMode("light");
|
||||
assert.ok(lch(l.surface_sunken).L < lch(l.bg).L, "sunken not below bg");
|
||||
assert.ok(lch(l.surface_overlay).L > lch(l.bg).L, "overlay not above bg");
|
||||
});
|
||||
|
||||
test("dark surfaces are the Gray family (~264), not zinc (286)", () => {
|
||||
const d = resolveMode("dark");
|
||||
for (const k of ["bg", "surface", "surface_sunken", "text_muted"]) {
|
||||
assert.ok(nearHue(d[k], 264, 10), `${k} hue off: ${d[k]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("dark layering: page < card, sunken between page and card", () => {
|
||||
const d = resolveMode("dark");
|
||||
const bg = lch(d.bg).L, surf = lch(d.surface).L, sunk = lch(d.surface_sunken).L;
|
||||
assert.ok(bg < surf, "page not darker than card");
|
||||
assert.ok(sunk >= bg && sunk < surf, `sunken not between page and card: ${d.surface_sunken}`);
|
||||
});
|
||||
|
||||
test("mega-menu panel and overlays use stronger blur", () => {
|
||||
const layout = read("../src/media/_layout.css");
|
||||
const overlay = read("../src/media/components/_overlay.css");
|
||||
// The mega-menu panel line carries the menu background + blur.
|
||||
const panel = layout.split("\n").find((l) => l.includes("bg-mega-menu-bg"));
|
||||
assert.ok(panel?.includes("backdrop-blur-lg"), `panel blur: ${panel}`);
|
||||
assert.ok(overlay.includes("max-md:backdrop-blur-lg"), "mobile overlay blur");
|
||||
assert.ok(
|
||||
/&\.active\s*\{\s*@apply[^;]*backdrop-blur-lg/.test(overlay),
|
||||
"desktop overlay blur",
|
||||
);
|
||||
});
|
||||
|
||||
test("maincontent cards use a hairline border, not heavy shadow", () => {
|
||||
const card = read("../src/media/components/_card.css");
|
||||
const rule = card.match(/#maincontent &\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
assert.ok(rule.includes("border-hairline"), `no hairline: ${rule}`);
|
||||
assert.ok(rule.includes("border"), `no border: ${rule}`);
|
||||
assert.ok(!rule.includes("shadow-lg"), `still shadow-lg: ${rule}`);
|
||||
});
|
||||
|
||||
test("tables get a hairline frame + sunken header, body stays unclipped", () => {
|
||||
const t = read("../src/media/components/_table.css");
|
||||
const root = t.match(/table\.table,\s*\.table\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
assert.ok(root.includes("border-hairline") && root.includes("border"), `no frame: ${root}`);
|
||||
assert.ok(root.includes("rounded-2xl"), `no radius: ${root}`);
|
||||
// Dropdowns inside cells must not be clipped: no global overflow-hidden on the table.
|
||||
assert.ok(!root.includes("overflow-hidden"), `body clipped: ${root}`);
|
||||
assert.ok(root.includes("overflow-visible"), `not overflow-visible: ${root}`);
|
||||
// Header carries the sunken surface.
|
||||
const th = t.match(/&\s*th,\s*&\s*\.th\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
assert.ok(th.includes("bg-surface-sunken"), `header not sunken: ${th}`);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// 10 editable input defaults for light/dark, sourced from v2 (commit 3c4c67a).
|
||||
export const DEFAULTS = {
|
||||
light: {
|
||||
bg: "oklch(0.967 0.003 264)",
|
||||
surface: "oklch(1 0 0)",
|
||||
text: "oklch(0.21 0.02 264)",
|
||||
brand: "oklch(0.68 0.11 233)",
|
||||
on_brand: "oklch(1 0 0)",
|
||||
link: "oklch(0.74 0.238 322.16)",
|
||||
info: "oklch(0.43 0.2 255)",
|
||||
warning: "oklch(0.35 0.08 60)",
|
||||
success: "oklch(0.32 0.09 165)",
|
||||
danger: "oklch(0.35 0.12 25)",
|
||||
},
|
||||
dark: {
|
||||
bg: "oklch(0.13 0.018 264)",
|
||||
surface: "oklch(0.21 0.02 264)",
|
||||
text: "oklch(0.985 0.002 264)",
|
||||
brand: "oklch(0.6 0.13 188.745)",
|
||||
on_brand: "oklch(1 0 0)",
|
||||
link: "oklch(0.77 0.14 168)",
|
||||
info: "oklch(0.8 0.11 255)",
|
||||
warning: "oklch(0.82 0.13 80)",
|
||||
success: "oklch(0.72 0.13 158)",
|
||||
danger: "oklch(0.7 0.16 22)",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import Color from "colorjs.io";
|
||||
|
||||
const C = (v) => (v instanceof Color ? v : new Color(v));
|
||||
|
||||
// color-mix(in oklab, a p%, b) => position toward b is (1 - p)
|
||||
export const mix = (a, b, p) =>
|
||||
Color.mix(C(a), C(b), 1 - p, { space: "oklab", outputSpace: "oklch" });
|
||||
|
||||
export const shade = (a, dl) => {
|
||||
const c = C(a).to("oklch");
|
||||
c.coords[0] += dl;
|
||||
return c;
|
||||
};
|
||||
|
||||
export const set = (a, L, Ch) => {
|
||||
const c = C(a).to("oklch");
|
||||
c.coords[0] = L;
|
||||
c.coords[1] = Ch;
|
||||
return c;
|
||||
};
|
||||
|
||||
export const alpha = (a, p) => {
|
||||
const c = C(a).to("oklch");
|
||||
c.alpha = p;
|
||||
return c;
|
||||
};
|
||||
|
||||
export const konst = (s) => C(s).to("oklch");
|
||||
|
||||
// Serialize a Color (or string) to an oklch() literal -- no color-mix / var().
|
||||
export const toOklch = (v) =>
|
||||
C(v).to("oklch").toString({ precision: 4, format: "oklch" });
|
||||
@@ -0,0 +1,51 @@
|
||||
import { DERIVATIONS } from "./spec.js";
|
||||
import { DEFAULTS } from "./defaults.js";
|
||||
import { mix, shade, set, alpha, konst, toOklch } from "./engine.js";
|
||||
|
||||
// inputs: {name: oklchString}. Returns flat {name: oklchString} values.
|
||||
export function resolveTokens(mode, inputs) {
|
||||
const derivs = DERIVATIONS[mode];
|
||||
const resolved = { ...inputs };
|
||||
|
||||
const ref = (name) => {
|
||||
if (resolved[name] === undefined) compute(name);
|
||||
return resolved[name];
|
||||
};
|
||||
|
||||
function compute(name) {
|
||||
const rule = derivs[name];
|
||||
if (!rule) throw new Error(`unknown derived token: ${name}`);
|
||||
const [op, ...args] = rule;
|
||||
let color;
|
||||
switch (op) {
|
||||
case "mix":
|
||||
color = mix(ref(args[0]), ref(args[1]), args[2]);
|
||||
break;
|
||||
case "shade":
|
||||
color = shade(ref(args[0]), args[1]);
|
||||
break;
|
||||
case "set":
|
||||
color = set(ref(args[0]), args[1], args[2]);
|
||||
break;
|
||||
case "alpha":
|
||||
color = alpha(ref(args[0]), args[1]);
|
||||
break;
|
||||
case "const":
|
||||
if (args[0].startsWith("var:")) {
|
||||
resolved[name] = ref(args[0].slice(4));
|
||||
return;
|
||||
}
|
||||
color = konst(args[0]);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown op: ${op}`);
|
||||
}
|
||||
resolved[name] = toOklch(color);
|
||||
}
|
||||
|
||||
for (const name of Object.keys(derivs)) compute(name);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export const resolveMode = (mode) =>
|
||||
resolveTokens(mode, { ...DEFAULTS[mode] });
|
||||
@@ -0,0 +1,62 @@
|
||||
// Operators: ['mix',a,b,p] ['shade',a,dl] ['set',a,L,C]
|
||||
// ['alpha',a,p] ['const',str]
|
||||
export const DERIVATIONS = {
|
||||
light: {
|
||||
text_muted: ["mix", "text", "bg", 0.62],
|
||||
text_subtle: ["mix", "text", "bg", 0.48],
|
||||
surface_sunken: ["shade", "bg", -0.010],
|
||||
surface_overlay: ["shade", "bg", 0.016],
|
||||
hairline: ["alpha", "text", 0.08],
|
||||
hover_faint: ["shade", "bg", -0.04],
|
||||
brand_hover: ["shade", "brand", -0.06],
|
||||
brand_subtle: ["mix", "brand", "bg", 0.12],
|
||||
brand_subtle_hover: ["shade", "brand_subtle", -0.04],
|
||||
focus_ring: ["alpha", "brand", 0.6],
|
||||
progress_start: ["mix", "brand", "surface_sunken", 0.65],
|
||||
progress_end: ["const", "var:brand"],
|
||||
info_surface: ["set", "info", 0.94, 0.05],
|
||||
warning_surface: ["set", "warning", 0.95, 0.05],
|
||||
success_surface: ["set", "success", 0.94, 0.05],
|
||||
danger_surface: ["set", "danger", 0.94, 0.05],
|
||||
danger_surface_hover: ["shade", "danger_surface", -0.04],
|
||||
scrim: ["const", "oklch(0 0 0 / 0.6)"],
|
||||
mega_menu_bg: ["alpha", "surface_overlay", 0.66],
|
||||
},
|
||||
dark: {
|
||||
text_muted: ["mix", "text", "bg", 0.62],
|
||||
text_subtle: ["mix", "text", "bg", 0.42],
|
||||
surface_sunken: ["shade", "surface", -0.045],
|
||||
surface_overlay: ["shade", "surface", 0.02],
|
||||
hairline: ["alpha", "text", 0.1],
|
||||
hover_faint: ["alpha", "text", 0.05],
|
||||
brand_hover: ["shade", "brand", -0.05],
|
||||
brand_subtle: ["mix", "brand", "bg", 0.16],
|
||||
brand_subtle_hover: ["shade", "brand_subtle", 0.04],
|
||||
focus_ring: ["alpha", "brand", 0.6],
|
||||
progress_start: ["const", "oklch(0.4318 0.0865 166.91)"],
|
||||
progress_end: ["const", "oklch(0.621 0.145 189.632)"],
|
||||
info_surface: ["set", "info", 0.32, 0.05],
|
||||
warning_surface: ["set", "warning", 0.33, 0.06],
|
||||
success_surface: ["set", "success", 0.3, 0.05],
|
||||
danger_surface: ["set", "danger", 0.32, 0.08],
|
||||
danger_surface_hover: ["shade", "danger_surface", 0.04],
|
||||
scrim: ["const", "oklch(0 0 0 / 0.6)"],
|
||||
mega_menu_bg: ["alpha", "surface_overlay", 0.62],
|
||||
},
|
||||
};
|
||||
|
||||
// Fixed mode-specific literals, emitted verbatim rather than derived.
|
||||
export const FIXED = {
|
||||
light: {
|
||||
app_shadow_sm:
|
||||
"0 1px 3px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.04)",
|
||||
app_shadow_md:
|
||||
"0 4px 16px oklch(0 0 0 / 0.08), 0 1px 3px oklch(0 0 0 / 0.04)",
|
||||
app_shadow_lg: "0 12px 32px oklch(0 0 0 / 0.12)",
|
||||
},
|
||||
dark: {
|
||||
app_shadow_sm: "0 4px 12px oklch(0 0 0 / 0.3)",
|
||||
app_shadow_md: "0 10px 28px oklch(0 0 0 / 0.42)",
|
||||
app_shadow_lg: "0 20px 48px oklch(0 0 0 / 0.55)",
|
||||
},
|
||||
};
|
||||
@@ -4,9 +4,11 @@
|
||||
*/
|
||||
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import browserslist from "browserslist";
|
||||
import { exec } from "child_process";
|
||||
import { watch as fsWatch } from "fs";
|
||||
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
||||
import { browserslistToTargets } from "lightningcss";
|
||||
import { basename, dirname, join, relative, resolve } from "path";
|
||||
import { minify as terserMinify } from "terser";
|
||||
import { promisify } from "util";
|
||||
@@ -382,6 +384,11 @@ export default defineConfig(({ mode }) => {
|
||||
],
|
||||
|
||||
css: {
|
||||
lightningcss: {
|
||||
targets: browserslistToTargets(
|
||||
browserslist("last 4 versions, Firefox ESR, not dead"),
|
||||
),
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
{
|
||||
|
||||
@@ -8,12 +8,14 @@ All dev commands run from `.dev/`:
|
||||
|
||||
```bash
|
||||
cd .dev/
|
||||
pnpm dev # Start Vite dev server (proxies to OpenWrt device)
|
||||
pnpm build # Clean + build production assets to htdocs/luci-static/
|
||||
pnpm clean # Remove build output only
|
||||
pnpm dev # Start Vite dev server (proxies to OpenWrt device)
|
||||
pnpm build # Clean + regenerate tokens + build production assets to htdocs/luci-static/
|
||||
pnpm gen:tokens # Regenerate src/media/_tokens.css from tokens/ (also runs as part of build)
|
||||
pnpm test # Run tests/*.test.js (node:test)
|
||||
pnpm clean # Remove build output only
|
||||
```
|
||||
|
||||
No test suite or linter CLI. Formatting uses Prettier with format-on-save (`.vscode/settings.json`).
|
||||
No linter CLI. Formatting uses Prettier with format-on-save (`.vscode/settings.json`).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -24,7 +26,9 @@ No test suite or linter CLI. Formatting uses Prettier with format-on-save (`.vsc
|
||||
- `.dev/public/aurora/` → `htdocs/luci-static/aurora/` (copied as-is)
|
||||
- `ucode/template/themes/aurora/*.ut` — server-side templates, not processed by Vite
|
||||
|
||||
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/` — `main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_layout.css`, `components/*.css`, `_utilities.css`, `_patches.css`) and `login.css` (standalone login page). All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. `@layer` at-rules stripped by PostCSS plugin for OpenWrt compatibility. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
|
||||
**CSS**: two Tailwind CSS v4 entry points in `.dev/src/media/` — `main.css` (admin UI; a pure import manifest over `_tokens.css`, `_base.css`, `_elements.css`, `_layout.css`, `components/*.css`, `_utilities.css`, `_patches.css`) and `login.css` (standalone login page; imports `_base.css`). All styling MUST use TailwindCSS v4 utility classes via `@apply` — no raw CSS properties (e.g. write `@apply text-sm font-semibold;` not `font-size: 14px; font-weight: 600;`). Use [CSS Nesting syntax](https://drafts.csswg.org/css-nesting/) for selectors. Theme colors defined as OKLCH custom properties in `_tokens.css` and mapped via `@theme inline`. `@layer` at-rules stripped by PostCSS plugin for OpenWrt compatibility. See `.dev/docs/DEVELOPMENT.md` for the full CSS file layout.
|
||||
|
||||
**Design tokens**: `_tokens.css` is GENERATED — do not hand-edit (see its header comment). Source of truth is `.dev/tokens/`: `defaults.js` holds the 10 editable input colors per mode (`bg`, `surface`, `text`, `brand`, `on_brand`, `link`, `info`, `warning`, `success`, `danger`); `spec.js` declares how every other token derives from those via `mix`/`shade`/`set`/`alpha`/`const` operators plus `FIXED` literals (shadows); `engine.js`/`resolve.js` do the OKLCH math and flatten everything to plain `oklch(...)` values. After changing inputs or derivations, run `pnpm gen:tokens` to rewrite `_tokens.css`, and `pnpm test` to check the color-math and token invariants.
|
||||
|
||||
**JavaScript**: LuCI `E()` DOM API (not React/Vue). Minified but not bundled.
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=0.12.8
|
||||
PKG_RELEASE:=26
|
||||
PKG_VERSION:=0.12.10
|
||||
PKG_RELEASE:=27
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
||||
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:"";--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base)*2);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkitLine truncated
|
||||
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-tracking:initial}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--animate-spin:spin 1s linear infinite;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:wheLine truncated
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
||||
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-2xl:calc(var(--radius-base)*2);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{Line truncated
|
||||
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-tracking:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base)*.25);--radius-md:calc(var(--radius-base)*.75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base)*1.5);--radius-2xl:calc(var(--radius-base)*2);--radius-3xl:calc(var(--radius-base)*3);--radius-4xl:calc(var(--radius-base)*4);--shadow-sm:var(--app-shadow-sm);--shadow-md:var(--app-shadow-md);--shadow-lg:var(--app-shadow-md);--shadow-xl:var(--app-shadow-lg);--shadow-2xl:var(--app-shadow-lg);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base)*.5);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feLine truncated
|
||||
@@ -1 +1 @@
|
||||
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const resetMobileSubmenus=()=>{overlay.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(item=>this.setMobileSubmenuExpanded(item,false))};const expandActiveMobileGroup=()=>{const activeGroup=overlay.querySelector(".mobile-nav-item.has-active");if(activeGroup)this.setMobileSubmenuExpanded(activeGroup,true)};const updateToggleState=expanded=>{const desktopSidebar=isDesktopSidebar();navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded?"true":"false");navigationToggle.setAttribute("aria-controls",desktopSidebar?"sidebar-panel":"mobile-menu-overlay");const label=expanded&&!desktopSidebar?_("Close"):_("Navigation");navigationToggle.setAttribute("title",label);navigationToggle.setAttribute("aria-label",label)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.classList.remove("mobile-navigation-open");document.body.style.overflow="";resetMobileSubmenus()};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.classList.toggle("mobile-navigation-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)expandActiveMobileGroup();else resetMobileSubmenus();updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const toggle=e.target.closest(".mobile-nav-toggle");if(toggle&&overlay.contains(toggle)){e.preventDefault();e.stopPropagation();const item=toggle.closest(".mobile-nav-item");if(!item)return;const isExpanded=item.classList.contains("submenu-expanded");overlay.querySelectorAll(".mobile-nav-item.submenu-expanded").forEach(expandedItem=>{if(expandedItem!==item){this.setMobileSubmenuExpanded(expandedItem,false)}});this.setMobileSubmenuExpanded(item,!isExpanded);return}const destination=e.target.closest(".mobile-nav-link:not(.mobile-nav-toggle), .mobile-nav-sublink, .mobile-nav-logout");if(destination&&overlay.contains(destination)){setNavigationExpanded(false)}})},renderMobileMenu(tree,url){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");const children=ui.menu.getChildren(tree);if(!list)return;list.innerHTML="";if(footerAction)footerAction.innerHTML="";if(!children.length)return;children.forEach(child=>{const submenu=ui.menu.getChildren(child);const hasSubmenu=submenu.length>0;const isActive=this.isActivePath(child.name);if(child.name==="logout"){if(footerAction){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:L.url(url,child.name)},[_(child.title)]))}return}const itemClasses=["mobile-nav-item",hasSubmenu?"has-submenu":"",isActive&&hasSubmenu?"has-active":""].filter(Boolean);const submenuId=`mobile-submenu-${String(child.name).replace(/[^A-Za-z0-9_-]/g,"-")}`;const control=hasSubmenu?E("button",{class:`mobile-nav-link mobile-nav-toggle${isActive?" nav-link-active":""}`,type:"button","aria-expanded":"false","aria-controls":sLine truncated
|
||||
"use strict";"require baseclass";"require ui";return baseclass.extend({__init__(){ui.menu.load().then(tree=>this.render(tree));this.initNavigationControls();this.initUciIndicator()},initUciIndicator(){const original=ui.changes?.setIndicator;if(!original)return;ui.changes.setIndicator=function(n){original.call(this,n);document.querySelector('[data-indicator="uci-changes"]')?.setAttribute("data-count",n||0)}},initNavigationControls(){const overlay=document.querySelector("#mobile-menu-overlay");const navigationToggle=document.querySelector("#navigation-toggle");if(!navigationToggle||!overlay)return;const mobileList=overlay.querySelector("#mobile-nav-list");const desktop=window.matchMedia("(min-width: 768px)");const SIDEBAR_COLLAPSED_KEY="aurora.sidebarCollapsed";const isDesktopSidebar=()=>desktop.matches&&document.body.dataset.navType==="sidebar";const updateToggleState=expanded=>{const desktopSidebar=isDesktopSidebar();navigationToggle.classList.toggle("is-expanded",expanded&&!desktop.matches);navigationToggle.setAttribute("aria-expanded",expanded?"true":"false");navigationToggle.setAttribute("aria-controls",desktopSidebar?"sidebar-panel":"mobile-menu-overlay");const label=expanded&&!desktopSidebar?_("Close"):_("Navigation");navigationToggle.setAttribute("title",label);navigationToggle.setAttribute("aria-label",label)};const closeMobileNavigation=()=>{overlay.classList.remove("mobile-menu-open");document.body.classList.remove("mobile-navigation-open");document.body.style.overflow="";this.resetNavigationGroups(mobileList)};const getNavigationExpanded=()=>{if(isDesktopSidebar()){return!document.body.classList.contains("sidebar-collapsed")}return!desktop.matches&&overlay.classList.contains("mobile-menu-open")};const setNavigationExpanded=expanded=>{if(isDesktopSidebar()){closeMobileNavigation();const collapsed=!expanded;document.body.classList.toggle("sidebar-collapsed",collapsed);localStorage.setItem(SIDEBAR_COLLAPSED_KEY,collapsed);updateToggleState(expanded);return}if(desktop.matches){closeMobileNavigation();updateToggleState(false);return}overlay.classList.toggle("mobile-menu-open",expanded);document.body.classList.toggle("mobile-navigation-open",expanded);document.body.style.overflow=expanded?"hidden":"";if(expanded)this.expandActiveNavigationGroup(mobileList);else this.resetNavigationGroups(mobileList);updateToggleState(expanded)};const toggleNavigation=()=>setNavigationExpanded(!getNavigationExpanded());const syncNavigationState=()=>{closeMobileNavigation();if(isDesktopSidebar()){const collapsed=localStorage.getItem(SIDEBAR_COLLAPSED_KEY)==="true";document.body.classList.toggle("sidebar-collapsed",collapsed);updateToggleState(!collapsed);return}updateToggleState(false)};navigationToggle.addEventListener("click",e=>{e.stopPropagation();toggleNavigation()});overlay.addEventListener("click",e=>{if(e.target===overlay)setNavigationExpanded(false)});document.addEventListener("keydown",e=>{if(e.key==="Escape"&&overlay.classList.contains("mobile-menu-open")){setNavigationExpanded(false)}});desktop.addEventListener("change",syncNavigationState);syncNavigationState();document.addEventListener("click",e=>{const destination=e.target?.closest?.(".navigation-direct, .navigation-sublink, .mobile-nav-logout");if(destination&&overlay.contains(destination)){setNavigationExpanded(false)}})},renderMobileMenu(items){const list=document.querySelector("#mobile-nav-list");const footerAction=document.querySelector("#mobile-nav-footer-action");if(list)list.innerHTML="";if(footerAction)footerAction.innerHTML="";if(!list)return;items.forEach(item=>{if(item.isLogout){if(footerAction){footerAction.appendChild(E("a",{class:"mobile-nav-logout",href:item.href},[item.title]))}return}list.appendChild(this.renderNavigationItem(item,"mobile"))});this.bindNavigationAccordion(list)},render(tree){this.renderModeMenu(tree);if(L.env.dispatchpath.length>=3){let node=tree;let url="";for(let i=0;i<3&&node;i++){const segment=L.env.dispatchpath[i];node=node.children?.[segment];url+=(url?"/":"")+segment}if(node)this.renderTabMenu(node,url)}},renderTabMenu(tree,url,level=0){const container=document.querySelector("#tabmenu");const ul=E("ul",{class:"tabs"});const children=ui.menu.getChildren(tree);let activeNode=null;children.forEach(child=>{const isActive=L.env.dispatchpath[3+level]===child.name;ul.appendChild(E("li",{class:`tabmenu-item-${child.name}${isActive?" active":""}`},[E("a",{href:L.url(url,child.name)},[_(child.title)])]));if(isActive)activeNode=child});if(!ul.children.length)return E([]);container.appendChild(ul);container.style.display="";if(activeNode){this.renderTabMenu(activeNode,`${url}/${activeNode.name}`,level+1)}return ul},renderMainMenu(tree,url,level=0,navigationItems=null){const ul=level?E("ul",{class:"desktop-nav-list"}):document.querySelector("#topmenu");const children=ui.menu.getChildren(tree);if(level>1)return E([]);if(level===0){const navType=document.body?.dataset?.navType||"mega-menu";if(navType==="sidebar"){this.renderSidebLine truncated
|
||||
+17
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=opera-proxy
|
||||
PKG_VERSION:=1.24.0
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
PKG_LICENSE:=MIT
|
||||
@@ -44,9 +44,24 @@ endif
|
||||
PKG_SOURCE_URL:=https://github.com/Alexey71/opera-proxy/releases/download/v$(PKG_VERSION)
|
||||
PKG_SOURCE:=opera-proxy.linux-$(GOARCH_SUFFIX)
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DEPENDS:=upx/host
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
UPX:=$(STAGING_DIR_HOST)/bin/upx
|
||||
UPX_AVAILABLE:=$(shell [ -x "$(UPX)" ] && echo yes || echo no)
|
||||
|
||||
ifeq ($(UPX_AVAILABLE),yes)
|
||||
define upxcompress
|
||||
@echo "Compressing $(1) with UPX..."
|
||||
$(UPX) --lzma --best $(1)
|
||||
endef
|
||||
else
|
||||
define upxcompress
|
||||
@echo "UPX not found at $(UPX), skipping compression"
|
||||
endef
|
||||
endif
|
||||
|
||||
define Package/opera-proxy
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
@@ -69,6 +84,7 @@ define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) $(DL_DIR)/$(PKG_SOURCE) $(PKG_BUILD_DIR)/opera-proxy
|
||||
chmod +x $(PKG_BUILD_DIR)/opera-proxy
|
||||
$(call upxcompress,$(PKG_BUILD_DIR)/opera-proxy)
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
|
||||
+22
-2
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=tun2socks
|
||||
PKG_VERSION:=2.6.0
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/xjasonlyu/tun2socks.git
|
||||
@@ -12,7 +12,7 @@ PKG_MAINTAINER:=Konstantine Shevlakov <shevlako@132lan.ru>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_DEPENDS:=golang/host upx/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
@@ -21,6 +21,25 @@ GO_PKG=github.com/xjasonlyu/tun2socks
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
# --- UPX support ---
|
||||
UPX:=$(STAGING_DIR_HOST)/bin/upx
|
||||
UPX_AVAILABLE:=$(shell [ -x "$(UPX)" ] && echo yes || echo no)
|
||||
|
||||
ifeq ($(UPX_AVAILABLE),yes)
|
||||
define upxcompress
|
||||
@echo "Compressing $(1) with UPX..."
|
||||
$(UPX) --lzma --best $(1)
|
||||
endef
|
||||
else
|
||||
define upxcompress
|
||||
@echo "UPX not found at $(UPX), skipping compression"
|
||||
endef
|
||||
endif
|
||||
|
||||
GO_PKG_LDFLAGS:=-s -w
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
github.com/xjasonlyu/tun2socks/internal/version.Version=v$(PKG_VERSION) \
|
||||
github.com/xjasonlyu/tun2socks/internal/version.GitCommit=$(shell echo $(PKG_SOURCE_VERSION) | cut -c1-7)
|
||||
GO_MOD_ARGS:=
|
||||
GO_PKG_BUILD_VARS+= GO111MODULE=on
|
||||
|
||||
@@ -37,6 +56,7 @@ define Package/$(PKG_NAME)/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/$(PKG_NAME) $(1)/usr/sbin/
|
||||
$(call upxcompress,$(1)/usr/sbin/$(PKG_NAME))
|
||||
$(CP) ./root/* $(1)/
|
||||
endef
|
||||
|
||||
|
||||
Reference in new issue
Block a user