This commit is contained in:
Mateusz Gruszczyński committed 2026-11-07 15:26:06 +01:00
1 parent 6d7f152a0b
commit 064772765b
24 files changed
+418 -67

No files matched your search

+84 -29
View File
@@ -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") {
+55
View File
@@ -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 };
+14 -5
View File
@@ -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);