v0.36.2
This commit is contained in:
1 parent
6d7f152a0b
commit
064772765b
24 files changed
+418
-67
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.2",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -3827,6 +3827,11 @@
|
||||
"default": false,
|
||||
"description": "Account-scoped administration UI preference. When enabled, semantic inline SVG icons are added to navigation, actions, tabs and dialogs while preserving text labels and accessibility."
|
||||
},
|
||||
"logo_animation": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped administration UI preference. When enabled, the sidebar logo plays short, subtle animations at random intervals and on hover. Paused while the tab is hidden and suppressed when the browser requests reduced motion."
|
||||
},
|
||||
"button_style": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -120,6 +120,8 @@ struct AdminPreferences {
|
||||
chart_axes: bool,
|
||||
#[serde(default)]
|
||||
interface_icons: bool,
|
||||
#[serde(default)]
|
||||
logo_animation: bool,
|
||||
#[serde(default = "default_admin_button_style")]
|
||||
button_style: String,
|
||||
#[serde(default = "default_admin_corner_style")]
|
||||
@@ -183,6 +185,7 @@ impl Default for AdminPreferences {
|
||||
chart_style: default_admin_chart_style(),
|
||||
chart_axes: false,
|
||||
interface_icons: false,
|
||||
logo_animation: false,
|
||||
button_style: default_admin_button_style(),
|
||||
corner_style: default_admin_corner_style(),
|
||||
table_density: default_admin_table_density(),
|
||||
|
||||
@@ -42,6 +42,20 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
assert!(validate_admin_preferences(&AdminPreferences::default()).is_ok());
|
||||
assert!(!AdminPreferences::default().chart_axes);
|
||||
assert!(!AdminPreferences::default().interface_icons);
|
||||
assert!(!AdminPreferences::default().logo_animation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_logo_animation_preference_is_backward_compatible_and_off_by_default() {
|
||||
let legacy: AdminPreferences = serde_json::from_str(r#"{"theme":"light"}"#).unwrap();
|
||||
assert!(!legacy.logo_animation);
|
||||
let mut enabled = legacy.clone();
|
||||
enabled.logo_animation = true;
|
||||
assert!(validate_admin_preferences(&enabled).is_ok());
|
||||
let stored = serde_json::to_string(&enabled).unwrap();
|
||||
assert!(stored.contains("\"logo_animation\":true"));
|
||||
let recovered: AdminPreferences = serde_json::from_str(&stored).unwrap();
|
||||
assert!(recovered.logo_animation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -58,6 +58,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/overview-samples.js"),
|
||||
include_str!("web/app/core.js"),
|
||||
include_str!("web/app/icons.js"),
|
||||
include_str!("web/app/logo-animation.js"),
|
||||
include_str!("web/app/path-permissions.js"),
|
||||
include_str!("web/app/session-resume.js"),
|
||||
include_str!("web/app/refresh.js"),
|
||||
|
||||
@@ -2147,6 +2147,7 @@ mod tests {
|
||||
assert_eq!(initial["chart_style"], "default");
|
||||
assert_eq!(initial["chart_axes"], false);
|
||||
assert_eq!(initial["interface_icons"], false);
|
||||
assert_eq!(initial["logo_animation"], false);
|
||||
assert_eq!(initial["button_style"], "normal");
|
||||
assert_eq!(initial["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2185,6 +2186,7 @@ mod tests {
|
||||
assert_eq!(legacy["chart_style"], "default");
|
||||
assert_eq!(legacy["chart_axes"], false);
|
||||
assert_eq!(legacy["interface_icons"], false);
|
||||
assert_eq!(legacy["logo_animation"], false);
|
||||
assert_eq!(legacy["button_style"], "normal");
|
||||
assert_eq!(legacy["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2208,6 +2210,7 @@ mod tests {
|
||||
"chart_style":"neon",
|
||||
"chart_axes":true,
|
||||
"interface_icons":true,
|
||||
"logo_animation":true,
|
||||
"button_style":"compact",
|
||||
"corner_style":"rounded",
|
||||
"table_density":"ultra",
|
||||
|
||||
+84
-29
@@ -3,6 +3,7 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
activity: [["path", { d: "M3 12h4l2-6 4 12 2-6h6" }]],
|
||||
alert: [["path", { d: "M12 3 2.8 20h18.4L12 3Z" }], ["path", { d: "M12 9v4" }], ["circle", { cx: "12", cy: "17", r: ".7", fill: "currentColor", stroke: "none" }]],
|
||||
archive: [["rect", { x: "3", y: "5", width: "18", height: "4", rx: "1" }], ["path", { d: "M5 9v10h14V9M9 13h6" }]],
|
||||
ban: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "m5.6 5.6 12.8 12.8" }]],
|
||||
book: [["path", { d: "M4 4.5h5.5A2.5 2.5 0 0 1 12 7v13a3 3 0 0 0-3-3H4V4.5ZM20 4.5h-5.5A2.5 2.5 0 0 0 12 7v13a3 3 0 0 1 3-3h5V4.5Z" }]],
|
||||
box: [["path", { d: "m12 3 8 4.5v9L12 21l-8-4.5v-9L12 3Z" }], ["path", { d: "m4 7.5 8 4.5 8-4.5M12 12v9" }]],
|
||||
certificate: [["rect", { x: "4", y: "3", width: "16", height: "14", rx: "2" }], ["path", { d: "M8 7h8M8 10h5M9 17l-1 4 4-2 4 2-1-4" }]],
|
||||
@@ -13,11 +14,15 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
chevronUp: [["path", { d: "m6 15 6-6 6 6" }]],
|
||||
code: [["path", { d: "m9 7-5 5 5 5M15 7l5 5-5 5M13 5l-2 14" }]],
|
||||
command: [["rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }], ["path", { d: "M8 12h8M12 8v8" }]],
|
||||
collapse: [["path", { d: "M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" }]],
|
||||
compare: [["rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }], ["path", { d: "M12 4v16M6 9h3M6 13h3M15 11h3M15 15h3" }]],
|
||||
copy: [["rect", { x: "8", y: "8", width: "11", height: "11", rx: "2" }], ["path", { d: "M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" }]],
|
||||
cpu: [["rect", { x: "7", y: "7", width: "10", height: "10", rx: "2" }], ["path", { d: "M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4M10 10h4v4h-4z" }]],
|
||||
database: [["ellipse", { cx: "12", cy: "5", rx: "7", ry: "3" }], ["path", { d: "M5 5v7c0 1.7 3.1 3 7 3s7-1.3 7-3V5M5 12v7c0 1.7 3.1 3 7 3s7-1.3 7-3v-7" }]],
|
||||
download: [["path", { d: "M12 3v12m0 0-4-4m4 4 4-4M4 19h16" }]],
|
||||
edit: [["path", { d: "M4 20h4l11-11-4-4L4 16v4ZM13.5 6.5l4 4" }]],
|
||||
eraser: [["path", { d: "m7 20-3.3-3.3a1 1 0 0 1 0-1.4l9.6-9.6a1 1 0 0 1 1.4 0l5.6 5.6a1 1 0 0 1 0 1.4L13 20H7ZM21 20H7M5.5 11.5l7 7" }]],
|
||||
expand: [["path", { d: "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }]],
|
||||
external: [["path", { d: "M14 4h6v6M20 4l-9 9" }], ["path", { d: "M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6" }]],
|
||||
eye: [["path", { d: "M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6S2.5 12 2.5 12Z" }], ["circle", { cx: "12", cy: "12", r: "2.5" }]],
|
||||
eyeOff: [["path", { d: "M3 3l18 18M10.6 6.2A10 10 0 0 1 12 6c6 0 9.5 6 9.5 6a15 15 0 0 1-3 3.6M6.1 6.2C3.8 8 2.5 12 2.5 12s3.5 6 9.5 6a10 10 0 0 0 3.2-.5M9.9 9.9a3 3 0 0 0 4.2 4.2" }]],
|
||||
@@ -26,33 +31,40 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
filter: [["path", { d: "M3 5h18l-7 8v6l-4 2v-8L3 5Z" }]],
|
||||
globe: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M3 12h18M12 3c2.5 2.7 3.5 5.7 3.5 9S14.5 18.3 12 21M12 3C9.5 5.7 8.5 8.7 8.5 12S9.5 18.3 12 21" }]],
|
||||
grid: [["rect", { x: "4", y: "4", width: "6", height: "6", rx: "1" }], ["rect", { x: "14", y: "4", width: "6", height: "6", rx: "1" }], ["rect", { x: "4", y: "14", width: "6", height: "6", rx: "1" }], ["rect", { x: "14", y: "14", width: "6", height: "6", rx: "1" }]],
|
||||
hash: [["path", { d: "M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" }]],
|
||||
history: [["path", { d: "M4 8V4m0 0h4M4 4l3 3a8 8 0 1 1-2.2 7" }], ["path", { d: "M12 8v5l3 2" }]],
|
||||
info: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M12 11v5" }], ["circle", { cx: "12", cy: "7.8", r: ".8", fill: "currentColor", stroke: "none" }]],
|
||||
key: [["circle", { cx: "8", cy: "12", r: "4" }], ["path", { d: "M12 12h9M18 12v3M15 12v2" }]],
|
||||
layers: [["path", { d: "m12 3 9 5-9 5-9-5 9-5Z" }], ["path", { d: "m3 12.5 9 5 9-5M3 16.5l9 5 9-5" }]],
|
||||
list: [["path", { d: "M9 6h11M9 12h11M9 18h11" }], ["circle", { cx: "4.5", cy: "6", r: ".8", fill: "currentColor", stroke: "none" }], ["circle", { cx: "4.5", cy: "12", r: ".8", fill: "currentColor", stroke: "none" }], ["circle", { cx: "4.5", cy: "18", r: ".8", fill: "currentColor", stroke: "none" }]],
|
||||
loader: [["path", { d: "M20 12a8 8 0 1 1-2.3-5.7" }]],
|
||||
lock: [["rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }], ["path", { d: "M8 10V7a4 4 0 0 1 8 0v3" }]],
|
||||
login: [["path", { d: "M10 5H5v14h5M14 8l4 4-4 4M8 12h10" }]],
|
||||
logout: [["path", { d: "M14 5h5v14h-5M10 8l-4 4 4 4M16 12H6" }]],
|
||||
login: [["path", { d: "M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 8l4 4-4 4M14 12H3" }]],
|
||||
logout: [["path", { d: "M9 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h3M16 8l4 4-4 4M20 12H9" }]],
|
||||
mapPin: [["path", { d: "M12 21s-7-6.2-7-11a7 7 0 0 1 14 0c0 4.8-7 11-7 11Z" }], ["circle", { cx: "12", cy: "10", r: "2.5" }]],
|
||||
menu: [["path", { d: "M4 7h16M4 12h16M4 17h16" }]],
|
||||
network: [["rect", { x: "9", y: "3", width: "6", height: "5", rx: "1" }], ["rect", { x: "3", y: "16", width: "6", height: "5", rx: "1" }], ["rect", { x: "15", y: "16", width: "6", height: "5", rx: "1" }], ["path", { d: "M12 8v4M6 16v-4h12v4" }]],
|
||||
palette: [["path", { d: "M12 3a9 9 0 1 0 0 18h1.5a2 2 0 0 0 0-4H12a2 2 0 0 1 0-4h4a5 5 0 0 0 0-10h-4Z" }], ["circle", { cx: "7", cy: "10", r: "1", fill: "currentColor", stroke: "none" }], ["circle", { cx: "9", cy: "6.5", r: "1", fill: "currentColor", stroke: "none" }], ["circle", { cx: "14", cy: "6", r: "1", fill: "currentColor", stroke: "none" }]],
|
||||
pause: [["path", { d: "M8 5v14M16 5v14" }]],
|
||||
pin: [["path", { d: "m9 3 6 6-2 2 4 4-2 2-4-4-2 2-6-6 2-2 4 4 2-2-4-4 2-2ZM5 19l4-4" }]],
|
||||
pin: [["path", { d: "M8 3h8M10 3v5l-3 4h10l-3-4V3M12 12v9" }]],
|
||||
play: [["path", { d: "m8 5 11 7-11 7V5Z" }]],
|
||||
plug: [["path", { d: "M9 3v5M15 3v5M6 8h12v3a6 6 0 0 1-12 0V8ZM12 17v4" }]],
|
||||
plus: [["path", { d: "M12 5v14M5 12h14" }]],
|
||||
refresh: [["path", { d: "M20 7v5h-5M4 17v-5h5" }], ["path", { d: "M18.5 9A7 7 0 0 0 6.2 6.2L4 8M5.5 15A7 7 0 0 0 17.8 17.8L20 16" }]],
|
||||
route: [["circle", { cx: "6", cy: "6", r: "2" }], ["circle", { cx: "18", cy: "18", r: "2" }], ["path", { d: "M8 6h4a4 4 0 0 1 4 4v0a4 4 0 0 1-4 4H8a2 2 0 0 0-2 2v0" }]],
|
||||
save: [["path", { d: "M5 4h12l2 2v14H5V4Z" }], ["path", { d: "M8 4v6h8V4M8 20v-6h8v6" }]],
|
||||
search: [["circle", { cx: "10.5", cy: "10.5", r: "6.5" }], ["path", { d: "m15.5 15.5 5 5" }]],
|
||||
settings: [["circle", { cx: "12", cy: "12", r: "3" }], ["path", { d: "M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6 7 7M17 17l1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4" }]],
|
||||
settings: [["path", { d: "M18.9 9.9L21.3 10.3L21.3 13.7L18.9 14.1L18.4 15.4L19.8 17.4L17.4 19.8L15.4 18.4L14.1 18.9L13.7 21.3L10.3 21.3L9.9 18.9L8.6 18.4L6.6 19.8L4.2 17.4L5.6 15.4L5.1 14.1L2.7 13.7L2.7 10.3L5.1 9.9L5.6 8.6L4.2 6.6L6.6 4.2L8.6 5.6L9.9 5.1L10.3 2.7L13.7 2.7L14.1 5.1L15.4 5.6L17.4 4.2L19.8 6.6L18.4 8.6Z" }], ["circle", { cx: "12", cy: "12", r: "3" }]],
|
||||
shield: [["path", { d: "M12 3 20 6v6c0 5-3.3 8-8 9-4.7-1-8-4-8-9V6l8-3Z" }], ["path", { d: "m8.5 12 2.2 2.2 4.8-5" }]],
|
||||
sliders: [["path", { d: "M4 7h6M14 7h6M4 17h10M18 17h2M10 4v6M14 14v6" }]],
|
||||
sortDown: [["path", { d: "M8 5v14M5 16l3 3 3-3M14 8h5M14 12h4M14 16h3" }]],
|
||||
sortUp: [["path", { d: "M8 19V5M5 8l3-3 3 3M14 8h3M14 12h4M14 16h5" }]],
|
||||
transfer: [["path", { d: "M4 8h15m0 0-4-4m4 4-4 4M20 16H5m0 0 4-4m-4 4 4 4" }]],
|
||||
trash: [["path", { d: "M4 7h16M9 3h6l1 4H8l1-4ZM7 7l1 14h8l1-14M10 11v6M14 11v6" }]],
|
||||
toggleOff: [["rect", { x: "3", y: "7", width: "18", height: "10", rx: "5" }], ["circle", { cx: "8", cy: "12", r: "3", fill: "currentColor", stroke: "none" }]],
|
||||
toggleOn: [["rect", { x: "3", y: "7", width: "18", height: "10", rx: "5" }], ["circle", { cx: "16", cy: "12", r: "3", fill: "currentColor", stroke: "none" }]],
|
||||
undo: [["path", { d: "M9 7H4v-5M4 7l4-4M4.5 8A8 8 0 1 1 5 17" }]],
|
||||
undo: [["path", { d: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11" }]],
|
||||
unlock: [["rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }], ["path", { d: "M8 10V7a4 4 0 0 1 7.8-1.2" }]],
|
||||
upload: [["path", { d: "M12 16V4m0 0-4 4m4-4 4 4M4 20h16" }]],
|
||||
users: [["circle", { cx: "9", cy: "9", r: "3" }], ["path", { d: "M3 20v-2a5 5 0 0 1 10 0v2M16 8a3 3 0 0 1 0 6M16 15a5 5 0 0 1 5 5" }]],
|
||||
wand: [["path", { d: "m4 20 12-12 3 3L7 23 4 20ZM14 4l1-3 1 3 3 1-3 1-1 3-1-3-3-1 3-1ZM19 14l.7-2 .8 2 2 .8-2 .7-.8 2-.7-2-2-.7 2-.8Z" }]],
|
||||
@@ -61,13 +73,13 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
|
||||
const UI_PANEL_ICONS = Object.freeze({
|
||||
overview: "grid", vhosts: "network", "web-server": "globe", "docker-vhosts": "box",
|
||||
redirects: "route", notfound: "fileX", streams: "network", "acl-profiles": "shield",
|
||||
redirects: "route", notfound: "fileX", streams: "transfer", "acl-profiles": "shield",
|
||||
tls: "certificate", traffic: "activity", cache: "database", transform: "wand", settings: "settings",
|
||||
});
|
||||
const UI_SETTINGS_TAB_ICONS = Object.freeze({
|
||||
appearance: "palette", listeners: "network", connections: "globe", "docker-agents": "box",
|
||||
appearance: "palette", listeners: "plug", connections: "globe", "docker-agents": "box",
|
||||
policies: "shield", cache: "database", transform: "wand", forwarding: "route", pages: "file",
|
||||
logging: "list", geoip: "globe", prometheus: "activity", administration: "settings",
|
||||
logging: "list", geoip: "mapPin", prometheus: "activity", administration: "users",
|
||||
"system-info": "cpu", "certificate-api": "key", "tls-policy": "lock", "manual-config": "code", security: "shield",
|
||||
});
|
||||
const UI_VHOST_TAB_ICONS = Object.freeze({
|
||||
@@ -75,17 +87,19 @@ const UI_VHOST_TAB_ICONS = Object.freeze({
|
||||
cache: "database", compression: "archive", transform: "wand", security: "shield", pages: "file", advanced: "code",
|
||||
});
|
||||
const UI_TRANSLATED_ICON_KEYS = Object.freeze([
|
||||
["trash", ["remove", "delete", "draftDiscard", "cacheRemoveBan", "certificateApiRevokeAll", "revokeAll"]],
|
||||
["trash", ["remove", "delete", "draftDiscard", "cacheRemoveBan", "cachePurgeUrl", "certificateApiRevoke", "certificateApiRevokeAll", "apiTokenRevoke", "revokeAll"]],
|
||||
["x", ["close", "cancelAction", "overviewLayoutInlineCancel"]],
|
||||
["save", ["saveCertificate", "savePassword", "dashboardPresetsSave", "overviewLayoutInlineSave"]],
|
||||
["check", ["apply", "applyFilters", "savePolicy", "continueAction", "webServicePresetApply", "dockerApplyService"]],
|
||||
["save", ["certAssignApply", "saveCertificate", "savePassword", "dashboardPresetsSave", "overviewLayoutInlineSave"]],
|
||||
["check", ["apply", "applyFilters", "savePolicy", "continueAction", "dockerApplyService"]],
|
||||
["plus", ["addCertificate", "vhNew", "webServiceNew", "redirectNew", "notfoundNew", "streamNew", "aclProfileNew", "phpProfileAdd", "redirectAddRule", "streamAddListener", "aclProfileAddIpException", "apiTokenCreate"]],
|
||||
["refresh", ["refresh", "refreshNow", "reload", "reloadAudit", "reloadCertificates", "tunnelsReload", "dockerRefresh", "cacheRefresh", "sessionRetry"]],
|
||||
["undo", ["configHistoryRestoreAction", "dashboardPresetsFactory", "draftRestore"]],
|
||||
["edit", ["edit"]],
|
||||
["eyeOff", ["hideMetric", "overviewLayoutEditorHide"]],
|
||||
["edit", ["edit", "certificateApiEdit"]],
|
||||
["key", ["certificateApiRegenerate"]],
|
||||
["download", ["export"]],
|
||||
["upload", ["acmeImport", "aclProfileImportRules"]],
|
||||
["eye", ["certificateReadNames", "dockerShowHidden", "dashboardWidgetsShowAll"]],
|
||||
["eye", ["certificateReadNames", "dockerShowHidden", "overviewLayoutEditorShow"]],
|
||||
["settings", ["settings", "vhManage", "dockerAgentsSettings"]],
|
||||
["book", ["webServiceHtaccessDocs", "webServicePhpDocsButton"]],
|
||||
["pause", ["logPause"]],
|
||||
@@ -116,20 +130,52 @@ function uiIconMatchesTranslation(target, keys) {
|
||||
if (typeof t !== "function") return false;
|
||||
const strings = uiIconStrings(target); if (!strings.length) return false;
|
||||
for (const key of keys) {
|
||||
const translated = String(t(key) || "").trim().toLocaleLowerCase();
|
||||
if (!translated || translated === key.toLocaleLowerCase()) continue;
|
||||
const raw = String(t(key) || "").trim(), translated = raw.toLocaleLowerCase();
|
||||
// t() returns the key itself when a translation is missing; compare case-sensitively
|
||||
// so an English label such as "Edit" for the key "edit" still counts as translated.
|
||||
if (!translated || raw === key) continue;
|
||||
if (strings.some(value => value === translated || value.startsWith(`${translated} `) || value.startsWith(`${translated}:`))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Explicit choices for controls whose ids/i18n keys would otherwise collide with
|
||||
// broader keyword rules (for example "unlock" vs "lock", "presets" vs "reset",
|
||||
// "docker" vs "dock", or a domain noun such as "cache" before a verb such as "next").
|
||||
const UI_ID_ICONS = Object.freeze({
|
||||
"about-open": "info", "login-submit": "login",
|
||||
"overview-layout-unlock": "unlock", "overview-inline-save": "lock", "overview-inline-compact": "grid",
|
||||
"overview-layout-open": "layers", "overview-layout-open-settings": "layers",
|
||||
"overview-presets-close": "x", "overview-presets-save": "save", "overview-presets-factory": "undo",
|
||||
"docker-settings-open": "settings", "settings-vhost-open": "network", "settings-file-logs-open": "list",
|
||||
"log-format-open": "sliders", "file-log-tab-counts": "hash", "refresh-reset": "undo",
|
||||
"acme-submit": "certificate", "geoip-update-now": "download",
|
||||
"cache-panel-cleanup": "eraser", "cache-invalidation-open": "ban", "cache-bans-open": "ban",
|
||||
"prom-token-generate": "key", "certificate-api-save": "key", "revoke-sessions": "logout",
|
||||
"acl-profile-reset": "eraser", "reset-certificate": "eraser",
|
||||
"config-history-compare-open": "compare", "logs-load-history": "history",
|
||||
});
|
||||
const UI_I18N_ICONS = Object.freeze({
|
||||
apply: "check", fileLogsCountRepeats: "hash", cacheRunInvalidation: "play",
|
||||
previous: "chevronLeft", next: "chevronRight", cachePrevious: "chevronLeft", cacheNext: "chevronRight",
|
||||
renewalEnable: "toggleOn", renewalDisable: "toggleOff", tunnelKill: "ban", certAssignVhosts: "network",
|
||||
});
|
||||
// Buttons whose visible label is already a single symbol (×, −, +, ←, →) keep the
|
||||
// native glyph instead of receiving a second, competing SVG.
|
||||
const UI_GLYPH_LABEL = /^[×✕−+←→↑↓⠿]$/;
|
||||
function uiIconForTarget(target) {
|
||||
const manual = target.dataset.uiIconName;
|
||||
if (manual === "none") return null;
|
||||
if (manual && UI_ICON_SHAPES[manual]) return manual;
|
||||
if (target.matches(".chart-style-choice")) return null;
|
||||
if (target.matches(".overview-inline-grip, .overview-inline-narrow, .overview-inline-widen, .overview-inline-up, .overview-inline-down, .overview-inline-hide, .overview-layout-narrow, .overview-layout-widen, .overview-layout-up, .overview-layout-down, .overview-layout-hide")) return null;
|
||||
if (target.matches("#tunnels-open")) return null;
|
||||
if (target.matches("#web-service-preset-apply")) return "wand";
|
||||
if (target.matches("#tunnels-open, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle")) return null;
|
||||
if (UI_GLYPH_LABEL.test(String(target.textContent || "").trim())) return null;
|
||||
if (UI_ID_ICONS[target.id]) return UI_ID_ICONS[target.id];
|
||||
if (target.matches("#web-service-preset-apply, [data-cache-memory-preset], [data-cache-cold-preset], [data-transform-preset]")) return "wand";
|
||||
if (target.matches("#logs-pause")) return uiIconMatchesTranslation(target, ["logResume"]) ? "play" : "pause";
|
||||
if (target.matches("#docker-metrics-toggle-all, #dashboard-widget-expand")) return target.getAttribute("aria-expanded") === "true" ? "collapse" : "expand";
|
||||
if (target.matches(".docker-open-logs")) return "list";
|
||||
if (target.matches(".docker-view-toggle")) return uiIconMatchesTranslation(target, ["dockerUnhideVhost"]) ? "eye" : "eyeOff";
|
||||
if (target.matches(".vhost-quick-toggle.is-enable")) return "toggleOn";
|
||||
if (target.matches(".vhost-quick-toggle.is-disable")) return "toggleOff";
|
||||
if (target.matches(".listener-h2-settings-link, .listener-h3-settings-link")) return "settings";
|
||||
@@ -145,6 +191,9 @@ function uiIconForTarget(target) {
|
||||
if (target.dataset.dashboardWidgetAction === "toggle") return target.classList.contains("is-hidden") ? "eyeOff" : "eye";
|
||||
if (target.dataset.dashboardWidgetAction === "up") return "chevronLeft";
|
||||
if (target.dataset.dashboardWidgetAction === "down") return "chevronRight";
|
||||
const i18nKey = target.dataset.i18n || target.dataset.action || target.dataset.presetAction || "";
|
||||
if (UI_I18N_ICONS[i18nKey]) return UI_I18N_ICONS[i18nKey];
|
||||
for (const [key, icon] of Object.entries(UI_I18N_ICONS)) if (uiIconMatchesTranslation(target, [key]) && !["previous", "next", "apply"].includes(key)) return icon;
|
||||
|
||||
const signature = [target.id, target.dataset.i18n, target.dataset.i18nAria, target.dataset.action, target.className]
|
||||
.filter(Boolean).join(" ").toLocaleLowerCase();
|
||||
@@ -152,10 +201,17 @@ function uiIconForTarget(target) {
|
||||
if (/mobile-menu|menu-toggle/.test(signature)) return "menu";
|
||||
if (/is-saving|saving/.test(signature)) return "loader";
|
||||
if (/sort-direction|sort-button/.test(signature)) return /descending|desc/.test(aria) ? "sortDown" : "sortUp";
|
||||
if (/scroll-prev|move-earlier|layout-up/.test(signature)) return "chevronLeft";
|
||||
if (/scroll-next|move-later|layout-down/.test(signature)) return "chevronRight";
|
||||
if (/collapse|narrow/.test(signature)) return "chevronDown";
|
||||
if (/expand|widen/.test(signature)) return "chevronUp";
|
||||
if (/scroll-prev|move-earlier|layout-up|-prev\b/.test(signature)) return "chevronLeft";
|
||||
if (/scroll-next|move-later|layout-down|-next\b/.test(signature)) return "chevronRight";
|
||||
// Verbs win over domain nouns: a "Close" button inside the health or history
|
||||
// dialog is still a close action.
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/sign-in|login/.test(signature)) return "login";
|
||||
if (/logout|sign-out/.test(signature)) return "logout";
|
||||
if (/unlock/.test(signature)) return "unlock";
|
||||
if (/collapse|narrow/.test(signature)) return "collapse";
|
||||
if (/expand|widen/.test(signature)) return "expand";
|
||||
if (/compare|diff/.test(signature)) return "compare";
|
||||
if (/health/.test(signature) && !/certificate/.test(signature)) return "activity";
|
||||
if (/copy/.test(signature)) return "copy";
|
||||
if (/export|download/.test(signature)) return "download";
|
||||
@@ -163,21 +219,20 @@ function uiIconForTarget(target) {
|
||||
if (/search|find/.test(signature)) return "search";
|
||||
if (/filter/.test(signature)) return "filter";
|
||||
if (/inspect|preview|read-names|readnames|show-hidden|visibility|view-toggle/.test(signature)) return "eye";
|
||||
if (/history|revision/.test(signature)) return "history";
|
||||
if (/restore|factory/.test(signature)) return "undo";
|
||||
if (/(^|[^p])reset/.test(signature)) return "undo";
|
||||
if (/refresh|reload|retry|renew|update-now|updatenow/.test(signature)) return "refresh";
|
||||
if (/reset/.test(signature)) return "undo";
|
||||
if (/history|revision/.test(signature)) return "history";
|
||||
if (/revoke|remove|delete|discard|purge|kill/.test(signature)) return "trash";
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/preset/.test(signature)) return "layers";
|
||||
if (/new|add|create|generate|issue/.test(signature)) return "plus";
|
||||
if (/save/.test(signature)) return "save";
|
||||
if (/apply|submit|continue/.test(signature)) return "check";
|
||||
if (/edit/.test(signature)) return "edit";
|
||||
if (/pause/.test(signature)) return "pause";
|
||||
if (/pin|dock/.test(signature)) return "pin";
|
||||
if (/logs-pin|logpin|(^|[\s-])pin($|[\s-])|(^|[\s-])dock($|[\s-])/.test(signature)) return "pin";
|
||||
if (/start|run-operation|cleanup-start/.test(signature)) return "play";
|
||||
if (/logout|sign-out|lock/.test(signature)) return "logout";
|
||||
if (/login|sign-in/.test(signature)) return "login";
|
||||
if (/(^|[\s-])lock($|[\s-])/.test(signature)) return "lock";
|
||||
if (/certificate-api|api-token|token|secret|key/.test(signature)) return "key";
|
||||
if (/certificate|acme|tls/.test(signature)) return "certificate";
|
||||
if (/security|acl|policy|audit/.test(signature)) return "shield";
|
||||
@@ -192,8 +247,8 @@ function uiIconForTarget(target) {
|
||||
if (/log/.test(signature)) return "list";
|
||||
if (/docs|help|documentation/.test(signature)) return "book";
|
||||
if (/php|json|advanced|code/.test(signature)) return "code";
|
||||
if (/layout|preset|dashboard|overview/.test(signature)) return "grid";
|
||||
if (/about|info/.test(signature)) return "book";
|
||||
if (/layout|dashboard|overview/.test(signature)) return "grid";
|
||||
if (/about|info/.test(signature)) return "info";
|
||||
if (target.matches("a[href]")) return target.href?.startsWith(location.origin) ? "file" : "external";
|
||||
for (const [icon, keys] of UI_TRANSLATED_ICON_KEYS) if (uiIconMatchesTranslation(target, keys)) return icon;
|
||||
if (aria && typeof t === "function") {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Optional, account-scoped subtle animation of the sidebar r/p logo
|
||||
// (Settings → Appearance → Interface → Animate logo). Off by default.
|
||||
// Effects are short (≤1.2 s), run at random intervals, pause while the tab is
|
||||
// hidden and are skipped entirely when the system asks for reduced motion.
|
||||
const LOGO_ANIMATION_EFFECTS = Object.freeze(["spin-slash", "flip", "wave", "glow", "sheen", "glitch", "wobble"]);
|
||||
const LOGO_ANIMATION_DURATION_MS = 1400;
|
||||
let logoAnimationTimer = null, logoAnimationReset = null, logoAnimationLast = "", logoAnimationHoverBound = false;
|
||||
function logoAnimationMark() { return document.querySelector(".sidebar a.brand > .brand-mark") || document.querySelector("a.brand > .brand-mark"); }
|
||||
function logoAnimationReducedMotion() { return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true; }
|
||||
function prepareLogoAnimationMark(mark) {
|
||||
if (mark.dataset.logoPrepared === "true") return;
|
||||
// Wrap the bare "r" and "p" text nodes so each letter can be transformed on its own.
|
||||
for (const node of [...mark.childNodes]) {
|
||||
if (node.nodeType !== Node.TEXT_NODE || !node.textContent.trim()) continue;
|
||||
const letter = document.createElement("b"); letter.className = "brand-letter"; letter.textContent = node.textContent.trim();
|
||||
node.replaceWith(letter);
|
||||
}
|
||||
mark.dataset.logoPrepared = "true";
|
||||
}
|
||||
function playLogoAnimation(effect) {
|
||||
const mark = logoAnimationMark();
|
||||
if (!mark || document.documentElement.dataset.logoAnimation !== "true" || document.hidden || logoAnimationReducedMotion() || mark.dataset.logoEffect) return;
|
||||
prepareLogoAnimationMark(mark);
|
||||
const choices = LOGO_ANIMATION_EFFECTS.filter(name => name !== logoAnimationLast);
|
||||
const next = LOGO_ANIMATION_EFFECTS.includes(effect) ? effect : choices[Math.floor(Math.random() * choices.length)];
|
||||
logoAnimationLast = next; mark.dataset.logoEffect = next;
|
||||
clearTimeout(logoAnimationReset);
|
||||
logoAnimationReset = setTimeout(() => { delete mark.dataset.logoEffect; }, LOGO_ANIMATION_DURATION_MS);
|
||||
}
|
||||
function scheduleLogoAnimation() {
|
||||
clearTimeout(logoAnimationTimer);
|
||||
if (document.documentElement.dataset.logoAnimation !== "true") return;
|
||||
const delay = 9000 + Math.random() * 14000;
|
||||
logoAnimationTimer = setTimeout(() => { playLogoAnimation(); scheduleLogoAnimation(); }, delay);
|
||||
}
|
||||
function setLogoAnimationEnabled(value) {
|
||||
const enabled = value === true;
|
||||
document.documentElement.dataset.logoAnimation = String(enabled);
|
||||
const input = $("ui-logo-animation"); if (input) input.checked = enabled;
|
||||
const mark = logoAnimationMark();
|
||||
if (!enabled) {
|
||||
clearTimeout(logoAnimationTimer); clearTimeout(logoAnimationReset); logoAnimationTimer = null;
|
||||
if (mark) delete mark.dataset.logoEffect;
|
||||
return false;
|
||||
}
|
||||
if (mark) prepareLogoAnimationMark(mark);
|
||||
if (!logoAnimationHoverBound) {
|
||||
mark?.closest("a.brand")?.addEventListener("mouseenter", () => playLogoAnimation());
|
||||
logoAnimationHoverBound = !!mark;
|
||||
}
|
||||
if (!logoAnimationTimer) { scheduleLogoAnimation(); setTimeout(() => playLogoAnimation(), 600); }
|
||||
return true;
|
||||
}
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden && document.documentElement.dataset.logoAnimation === "true") scheduleLogoAnimation(); });
|
||||
window.rustProxyLogoAnimation = { setEnabled: setLogoAnimationEnabled, play: playLogoAnimation, effects: LOGO_ANIMATION_EFFECTS };
|
||||
@@ -13,7 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
])]);
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
chart_axes: false, interface_icons: false,
|
||||
chart_axes: false, interface_icons: false, logo_animation: false,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
|
||||
@@ -124,6 +124,7 @@ function applyAdminChartAxes(value) {
|
||||
return enabled;
|
||||
}
|
||||
function applyAdminInterfaceIcons(value) { return window.rustProxyIcons?.setEnabled?.(value === true) ?? false; }
|
||||
function applyAdminLogoAnimation(value) { return window.rustProxyLogoAnimation?.setEnabled?.(value === true) ?? false; }
|
||||
function applyAdminButtonStyle(value) {
|
||||
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
||||
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
||||
@@ -288,7 +289,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), logo_animation: applyAdminLogoAnimation(value.logo_animation), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
@@ -336,7 +337,7 @@ function updateAdminPreferences(patch) {
|
||||
const next = { ...current, ...patch };
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.logo_animation = next.logo_animation === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
@@ -344,7 +345,7 @@ function updateAdminPreferences(patch) {
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined); return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); applyAdminLogoAnimation(false); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function renderRefreshPreferences(value = REFRESH_DEFAULTS) {
|
||||
const refresh = normalizeRefreshPreferences(value);
|
||||
const switcher = $("refresh-live-enabled"); if (switcher) switcher.checked = refresh.live_enabled;
|
||||
@@ -428,6 +429,14 @@ $("ui-interface-icons")?.addEventListener("change", () => {
|
||||
applyAdminInterfaceIcons(previous);
|
||||
});
|
||||
});
|
||||
$("ui-logo-animation")?.addEventListener("change", () => {
|
||||
const control = $("ui-logo-animation"), previous = adminPreferences?.logo_animation === true, next = control.checked;
|
||||
applyAdminLogoAnimation(next); if (adminPreferences) adminPreferences.logo_animation = next;
|
||||
void saveAppearancePreference({ logo_animation: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.logo_animation = previous;
|
||||
applyAdminLogoAnimation(previous);
|
||||
});
|
||||
});
|
||||
$("ui-button-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
|
||||
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
|
||||
@@ -459,7 +468,7 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
|
||||
}
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, logo_animation: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
|
||||
@@ -869,4 +869,160 @@ body[data-density=compact] .page-intro .lead {
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Optional subtle logo animation (Settings → Appearance → Animate logo). */
|
||||
.brand-mark .brand-letter {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark {
|
||||
overflow: hidden;
|
||||
transition: box-shadow .4s ease;
|
||||
}
|
||||
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark>.brand-letter,
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark>span {
|
||||
display: inline-block;
|
||||
transform-origin: 50% 55%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="spin-slash"]>span {
|
||||
animation: brand-logo-spin 1.1s cubic-bezier(.55, 0, .25, 1);
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="flip"]>.brand-letter {
|
||||
animation: brand-logo-flip .9s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="flip"]>.brand-letter:last-child {
|
||||
animation-delay: .18s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>* {
|
||||
animation: brand-logo-wave .7s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>:nth-child(2) {
|
||||
animation-delay: .1s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>:nth-child(3) {
|
||||
animation-delay: .2s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="glow"] {
|
||||
animation: brand-logo-glow 1.3s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="sheen"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(105deg, transparent 30%, color-mix(in srgb, var(--logo-accent, var(--accent)) 28%, transparent) 50%, transparent 70%);
|
||||
animation: brand-logo-sheen 1s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="glitch"]>* {
|
||||
animation: brand-logo-glitch .45s steps(2, end);
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wobble"]>span {
|
||||
animation: brand-logo-wobble .9s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes brand-logo-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-flip {
|
||||
to {
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-wave {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 12px 1px color-mix(in srgb, var(--logo-accent, var(--accent)) 45%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-sheen {
|
||||
from {
|
||||
transform: translateX(-110%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(110%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-glitch {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-1px);
|
||||
text-shadow: 1px 0 var(--logo-secondary, var(--brand-secondary)), -1px 0 var(--logo-accent, var(--accent));
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateX(1px);
|
||||
text-shadow: -1px 0 var(--logo-secondary, var(--brand-secondary)), 1px 0 var(--logo-accent, var(--accent));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-wobble {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(-16deg);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect],
|
||||
a.brand>.brand-mark[data-logo-effect]>*,
|
||||
a.brand>.brand-mark[data-logo-effect]::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -82,12 +82,14 @@
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting,
|
||||
.appearance-logo-animation-setting,
|
||||
.appearance-chart-axes-setting {
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting>span,
|
||||
.appearance-logo-animation-setting>span,
|
||||
.appearance-chart-axes-setting>span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
@@ -1799,6 +1799,11 @@
|
||||
data-i18n="interfaceIconsHelp">Adds semantic SVG icons to
|
||||
navigation,
|
||||
actions, tabs and dialogs.</small></span></label>
|
||||
<label class="check appearance-logo-animation-setting"><input
|
||||
id="ui-logo-animation" type="checkbox" /><span><b
|
||||
data-i18n="logoAnimation">Animate logo</b><small
|
||||
data-i18n="logoAnimationHelp">Plays short, subtle animations of
|
||||
the sidebar logo from time to time and on hover.</small></span></label>
|
||||
<label><span data-i18n="cornerStyle">Corner rounding</span><select
|
||||
id="ui-corner-style">
|
||||
<option data-i18n="cornerStyleDefault" value="default">Original
|
||||
|
||||
Reference in new issue
Block a user