/* Tweak bootstrap 5 colors for better accessibility */
--bs-danger-rgb: 221, 0, 0;
--bs-success-rgb: 40, 139, 69;
+
+ /* Light mode (default) colors */
+ --rspamd-bg-card-gradient-start: #f9f9f9;
+ --rspamd-bg-card-gradient-end: #ededed;
+ --rspamd-text-primary: #000000;
+ --rspamd-text-secondary: #666666;
+ --rspamd-nav-active-bg: #e7e7e7;
+ --rspamd-sidebar-bg: #ffffee;
+ --rspamd-symbol-description-color: #484848;
+ --rspamd-symbol-negative-bg: #eef9e7;
+ --rspamd-symbol-positive-bg: #fbe9e5;
+ --rspamd-symbol-special-bg: #e2e9fe;
+ --rspamd-symbol-hover-bg: #e6e6e6;
+ --rspamd-table-danger-bg: #fbe9e5;
+ --rspamd-table-success-bg: #eef9e7;
+ --rspamd-table-warning-bg: #fff8e6;
+ --rspamd-bs-table-bg: #f8f9fa;
+
+ /* Light mode logo display rules */
+ --rspamd-display-logo-light: inline;
+ --rspamd-display-logo-dark: none;
+}
+
+[data-theme="dark"] {
+ /* Dark mode colors */
+ --rspamd-bg-card-gradient-start: #2a2a2a;
+ --rspamd-bg-card-gradient-end: #252525;
+ --rspamd-text-primary: #cccccc;
+ --rspamd-text-secondary: #b0b0b0;
+ --rspamd-nav-active-bg: #404040;
+ --rspamd-sidebar-bg: #444422;
+ --rspamd-symbol-description-color: #848484;
+ --rspamd-symbol-negative-bg: #1a3a1a;
+ --rspamd-symbol-positive-bg: #3a1a1a;
+ --rspamd-symbol-special-bg: #3a3a5a;
+ --rspamd-symbol-hover-bg: #000000;
+ --rspamd-table-danger-bg: #390b00;
+ --rspamd-table-success-bg: #102d00;
+ --rspamd-table-warning-bg: #43380b;
+ --rspamd-bs-table-bg: #272b2f;
+
+ /* Dark mode logo display rules */
+ --rspamd-display-logo-light: none;
+ --rspamd-display-logo-dark: inline;
}
/* bootstrap 4 overrides */
font-size: 85%;
}
.text-secondary {
- color: #666 !important;
+ color: var(--rspamd-text-secondary) !important;
}
.navbar {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 20px;
- border-bottom: 1px solid rgb(231 231 231);
}
.nav-pills .nav-link.active {
- background-color: #e7e7e7;
+ background-color: var(--rspamd-nav-active-bg);
}
.danger > td {
- background-color: #fbe9e5;
+ background-color: var(--rspamd-table-danger-bg);
}
.success > td {
- background-color: #eef9e7;
+ background-color: var(--rspamd-table-success-bg);
}
td.warning {
- background-color: #fff8e6;
+ background-color: var(--rspamd-table-warning-bg);
}
@media (max-width: 1199px) {
.navbar-collapse.order-3 {
border-bottom-right-radius:4px
}
+/* FooTable dark mode */
+[data-theme="dark"] .footable .pagination .footable-page-link,
+[data-theme="dark"] .footable .form-control {
+ color: #aaaaaa;
+ background-color: var(--bs-body-bg);
+ border-color: var(--bs-border-color)
+}
+[data-theme="dark"] .footable .btn-primary {
+ color: #ffffff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+[data-theme="dark"] .footable .btn-primary:hover {
+ background-color: #0b5ed7;
+ border-color: #0a58ca;
+}
+[data-theme="dark"] .footable .btn-default {
+ color: #ffffff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+[data-theme="dark"] .footable .btn-default:hover,
+[data-theme="dark"] .footable .open > .dropdown-toggle.btn-default {
+ background-color: #5c636a;
+ border-color: #565e64;
+}
+
+[data-theme="dark"] .footable .dropdown-menu {
+ background-color: var(--bs-body-bg);
+ border-color: var(--bs-dropdown-border-color);
+}
+[data-theme="dark"] .footable .dropdown-menu > li > a {
+ color: #aaaaaa;
+}
+[data-theme="dark"] .footable .dropdown-menu > li > a:hover {
+ background-color: var(--bs-dropdown-link-hover-bg);
+}
+
+[data-theme="dark"] .footable .pagination > li a:hover {
+ background-color: var(--bs-nav-link-disabled-color);
+ border-color: var(--bs-border-color);
+}
+[data-theme="dark"] .footable .pagination .footable-page-link:hover {
+ color: var(--bs-nav-link-hover-color);
+}
+[data-theme="dark"] .footable .pagination .footable-page.active .footable-page-link {
+ background-color: var(--rspamd-nav-active-bg);
+ color: var(--rspamd-text-primary);
+}
+
/* local overrides */
.navbar-brand > img {
height: 50px;
}
+.logo-light {
+ display: var(--rspamd-display-logo-light);
+}
+.logo-dark {
+ display: var(--rspamd-display-logo-dark);
+}
.btn-group > .btn.radius-right {
border-top-right-radius: var(--bs-btn-border-radius) !important;
border-bottom-right-radius: var(--bs-btn-border-radius) !important;
}
.alert {
margin-bottom: 4px;
- color: #c09853;
}
.alert.alert-modal {
top: 0;
display: inline-block;
padding-left: 35px;
}
-.alert-success {
+[data-theme="light"] .alert-success {
color: #468847;
background: #dff0d8;
border-color: #d6e9c6;
}
-.alert-danger {
+[data-theme="light"] .alert-danger {
color: #b94a48;
background: #f2dede;
border-color: #eed3d7;
}
-.alert-info {
+[data-theme="light"] .alert-warning {
+ color: #c09853;
+}
+[data-theme="light"] .alert-info {
color: #3a87ad;
background: #d9edf7;
border-color: #bce8f1;
.card-header,
.modal-header {
- background-color: #f3f3f3;
- background-image: linear-gradient(to bottom, #fdfdfd, #eaeaea);
+ background-image: linear-gradient(to bottom, var(--rspamd-bg-card-gradient-start), var(--rspamd-bg-card-gradient-end));
}
.card-header .h6 {
font-size: 0.857rem;
}
.stat-box {
- background-color: #f3f3f3;
- background-image: linear-gradient(to bottom, #f9f9f9, #ededed);
+ background-image: linear-gradient(to bottom, var(--rspamd-bg-card-gradient-start), var(--rspamd-bg-card-gradient-end));
line-height: 1;
}
.stat-box:not(.float-end) {
padding-right: 2px;
}
.symbol-default:hover {
- background-color: #e6e6e6;
+ background-color: var(--rspamd-symbol-hover-bg);
}
.symbol-negative.symbol-negative {
- background-color: #eef9e7;
+ background-color: var(--rspamd-symbol-negative-bg);
}
.symbol-positive.symbol-positive {
- background-color: #fbe9e5;
+ background-color: var(--rspamd-symbol-positive-bg);
}
.symbol-special {
- background-color: #e2e9fe;
+ background-color: var(--rspamd-symbol-special-bg);
}
.symbol-negative:hover {
- background-color: #dcf9d3;
+ background-color: var(--rspamd-symbol-hover-bg);
}
.symbol-positive:hover {
- background-color: #fbd6d1;
+ background-color: var(--rspamd-symbol-hover-bg);
}
.symbol-special:hover {
- background-color: #cddbff;
+ background-color: var(--rspamd-symbol-hover-bg);
}
/* For symbol description display on hover/focus */
}
.symbol-description {
display: none;
- color: #484848;
+ color: var(--rspamd-symbol-description-color);
}
.symbol-default:hover .symbol-description,
.symbol-default:focus .symbol-description {
text-align: left;
font-size: 12px;
z-index: 100;
+
+ --bs-table-bg: var(--rspamd-bs-table-bg);
}
#rrd-table td {
color: inherit;
transition-property: flex-basis, max-width, width;
}
+.sidebar,
+#sidebar-tab-left > a,
+#sidebar-tab-right > a {
+ background-color: var(--rspamd-sidebar-bg);
+}
.sidebar {
padding: 8px;
- background-color: #ffe;
transition: margin 0.3s ease;
}
.collapsed {
}
.sidebar-nav .nav-link,
.sidebar-nav .nav-link:hover {
- border: 1px solid #ddd;
+ border: 1px solid var(--bs-card-border-color);
}
#sidebar-tab-left > a,
#sidebar-tab-right > a {
- background-color: #ffe;
margin-left: 12px;
margin-right: 12px;
}
display: block;
}
#content {
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
+ border-left: 1px solid var(--bs-card-border-color);
+ border-right: 1px solid var(--bs-card-border-color);
}
#sidebar-tab-left {
display: flex;
border-bottom-right-radius: 3.5px;
}
#content {
- border-top: 1px solid #ddd;
- border-bottom: 1px solid #ddd;
+ border-top: 1px solid var(--bs-card-border-color);
+ border-bottom: 1px solid var(--bs-card-border-color);
}
#sidebar-tab-right {
bottom: 0;
filter: invert(1);
}
}
+
+/* Dark mode overrides for Bootstrap tables */
+[data-theme="dark"] .table {
+ --bs-table-color-state: var(--rspamd-text-primary);
+}
+
+/* Dark mode overrides for D3Evolution */
+[data-theme="dark"] .d3evolution .grid line {
+ stroke: #404040;
+}
+[data-theme="dark"] .d3evolution .cursor .background {
+ stroke: #1a1a1a;
+}
+[data-theme="dark"] .d3evolution .cursor .x.foreground {
+ stroke: #4db8ff;
+}
+[data-theme="dark"] .d3evolution .cursor circle.foreground {
+ stroke: white;
+}
+[data-theme="dark"] .d3evolution .chart-title,
+[data-theme="dark"] .d3evolution .axis,
+[data-theme="dark"] .d3evolution .legend,
+[data-theme="dark"] .d3evolution .y.label,
+[data-theme="dark"] .d3evolution .cursor-time {
+ fill: var(--rspamd-text-primary);
+}
+
+/* Dark mode overrides for D3Pie */
+[data-theme="dark"] .d3pie .chart-title,
+[data-theme="dark"] .d3pie .outer-label,
+[data-theme="dark"] .d3pie .total-text,
+[data-theme="dark"] .d3pie .total-value {
+ fill: var(--rspamd-text-primary);
+}
+[data-theme="dark"] .d3pie .slice-g.first-slice .inner-label {
+ fill: #666666;
+}
+[data-theme="dark"] .d3pie-tooltip {
+ background-color: rgb(80 80 80 / 80%);
+}
+
+/* Dark mode for NProgress */
+[data-theme="dark"] #nprogress .bar {
+ background: #4db8ff;
+}
<body>
<!-- .vw-100 and .pe-3 prevent navbar layout shift caused by scrollbar -->
-<nav class="navbar navbar-light bg-light navbar-expand-xl vw-100 pe-3 d-none" id="navBar">
+<nav class="navbar bg-body-tertiary border-bottom navbar-expand-xl vw-100 pe-3 d-none" id="navBar">
<div class="container-fluid">
<div class="navbar-header navbar-brand p-0">
- <img src="./img/rspamd_logo_navbar.png" alt="Rspamd">
+ <img class="logo-light" src="./img/rspamd_logo_navbar.png" alt="Rspamd">
+ <img class="logo-dark" src="./img/rspamd_logo_navbar_dark.png" alt="Rspamd">
</div>
<div class="collapse navbar-collapse order-3 order-xl-2 flex-grow-0">
<form class="my-2 me-auto">
</div>
</div>
<button class="btn btn-outline-secondary ms-2" id="disconnect" title="Disconnect"><i class="fas fa-power-off"></i></button>
+ <button class="btn btn-outline-secondary ms-2" id="theme-toggle" title="Toggle theme"><i class="fas fa-moon" id="theme-icon"></i></button>
<button class="btn btn-outline-secondary ms-2" id="settings" title="WebUI settings"><i class="fas fa-cog"></i></button>
<div class="d-none">
<div id="settings-popover">
</div>
</div>
+ <div class="card mt-1">
+ <div class="card-body">
+ <h6 class="card-title fw-bolder">Theme</h6>
+ <label class="ms-2">
+ <input type="radio" class="me-2" name="theme" value="light">
+ Light
+ </label>
+ <label class="ms-2">
+ <input type="radio" class="me-2" name="theme" value="dark">
+ Dark
+ </label>
+ <label class="ms-2">
+ <input type="radio" class="me-2" name="theme" value="auto" checked>
+ Auto
+ </label>
+ </div>
+ </div>
+
<div class="card mt-1">
<div class="card-body">
<h6 class="card-title fw-bolder">HTTP requests timeout, ms</h6>
</div>
<script>document.getElementById("loading").classList.remove("d-none");</script>
<div class="row position-absolute w-100 h-100 align-items-center text-center">
- <img class="img-fluid w-auto mh-100 mx-auto" src="./img/rspamd_logo_navbar.png" alt="Rspamd" />
+ <img class="img-fluid w-auto mh-100 mx-auto logo-light" src="./img/rspamd_logo_navbar.png" alt="Rspamd" />
+ <img class="img-fluid w-auto mh-100 mx-auto logo-dark" src="./img/rspamd_logo_navbar_dark.png" alt="Rspamd" />
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-server"></i></span>
<span class="h6 fw-bolder my-auto">Servers</span>
</table>
</div>
</div>
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-dice"></i></span>
<span class="h6 fw-bolder my-auto">Bayesian statistics</span>
</table>
</div>
</div>
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-hashtag"></i></span>
<span class="h6 fw-bolder my-auto">Fuzzy hashes</span>
</div>
</div>
<div class="col-lg-6">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-chart-pie"></i></span>
<span class="h6 fw-bolder my-auto">Statistics</span>
</div>
<div class="card-body">
<div class="row">
- <div class="bg-white w-auto mx-auto" id="chart"></div>
+ <div class="w-auto mx-auto" id="chart"></div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="throughput">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-chart-area"></i></span>
<span class="h6 fw-bolder my-auto">Throughput</span>
</div>
<div class="card-body text-center">
- <div class="d-inline-block bg-white">
+ <div class="d-inline-block">
<div class="row">
<div id="graph" class="mx-auto"></div>
</div>
<div id="summary-row" class="row">
<div class="col-fixed" id="rrd-pie"></div>
<div class="col-fluid">
- <table id="rrd-table" class="table table-light table-striped table-hover"></table>
+ <table id="rrd-table" class="table table-striped table-hover"></table>
<div id="rrd-table_toggle"></div>
<div id="rrd-total">Total messages: <span id="rrd-total-value"></span></div>
</div>
</div>
<div class="tab-pane" id="configuration">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-tasks"></i></span>
<span class="h6 fw-bolder my-auto">Actions</span>
</div>
</div>
</div>
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-list"></i></span>
<span class="h6 fw-bolder my-auto">Maps</span>
</div>
<div class="tab-pane" id="symbols">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-1 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-tasks"></i></span>
<span class="h6 fw-bolder my-auto ms-0">Symbols and rules</span>
</div>
<div class="tab-pane" id="scan">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-1 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-envelope"></i></span>
<span class="h6 fw-bolder my-auto">Scan suspected message</span>
<div class="card-body">
<div class="row g-3">
<div class="col-lg-auto d-flex">
- <div class="card bg-light shadow card-body card p-2">
+ <div class="card shadow card-body card p-2">
<p>Learn Bayesian classifier:</p>
<form>
<div class="d-flex flex-wrap flex-lg-column align-items-start align-items-lg-stretch gap-2">
</div>
</div>
<div class="col-lg d-flex">
- <div class="card bg-light shadow card-body p-2">
+ <div class="card shadow card-body p-2">
<p>Fuzzy hash storage management:</p>
<div class="row g-2 align-items-center">
<div class="col-auto d-flex align-items-center me-1">
</div>
</div>
- <div id="hash-card" class="card bg-light shadow my-3 d-none">
+ <div id="hash-card" class="card shadow my-3 d-none">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-hashtag"></i></span>
<span class="h6 fw-bolder my-auto">Fuzzy hashes</span>
</div>
</div>
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-1 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-eye"></i></span>
<span class="h6 fw-bolder my-auto ms-0">Scan results history</span>
</div>
<div class="tab-pane" id="selectors">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-envelope"></i></span>
<span class="h6 fw-bolder my-auto">Test Rspamd selectors</span>
<div class="card-body p-0">
<div class="row h-100 m-0" id="row-main">
<div class="col-lg-3 sidebar h-100" id="sidebar-left">
- <div class="p-0 table-responsive mh-100 bg-white">
+ <div class="p-0 table-responsive mh-100">
<table class="table table-sm small table-striped table-hover table-bordered mb-0" id="selectorsTable-extractors">
<thead><tr><th>Name</th><th>Description</th></tr></thead>
<tbody/>
<div class="tab-pane" id="history">
- <div class="card bg-light shadow my-3">
+ <div class="card shadow my-3">
<div class="card-header text-secondary py-1 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-eye"></i></span>
<span class="h6 fw-bolder my-auto ms-0">History</span>
</div>
</div>
</div>
- <div class="card bg-light shadow my-3 ro-hide" id="errors-history">
+ <div class="card shadow my-3 ro-hide" id="errors-history">
<div class="card-header text-secondary py-1 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-exclamation-triangle"></i></span>
<span class="h6 fw-bolder my-auto ms-0">Errors</span>
});
const $tr = $("<tr>").append($td).append($("<td>" + item.type + "</td>"));
- if (!item.loaded) $tr.addClass("table-light opacity-50");
+ if (!item.loaded) $tr.addClass("table-active opacity-50");
const $span = $('<span class="map-link">' + item.uri + "</span>").data("item", item);
$span.wrap("<td>").parent().appendTo($tr);
});
};
+ function updateThemeIcon(theme) {
+ const icon = $("#theme-icon");
+ icon.removeClass("fa-moon fa-sun fa-display");
+
+ switch (theme) {
+ case "light":
+ icon.addClass("fa-sun");
+ break;
+ case "dark":
+ icon.addClass("fa-moon");
+ break;
+ default:
+ icon.addClass("fa-display");
+ break;
+ }
+ }
(function initSettings() {
let selected_locale = null;
ajaxSetup(localStorage.getItem("ajax_timeout"), true);
$(historyCountSelector).val(parseInt(localStorage.getItem("historyCount"), 10) || historyCountDef);
+
+ // Restore theme selection
+ const savedTheme = localStorage.getItem("theme") || "auto";
+ $('.popover #settings-popover input:radio[name="theme"]').val([savedTheme]);
});
$(document).on("change", '.popover #settings-popover input:radio[name="locale"]', function () {
selected_locale = this.value;
custom_locale = $(localeTextbox).val();
validateLocale(true);
});
+ $(document).on("change", '.popover #settings-popover input:radio[name="theme"]', function () {
+ const theme = this.value;
+ if (window.rspamd && window.rspamd.theme) {
+ window.rspamd.theme.applyPreference(theme);
+ }
+ updateThemeIcon(theme || "auto");
+ });
+ updateThemeIcon(localStorage.getItem("theme") || "auto");
$(document).on("input", ajaxTimeoutBox, () => {
ajaxSetup($(ajaxTimeoutBox).val(), false, true);
});
tabClick("#autoRefresh");
});
+ $("#theme-toggle").on("click", (e) => {
+ e.preventDefault();
+ const currentTheme = localStorage.getItem("theme") || "light";
+ // eslint-disable-next-line no-useless-assignment
+ let newTheme = null;
+
+ // Cycle through: light -> dark -> auto -> light
+ switch (currentTheme) {
+ case "light":
+ newTheme = "dark";
+ break;
+ case "dark":
+ newTheme = "auto";
+ break;
+ default:
+ newTheme = "light";
+ break;
+ }
+
+ if (window.rspamd && window.rspamd.theme) {
+ window.rspamd.theme.applyPreference(newTheme);
+ }
+ updateThemeIcon(newTheme);
+
+ // Update radio button in settings popover if it's open
+ $('.popover #settings-popover input:radio[name="theme"]').val([newTheme]);
+ });
+
$("#selSrv").change(function () {
checked_server = this.value;
$("#selSrv [value=\"" + checked_server + "\"]").prop("checked", true);
/* global d3:writable, require, requirejs */ // eslint-disable-line no-unused-vars
+/**
+ * Theme initialization and management
+ *
+ * Initializes theme as early as possible, before loading any modules.
+ * Provides automatic theme detection and switching based on user preference
+ * and system settings. Invalid or missing preferences default to "auto" mode,
+ * which follows the system's color scheme preference.
+ *
+ * @exports window.rspamd.theme.applyPreference - Apply theme preference with listener management
+ * @exports window.rspamd.theme.getEffectiveTheme - Get effective theme for a given preference
+ */
+(function () {
+ "use strict";
+
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+
+ function normalizeTheme(value) {
+ const pref = (typeof value === "string") ? value.trim().toLowerCase() : "";
+ const allowed = new Set(["light", "dark", "auto"]);
+ return {
+ isAuto: !pref || !allowed.has(pref) || pref === "auto",
+ pref: pref
+ };
+ }
+
+ /**
+ * Get effective theme based on preference
+ * @param {string} themePref - Theme preference ("light", "dark", "auto", or invalid)
+ * @returns {string} Effective theme: "light" or "dark"
+ */
+ function getEffectiveTheme(themePref) {
+ const {isAuto, pref} = normalizeTheme(themePref);
+ // eslint-disable-next-line no-nested-ternary
+ return isAuto ? (mq.matches ? "dark" : "light") : pref;
+ }
+
+ function apply(theme) {
+ document.documentElement.setAttribute("data-bs-theme", theme);
+ document.body.setAttribute("data-theme", theme);
+ }
+
+ function handler() {
+ apply(getEffectiveTheme(localStorage.getItem("theme")));
+ }
+
+ // Apply theme immediately on page load
+ handler();
+
+ // Set up listener for system theme changes if in auto mode
+ const {isAuto: initialIsAuto} = normalizeTheme(localStorage.getItem("theme"));
+ if (initialIsAuto && typeof mq.addEventListener === "function") {
+ mq.addEventListener("change", handler);
+ }
+
+
+ // Export theme API to window.rspamd namespace
+ if (!window.rspamd) window.rspamd = {};
+
+ /**
+ * Theme management API
+ * @namespace
+ *
+ * @property {Function} applyPreference - Apply theme preference (handles auto mode and listener management)
+ * @property {Function} getEffectiveTheme - Get effective theme for a given preference
+ */
+ window.rspamd.theme = {
+
+ /**
+ * Apply theme preference (handles auto mode and listener management)
+ * @param {string} themePref - Theme preference to apply
+ */
+ applyPreference: (themePref) => {
+ localStorage.setItem("theme", themePref);
+ apply(getEffectiveTheme(themePref));
+
+ const {isAuto} = normalizeTheme(themePref);
+ mq.removeEventListener("change", handler);
+ if (isAuto) {
+ mq.addEventListener("change", handler);
+ }
+ },
+
+ getEffectiveTheme: getEffectiveTheme
+ };
+}());
+
requirejs.config({
baseUrl: "js/lib",
paths: {