From c3c8eccef5053fe147481ed2ab491c0cfa0460e5 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Tue, 14 Apr 2026 08:26:14 -0600 Subject: [PATCH 01/16] Add Home Assistant dark theme support --- saltgui/index.html | 2 + saltgui/static/scripts/ha-theme.js | 67 ++++++++++ saltgui/static/stylesheets/ha-theme.css | 169 ++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 saltgui/static/scripts/ha-theme.js create mode 100644 saltgui/static/stylesheets/ha-theme.css diff --git a/saltgui/index.html b/saltgui/index.html index c455b85af..3a2473384 100644 --- a/saltgui/index.html +++ b/saltgui/index.html @@ -3,6 +3,7 @@ SaltGUI + @@ -17,6 +18,7 @@ + diff --git a/saltgui/static/scripts/ha-theme.js b/saltgui/static/scripts/ha-theme.js new file mode 100644 index 000000000..97ab77b67 --- /dev/null +++ b/saltgui/static/scripts/ha-theme.js @@ -0,0 +1,67 @@ +(function () { + const root = document.documentElement; + const mediaQuery = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null; + + function hintText() { + const values = []; + + try { + const parentDoc = window.parent && window.parent !== window ? window.parent.document : null; + if (parentDoc) { + values.push(parentDoc.documentElement.getAttribute("data-theme") || ""); + values.push(parentDoc.documentElement.getAttribute("theme") || ""); + values.push(parentDoc.documentElement.className || ""); + values.push(parentDoc.body ? parentDoc.body.className || "" : ""); + } + } catch (_error) { + // Access to the parent frame can fail outside Home Assistant ingress. + } + + return values.join(" ").toLowerCase(); + } + + function wantsDarkTheme() { + const hints = hintText(); + + if (/(^|\s)(light)(\s|$)/.test(hints)) { + return false; + } + + if (/(^|\s)(dark|night)(\s|$)/.test(hints)) { + return true; + } + + return mediaQuery ? mediaQuery.matches : false; + } + + function applyTheme() { + root.dataset.theme = wantsDarkTheme() ? "dark" : "light"; + } + + applyTheme(); + + if (mediaQuery && mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", applyTheme); + } else if (mediaQuery && mediaQuery.addListener) { + mediaQuery.addListener(applyTheme); + } + + try { + const parentDoc = window.parent && window.parent !== window ? window.parent.document : null; + if (parentDoc) { + const observer = new MutationObserver(applyTheme); + observer.observe(parentDoc.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme", "theme"], + }); + if (parentDoc.body) { + observer.observe(parentDoc.body, { + attributes: true, + attributeFilter: ["class", "data-theme", "theme"], + }); + } + } + } catch (_error) { + // Ignore parent observer failures outside Home Assistant ingress. + } +})(); diff --git a/saltgui/static/stylesheets/ha-theme.css b/saltgui/static/stylesheets/ha-theme.css new file mode 100644 index 000000000..788e1371f --- /dev/null +++ b/saltgui/static/stylesheets/ha-theme.css @@ -0,0 +1,169 @@ +:root[data-theme="dark"] { + color-scheme: dark; + --salt-accent: #22c7bd; + --salt-accent-strong: #48dfd4; + --salt-bg: #071318; + --salt-bg-elevated: #0d1f26; + --salt-bg-panel: #122a33; + --salt-bg-soft: #173540; + --salt-bg-hover: rgba(34, 199, 189, 0.12); + --salt-text: #e6f7f5; + --salt-text-muted: #9ab8b5; + --salt-text-soft: #c4d7d5; + --salt-border: rgba(154, 184, 181, 0.18); + --salt-shadow: 0 18px 40px rgba(0, 0, 0, 0.38); + --salt-warning: #f4c14f; + --salt-danger: #ff7f8a; + --salt-link: #8fc4ff; + --salt-code: #0b171d; +} + +:root[data-theme="dark"] body, +:root[data-theme="dark"] #page-login { + background-color: var(--salt-bg); + color: var(--salt-text); +} + +:root[data-theme="dark"] header { + background-color: var(--salt-bg-elevated); + border-top-color: var(--salt-accent); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18); +} + +:root[data-theme="dark"] .logo, +:root[data-theme="dark"] .docu, +:root[data-theme="dark"] h1, +:root[data-theme="dark"] pre .minion-id, +:root[data-theme="dark"] table tr th, +:root[data-theme="dark"] .small-button:hover, +:root[data-theme="dark"] .menu-item:hover, +:root[data-theme="dark"] .run-command-button:hover, +:root[data-theme="dark"] .docu:hover { + color: var(--salt-accent); +} + +:root[data-theme="dark"] #button-manual-run { + fill: var(--salt-accent); +} + +:root[data-theme="dark"] .docu, +:root[data-theme="dark"] .msg, +:root[data-theme="dark"] .no-job-status, +:root[data-theme="dark"] .no-job-details, +:root[data-theme="dark"] .jobs td .time, +:root[data-theme="dark"] .attribution, +:root[data-theme="dark"] #login-panel h1, +:root[data-theme="dark"] #login-panel select option#eauth-default { + color: var(--salt-text-muted); +} + +:root[data-theme="dark"] .panel, +:root[data-theme="dark"] #login-panel, +:root[data-theme="dark"] .popup .run-command, +:root[data-theme="dark"] .dropdown-content, +:root[data-theme="dark"] .run-command-button .menu-dropdown-content { + background-color: var(--salt-bg-panel); + color: var(--salt-text); + border: 1px solid var(--salt-border); + box-shadow: var(--salt-shadow); +} + +:root[data-theme="dark"] .small-button, +:root[data-theme="dark"] .dropdown-content, +:root[data-theme="dark"] .run-command-button .menu-dropdown-content, +:root[data-theme="dark"] input[type="text"], +:root[data-theme="dark"] input[type="password"], +:root[data-theme="dark"] select { + background-color: var(--salt-bg-soft); + color: var(--salt-text); + border-color: var(--salt-border); +} + +:root[data-theme="dark"] input[type="text"]:focus, +:root[data-theme="dark"] input[type="password"]:focus, +:root[data-theme="dark"] select:focus { + border-color: var(--salt-accent); + outline: none; +} + +:root[data-theme="dark"] input[type="submit"] { + background-color: var(--salt-accent); + color: #052228; + box-shadow: 0 10px 30px rgba(34, 199, 189, 0.24); +} + +:root[data-theme="dark"] input[type="submit"]:hover { + background-color: var(--salt-accent-strong); +} + +:root[data-theme="dark"] .menu-item:hover, +:root[data-theme="dark"] .run-command-button .menu-dropdown-content div.run-command-button:hover, +:root[data-theme="dark"] .highlight-rows tbody tr:hover, +:root[data-theme="dark"] .tooltip:hover, +:root[data-theme="dark"] pre.output .tooltip:hover { + background: var(--salt-bg-hover); +} + +:root[data-theme="dark"] #warning { + background: rgba(244, 193, 79, 0.18); + border-top: 1px solid rgba(244, 193, 79, 0.35); + color: #ffe3a2; +} + +:root[data-theme="dark"] #motd, +:root[data-theme="dark"] #motdtxt, +:root[data-theme="dark"] #motdhtml { + color: var(--salt-text-soft); +} + +:root[data-theme="dark"] table thead th, +:root[data-theme="dark"] table tbody td { + color: var(--salt-text-soft); + border-bottom-color: var(--salt-border); +} + +:root[data-theme="dark"] table thead th { + border-bottom-color: rgba(34, 199, 189, 0.45); +} + +:root[data-theme="dark"] td.address > span, +:root[data-theme="dark"] pre.output a, +:root[data-theme="dark"] form a, +:root[data-theme="dark"] pre a { + color: var(--salt-link); +} + +:root[data-theme="dark"] .run-command-button, +:root[data-theme="dark"] .jobs td .target, +:root[data-theme="dark"] .jobs td .function, +:root[data-theme="dark"] .search-error, +:root[data-theme="dark"] .offline { + color: var(--salt-text); +} + +:root[data-theme="dark"] pre.output, +:root[data-theme="dark"] code, +:root[data-theme="dark"] tt { + background-color: var(--salt-code); + color: var(--salt-text); +} + +:root[data-theme="dark"] .tooltip > .tooltip-text, +:root[data-theme="dark"] pre.output .tooltip > .tooltip-text { + background-color: var(--salt-accent); + color: #062126; +} + +:root[data-theme="dark"] .tooltip > .tooltip-text::after, +:root[data-theme="dark"] pre.output .tooltip > .tooltip-text::after { + border-color: var(--salt-accent) transparent transparent transparent; +} + +:root[data-theme="dark"] .tooltip > .tooltip-text-error-bottom-left { + background-color: var(--salt-danger); + color: #24050a; +} + +:root[data-theme="dark"] .tooltip > .tooltip-text-error-bottom-left::after { + border-color: var(--salt-danger) transparent transparent; +} From 04dd925b5cced01241c3e5a740d55bb5787a6196 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Tue, 14 Apr 2026 16:44:14 -0600 Subject: [PATCH 02/16] Refactor configurable SaltGUI theming --- docs/README.md | 15 ++ saltgui/index.html | 8 +- saltgui/static/scripts/Character.js | 2 +- saltgui/static/scripts/Router.js | 2 +- saltgui/static/scripts/ha-theme.js | 67 ------- .../scripts/output/OutputDocumentation.js | 4 +- .../output/OutputHighstateSummaryOriginal.js | 12 +- saltgui/static/scripts/panels/HighState.js | 2 - saltgui/static/scripts/panels/Jobs.js | 11 +- saltgui/static/scripts/panels/JobsDetails.js | 12 +- saltgui/static/scripts/panels/Login.js | 29 +-- saltgui/static/scripts/panels/Options.js | 23 +++ saltgui/static/scripts/panels/Stats.js | 2 +- saltgui/static/scripts/theme.js | 107 +++++++++++ saltgui/static/stylesheets/beacons.css | 2 +- saltgui/static/stylesheets/controls.css | 13 +- saltgui/static/stylesheets/dropdown.css | 17 +- saltgui/static/stylesheets/ha-theme.css | 169 ------------------ saltgui/static/stylesheets/job.css | 2 +- saltgui/static/stylesheets/login.css | 12 +- saltgui/static/stylesheets/main.css | 52 +++--- saltgui/static/stylesheets/page.css | 59 +++--- saltgui/static/stylesheets/schedules.css | 2 +- saltgui/static/stylesheets/theme.css | 153 ++++++++++++++++ saltgui/static/stylesheets/tooltip.css | 21 ++- 25 files changed, 437 insertions(+), 361 deletions(-) delete mode 100644 saltgui/static/scripts/ha-theme.js create mode 100644 saltgui/static/scripts/theme.js delete mode 100644 saltgui/static/stylesheets/ha-theme.css create mode 100644 saltgui/static/stylesheets/theme.css diff --git a/docs/README.md b/docs/README.md index e343ff6a3..aee197dd7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -200,6 +200,21 @@ In all cases, a tooltip is added to a date+time field that shows the full repres When using very old browsers, the required date/time functions may not be present. In that case SaltGUI reverts to simply displaying the reported time from the Salt system. The tooltip is then not shown. +## Theme +SaltGUI can follow the browser preference automatically, or it can be forced to a specific theme by adding the following parameter to salt master configuration file `/etc/salt/master`. +e.g.: +``` +saltgui_theme: auto +``` + +Allowed values are `auto`, `light`, and `dark`. +With `auto`, SaltGUI follows the browser color-scheme preference and also uses theme hints from an embedding parent frame when those are available. +With `light` and `dark`, SaltGUI uses the selected theme unconditionally. + +The current value is also visible on the Settings page. +Changes made there are session-only and do not modify `/etc/salt/master`. + + ## Templates SaltGUI supports command templates for easier command entry into the command-box. The menu item for that becomes visible there when you define one or more templates diff --git a/saltgui/index.html b/saltgui/index.html index 3a2473384..22ff90cc2 100644 --- a/saltgui/index.html +++ b/saltgui/index.html @@ -3,7 +3,8 @@ SaltGUI - + + @@ -18,7 +19,6 @@ - @@ -28,8 +28,8 @@

- - >_ + + >_

diff --git a/saltgui/static/scripts/Character.js b/saltgui/static/scripts/Character.js index 46243d45e..4cfb7aa53 100644 --- a/saltgui/static/scripts/Character.js +++ b/saltgui/static/scripts/Character.js @@ -64,6 +64,6 @@ export class Character { } static buttonInText (txt) { - return " " + txt + " "; + return " " + txt + " "; } } diff --git a/saltgui/static/scripts/Router.js b/saltgui/static/scripts/Router.js index 017b57a4a..60fc871a3 100644 --- a/saltgui/static/scripts/Router.js +++ b/saltgui/static/scripts/Router.js @@ -272,7 +272,7 @@ export class Router { // perform the hiding/showing for (let nr = 1; nr <= 2; nr++) { const item = document.getElementById("button-" + pPage.path + nr); - item.style.color = !visible && hasVisibleChild ? "lightgray" : "black"; + item.classList.toggle("menu-item-dimmed", !visible && hasVisibleChild); if (!visible) { // hide the shortcut indicator item.classList.remove("menu-item-first-letter"); diff --git a/saltgui/static/scripts/ha-theme.js b/saltgui/static/scripts/ha-theme.js deleted file mode 100644 index 97ab77b67..000000000 --- a/saltgui/static/scripts/ha-theme.js +++ /dev/null @@ -1,67 +0,0 @@ -(function () { - const root = document.documentElement; - const mediaQuery = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null; - - function hintText() { - const values = []; - - try { - const parentDoc = window.parent && window.parent !== window ? window.parent.document : null; - if (parentDoc) { - values.push(parentDoc.documentElement.getAttribute("data-theme") || ""); - values.push(parentDoc.documentElement.getAttribute("theme") || ""); - values.push(parentDoc.documentElement.className || ""); - values.push(parentDoc.body ? parentDoc.body.className || "" : ""); - } - } catch (_error) { - // Access to the parent frame can fail outside Home Assistant ingress. - } - - return values.join(" ").toLowerCase(); - } - - function wantsDarkTheme() { - const hints = hintText(); - - if (/(^|\s)(light)(\s|$)/.test(hints)) { - return false; - } - - if (/(^|\s)(dark|night)(\s|$)/.test(hints)) { - return true; - } - - return mediaQuery ? mediaQuery.matches : false; - } - - function applyTheme() { - root.dataset.theme = wantsDarkTheme() ? "dark" : "light"; - } - - applyTheme(); - - if (mediaQuery && mediaQuery.addEventListener) { - mediaQuery.addEventListener("change", applyTheme); - } else if (mediaQuery && mediaQuery.addListener) { - mediaQuery.addListener(applyTheme); - } - - try { - const parentDoc = window.parent && window.parent !== window ? window.parent.document : null; - if (parentDoc) { - const observer = new MutationObserver(applyTheme); - observer.observe(parentDoc.documentElement, { - attributes: true, - attributeFilter: ["class", "data-theme", "theme"], - }); - if (parentDoc.body) { - observer.observe(parentDoc.body, { - attributes: true, - attributeFilter: ["class", "data-theme", "theme"], - }); - } - } - } catch (_error) { - // Ignore parent observer failures outside Home Assistant ingress. - } -})(); diff --git a/saltgui/static/scripts/output/OutputDocumentation.js b/saltgui/static/scripts/output/OutputDocumentation.js index 839c86bc0..5def31632 100644 --- a/saltgui/static/scripts/output/OutputDocumentation.js +++ b/saltgui/static/scripts/output/OutputDocumentation.js @@ -270,12 +270,12 @@ export class OutputDocumentation { // replace ``......`` // e.g. in "sys.doc state.apply" // named groups are only introduced in ES9/2018 - out = out.replace(/``([^`]*)``/g, "$1"); + out = out.replace(/``([^`]*)``/g, "$1"); // replace `......` // e.g. in "sys.doc state.apply" // named groups are only introduced in ES9/2018 - out = out.replace(/`([^`]*)`/g, "$1"); + out = out.replace(/`([^`]*)`/g, "$1"); // remove whitespace at end of lines out = out.replace(/ *\n/gm, ""); diff --git a/saltgui/static/scripts/output/OutputHighstateSummaryOriginal.js b/saltgui/static/scripts/output/OutputHighstateSummaryOriginal.js index 5aada5b86..cc9f18cf2 100644 --- a/saltgui/static/scripts/output/OutputHighstateSummaryOriginal.js +++ b/saltgui/static/scripts/output/OutputHighstateSummaryOriginal.js @@ -11,8 +11,7 @@ export class OutputHighstateSummaryOriginal { let txt = "\nSummary for " + pMinionId; txt += "\n------------"; - const summarySpan = Utils.createSpan("", txt); - summarySpan.style.color = "aqua"; + const summarySpan = Utils.createSpan("text-info", txt); pDiv.append(summarySpan); const total = pSucceeded + pSkipped + pFailed; @@ -24,7 +23,6 @@ export class OutputHighstateSummaryOriginal { if (pChangesSummary > 0) { txt = " ("; const oSpan = Utils.createSpan("", txt); - oSpan.style.color = "white"; pDiv.append(oSpan); txt = "changed=" + pChangesSummary; @@ -33,7 +31,6 @@ export class OutputHighstateSummaryOriginal { txt = ")"; const cSpan = Utils.createSpan("", txt); - cSpan.style.color = "white"; pDiv.append(cSpan); } @@ -42,7 +39,7 @@ export class OutputHighstateSummaryOriginal { if (pFailed > 0) { failedSpan.classList.add("task-failure"); } else { - failedSpan.style.color = "aqua"; + failedSpan.classList.add("text-info"); } pDiv.append(failedSpan); @@ -57,7 +54,7 @@ export class OutputHighstateSummaryOriginal { if (pFailed > 0) { failureSpan.classList.add("task-failure"); } else { - failureSpan.style.color = "aqua"; + failureSpan.classList.add("text-info"); } pDiv.append(failureSpan); } @@ -65,8 +62,7 @@ export class OutputHighstateSummaryOriginal { txt = "\n------------"; txt += "\nTotal states run: " + total; txt += "\nTotal run time: " + Output.getDuration(pTotalMilliSeconds); - const totalsSpan = Utils.createSpan("", txt); - totalsSpan.style.color = "aqua"; + const totalsSpan = Utils.createSpan("text-info", txt); pDiv.append(totalsSpan); pDiv.style.cursor = "pointer"; } diff --git a/saltgui/static/scripts/panels/HighState.js b/saltgui/static/scripts/panels/HighState.js index 15ebe9394..2bef3bd16 100644 --- a/saltgui/static/scripts/panels/HighState.js +++ b/saltgui/static/scripts/panels/HighState.js @@ -440,7 +440,6 @@ export class HighStatePanel extends Panel { // for information (keys.length > this._maxHighstateStates) const span = Utils.createSpan("task"); - span.style.backgroundColor = "black"; // this also sets the span's class(es) Output._setTaskToolTip(span, data); @@ -516,7 +515,6 @@ export class HighStatePanel extends Panel { // remove the priority indicator from the key const itemSpan = Utils.createSpan(["tasksummary", className], character); - itemSpan.style.backgroundColor = "black"; summarySpan.append(itemSpan); Utils.addToolTip(itemSpan, className.replace("task-", "").replace("-", " with ")); } diff --git a/saltgui/static/scripts/panels/Jobs.js b/saltgui/static/scripts/panels/Jobs.js index 081a120b7..bf4c9b0d9 100644 --- a/saltgui/static/scripts/panels/Jobs.js +++ b/saltgui/static/scripts/panels/Jobs.js @@ -296,18 +296,19 @@ export class JobsPanel extends Panel { // This element only exists when the user happens to look at the output of that jobId. const spans = this.div.querySelectorAll("#status" + jid); for (const span of spans) { - let oldLevel = span.dataset.level; - if (oldLevel === undefined) { + let oldLevel = Number(span.dataset.level); + if (Number.isNaN(oldLevel)) { oldLevel = 0; } if (newLevel > oldLevel) { span.dataset.level = newLevel; + span.classList.remove("text-success", "text-warning", "text-error"); if (newLevel === 1) { - span.style.color = "green"; + span.classList.add("text-success"); } else if (newLevel === 2) { - span.style.color = "orange"; + span.classList.add("text-warning"); } else if (newLevel === 3) { - span.style.color = "red"; + span.classList.add("text-error"); } } span.style.removeProperty("display"); diff --git a/saltgui/static/scripts/panels/JobsDetails.js b/saltgui/static/scripts/panels/JobsDetails.js index aab538a7e..7dcd9ed07 100644 --- a/saltgui/static/scripts/panels/JobsDetails.js +++ b/saltgui/static/scripts/panels/JobsDetails.js @@ -266,16 +266,16 @@ export class JobsDetailsPanel extends JobsPanel { if (pData.Minions.length === 0) { detailsHTML += ""; } else if (keyCount === pData.Minions.length) { - detailsHTML += ""; + detailsHTML += ""; } else { - detailsHTML += ""; + detailsHTML += ""; } detailsHTML += Utils.txtZeroOneMany(keyCount, "no results", "{0} result", "{0} results"); detailsHTML += ""; if (keyCount < pData.Minions.length) { - detailsHTML += ", "; + detailsHTML += ", "; detailsHTML += pData.Minions.length - keyCount; detailsHTML += " missing"; } @@ -299,13 +299,13 @@ export class JobsDetailsPanel extends JobsPanel { for (const key of keys) { detailsHTML += ", "; if (key === "0-0") { - detailsHTML += ""; + detailsHTML += ""; detailsHTML += Utils.txtZeroOneMany(summary[key], "", "{0} success", "{0} successes"); } else if (key.startsWith("0-")) { - detailsHTML += ""; + detailsHTML += ""; detailsHTML += Utils.txtZeroOneMany(summary[key], "", "{0} success", "{0} successes"); } else if (key.startsWith("1-")) { - detailsHTML += ""; + detailsHTML += ""; detailsHTML += Utils.txtZeroOneMany(summary[key], "", "{0} failure", "{0} failures"); } else { // if (key.startsWith("2-")) diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 4d324c991..5db672aeb 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -260,21 +260,21 @@ export class LoginPanel extends Panel { break; case "no-session": // gray because we cannot prove that the user was/wasnt logged in - this._showNoticeText("gray", "Not logged in", "notice_not_logged_in"); + this._showNoticeText("var(--color-notice-muted)", "Not logged in", "notice_not_logged_in"); break; case "session-cancelled": - this._showNoticeText("#F44336", "Session cancelled", "notice-session-cancelled"); + this._showNoticeText("var(--color-notice-danger)", "Session cancelled", "notice-session-cancelled"); break; case "session-expired": - this._showNoticeText("#F44336", "Session expired", "notice-session-expired"); + this._showNoticeText("var(--color-notice-danger)", "Session expired", "notice-session-expired"); break; case "logout": // gray because this is the result of a user action - this._showNoticeText("gray", "Logout", "notice_logout"); + this._showNoticeText("var(--color-notice-muted)", "Logout", "notice_logout"); break; default: // should not occur - this._showNoticeText("#F44336", reason, "notice_other:" + reason); + this._showNoticeText("var(--color-notice-danger)", reason, "notice_other:" + reason); } this._enableLoginControls(true); @@ -303,7 +303,7 @@ export class LoginPanel extends Panel { } _onLoginSuccess () { - this._showNoticeText("#4CAF50", "Please wait" + Character.HORIZONTAL_ELLIPSIS, "notice_please_wait"); + this._showNoticeText("var(--color-text-accent)", "Please wait" + Character.HORIZONTAL_ELLIPSIS, "notice_please_wait"); Utils.setStorageItem("local", "salt-motd-txt", ""); Utils.setStorageItem("local", "salt-motd-html", ""); @@ -390,6 +390,13 @@ export class LoginPanel extends Panel { static _handleLoginWheelConfigValues (pWheelConfigValuesData) { const wheelConfigValuesData = pWheelConfigValuesData.return[0].data.return; + const theme = wheelConfigValuesData.saltgui_theme || "auto"; + + Utils.setStorageItem("session", "theme", theme); + Utils.setStorageItem("local", "theme_default", theme); + if (globalThis.SaltGUITheme && globalThis.SaltGUITheme.applyTheme) { + globalThis.SaltGUITheme.applyTheme(); + } // store for later use @@ -517,19 +524,19 @@ export class LoginPanel extends Panel { _onLoginFailure (error) { if (typeof error === "string") { // something detected before trying to login - this._showNoticeText("#F44336", error, "notice_login_string_error"); + this._showNoticeText("var(--color-notice-danger)", error, "notice_login_string_error"); } else if (error && error.status === 503) { // Service Unavailable // e.g. salt-api running but salt-master not running - this._showNoticeText("#F44336", error.message, "notice_login_service_unavailable"); + this._showNoticeText("var(--color-notice-danger)", error.message, "notice_login_service_unavailable"); } else if (error && error.status === -1) { // No permissions: login valid, but no api functions executable // e.g. PAM says OK and /etc/salt/master says NO - this._showNoticeText("#F44336", error.message, "notice_login_other_error"); + this._showNoticeText("var(--color-notice-danger)", error.message, "notice_login_other_error"); } else if (error.toString().startsWith("TypeError: NetworkError")) { - this._showNoticeText("#F44336", "Network Error", "notice_login_other_error"); + this._showNoticeText("var(--color-notice-danger)", "Network Error", "notice_login_other_error"); } else { - this._showNoticeText("#F44336", "Authentication failed", "notice_auth_failed"); + this._showNoticeText("var(--color-notice-danger)", "Authentication failed", "notice_auth_failed"); } this._enableLoginControls(true); diff --git a/saltgui/static/scripts/panels/Options.js b/saltgui/static/scripts/panels/Options.js index 7b331c7dc..eb16c9522 100644 --- a/saltgui/static/scripts/panels/Options.js +++ b/saltgui/static/scripts/panels/Options.js @@ -108,6 +108,10 @@ export class OptionsPanel extends Panel { "tooltip-mode", "saltgui", "full", [["mode", "full", "simple", "none"]] ], + [ + "theme", "saltgui", "auto", + [["theme", "auto", "light", "dark"]] + ], /* last because it might be very long */ ["custom-command-help", "saltgui", "(none)"] @@ -198,6 +202,10 @@ export class OptionsPanel extends Panel { radio.addEventListener("change", () => { this._newFullReturn(); }); + } else if (pName === "theme") { + radio.addEventListener("change", () => { + this._newTheme(); + }); } else if (pName === "use-cache-for-grains") { radio.addEventListener("change", () => { this._newUseCacheForGrains(); @@ -583,4 +591,19 @@ export class OptionsPanel extends Panel { fullReturnTd.innerText = value; Utils.setStorageItem("session", "full_return", value); } + + _newTheme () { + let value = ""; + /* eslint-disable curly */ + if (this._isSelected("theme", "theme", "auto")) value = "auto"; + if (this._isSelected("theme", "theme", "light")) value = "light"; + if (this._isSelected("theme", "theme", "dark")) value = "dark"; + /* eslint-enable curly */ + const themeTd = this.div.querySelector("#option-theme-value"); + themeTd.innerText = value; + Utils.setStorageItem("session", "theme", value); + if (globalThis.SaltGUITheme && globalThis.SaltGUITheme.applyTheme) { + globalThis.SaltGUITheme.applyTheme(); + } + } } diff --git a/saltgui/static/scripts/panels/Stats.js b/saltgui/static/scripts/panels/Stats.js index a51bd5183..445ebeb57 100644 --- a/saltgui/static/scripts/panels/Stats.js +++ b/saltgui/static/scripts/panels/Stats.js @@ -69,7 +69,7 @@ export class StatsPanel extends Panel { _handleStats (pStatsData) { if (this.showErrorRowInstead(pStatsData)) { - this.statsTd.innerHTML = "this error is typically caused by using the collect_stats: True setting in the master configuration file, which is broken in at least the recent versions of salt-api"; + this.statsTd.innerHTML = "this error is typically caused by using the collect_stats: True setting in the master configuration file, which is broken in at least the recent versions of salt-api"; window.clearInterval(this.updateStatsInterval); this.updateStatsInterval = null; return; diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js new file mode 100644 index 000000000..82e2575ea --- /dev/null +++ b/saltgui/static/scripts/theme.js @@ -0,0 +1,107 @@ +(function () { + const context = globalThis; + const root = document.documentElement; + const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; + + function getStoredTheme() { + try { + const sessionTheme = context.sessionStorage ? context.sessionStorage.getItem("theme") : null; + if (sessionTheme) { + return sessionTheme; + } + const defaultTheme = context.localStorage ? context.localStorage.getItem("theme_default") : null; + if (defaultTheme) { + return defaultTheme; + } + } catch (_error) { + // Storage access can fail in restricted browser environments. + } + + return "auto"; + } + + function getConfiguredTheme() { + const theme = (getStoredTheme() || "auto").toLowerCase(); + if (theme === "light" || theme === "dark") { + return theme; + } + return "auto"; + } + + function getParentHints() { + const values = []; + + try { + const parentDoc = context.parent && context.parent !== context ? context.parent.document : null; + if (parentDoc) { + values.push(parentDoc.documentElement.dataset.theme || ""); + values.push(parentDoc.documentElement.getAttribute("theme") || ""); + values.push(parentDoc.documentElement.className || ""); + values.push(parentDoc.body ? parentDoc.body.className || "" : ""); + } + } catch (_error) { + // Access to the parent frame can fail outside an embedded environment. + } + + return values.join(" ").toLowerCase(); + } + + function wantsDarkTheme(configuredTheme) { + if (configuredTheme === "dark") { + return true; + } + if (configuredTheme === "light") { + return false; + } + + const hints = getParentHints(); + if (/(^|\s)(light)(\s|$)/.test(hints)) { + return false; + } + if (/(^|\s)(dark|night)(\s|$)/.test(hints)) { + return true; + } + + return mediaQuery ? mediaQuery.matches : false; + } + + function applyTheme() { + const configuredTheme = getConfiguredTheme(); + root.dataset.themePreference = configuredTheme; + root.dataset.theme = wantsDarkTheme(configuredTheme) ? "dark" : "light"; + } + + context.SaltGUITheme = { + applyTheme, + getConfiguredTheme, + }; + + applyTheme(); + + if (mediaQuery && mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", applyTheme); + } else if (mediaQuery && mediaQuery.addListener) { + mediaQuery.addListener(applyTheme); + } + + context.addEventListener("storage", applyTheme); + + try { + const parentDoc = context.parent && context.parent !== context ? context.parent.document : null; + if (parentDoc) { + const observer = new MutationObserver(applyTheme); + observer.observe(parentDoc.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme", "theme"], + }); + if (parentDoc.body) { + observer.observe(parentDoc.body, { + attributes: true, + attributeFilter: ["class", "data-theme", "theme"], + }); + } + } + } catch (_error) { + // Ignore parent observer failures outside embedded use. + } +})(); diff --git a/saltgui/static/stylesheets/beacons.css b/saltgui/static/stylesheets/beacons.css index 0a6c3f902..9f606e139 100644 --- a/saltgui/static/stylesheets/beacons.css +++ b/saltgui/static/stylesheets/beacons.css @@ -16,5 +16,5 @@ .beacon-disabled, .beacon-waiting { - color: gray; + color: var(--color-text-muted); } diff --git a/saltgui/static/stylesheets/controls.css b/saltgui/static/stylesheets/controls.css index 1e473f6e5..ec2b7ce50 100644 --- a/saltgui/static/stylesheets/controls.css +++ b/saltgui/static/stylesheets/controls.css @@ -2,7 +2,8 @@ input, select { - color: #272727; + background-color: var(--color-background-control); + color: var(--color-text-strong); padding: 7px 10px; display: inline-block; margin-bottom: 15px; @@ -14,24 +15,24 @@ select { input[type="text"], input[type="password"], select { - border: 2px solid #e2e2e2; + border: 2px solid var(--color-border-control); border-radius: 2px; } input[type="text"]:focus, input[type="password"]:focus, select:focus { - border: 2px solid #4caf50; + border: 2px solid var(--color-border-accent); } input[type="submit"] { - background-color: #4caf50; - color: white; + background-color: var(--color-text-accent); + color: var(--color-text-inverse); border: 0; margin-top: 5px; margin-bottom: 0; cursor: pointer; - box-shadow: 0 0 5px rgba(33, 33, 33, 50%); + box-shadow: var(--color-shadow-button); width: 20%; min-width: 200px; } diff --git a/saltgui/static/stylesheets/dropdown.css b/saltgui/static/stylesheets/dropdown.css index 49b8d7c59..7cee2e488 100644 --- a/saltgui/static/stylesheets/dropdown.css +++ b/saltgui/static/stylesheets/dropdown.css @@ -13,12 +13,13 @@ div.search-box .menu-dropdown { position: absolute; max-height: 300px; overflow-y: auto; - background: #fff; - box-shadow: 0 3px 10px -2px rgba(0, 0, 0, 30%); - border: 1px solid rgba(0, 0, 0, 10%); + background: var(--color-background-panel); + box-shadow: var(--color-shadow-dropdown); + border: 1px solid var(--color-border-default); z-index: 5; cursor: pointer; text-align: left; + color: var(--color-text-primary); } /* Links inside the menu-dropdown */ @@ -37,7 +38,7 @@ div.search-box .menu-dropdown { /* Change color of menu-dropdown links on hover */ .run-command-button .menu-dropdown-content div.run-command-button:hover { - background: rgba(0, 0, 0, 15%); + background: var(--color-background-hover); cursor: pointer; } @@ -60,8 +61,8 @@ pre.output .run-command-button { } pre.output .run-command-button:hover .menu-dropdown { - background-color: #f9f9f9; - color: black; + background-color: var(--color-background-control-soft); + color: var(--color-text-default); } .dropdown { @@ -72,8 +73,8 @@ pre.output .run-command-button:hover .menu-dropdown { .dropdown-content { display: none; position: absolute; - background-color: #f9f9f9; - box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 20%); + background-color: var(--color-background-control-soft); + box-shadow: var(--color-shadow-dropdown-large); z-index: 3; } diff --git a/saltgui/static/stylesheets/ha-theme.css b/saltgui/static/stylesheets/ha-theme.css deleted file mode 100644 index 788e1371f..000000000 --- a/saltgui/static/stylesheets/ha-theme.css +++ /dev/null @@ -1,169 +0,0 @@ -:root[data-theme="dark"] { - color-scheme: dark; - --salt-accent: #22c7bd; - --salt-accent-strong: #48dfd4; - --salt-bg: #071318; - --salt-bg-elevated: #0d1f26; - --salt-bg-panel: #122a33; - --salt-bg-soft: #173540; - --salt-bg-hover: rgba(34, 199, 189, 0.12); - --salt-text: #e6f7f5; - --salt-text-muted: #9ab8b5; - --salt-text-soft: #c4d7d5; - --salt-border: rgba(154, 184, 181, 0.18); - --salt-shadow: 0 18px 40px rgba(0, 0, 0, 0.38); - --salt-warning: #f4c14f; - --salt-danger: #ff7f8a; - --salt-link: #8fc4ff; - --salt-code: #0b171d; -} - -:root[data-theme="dark"] body, -:root[data-theme="dark"] #page-login { - background-color: var(--salt-bg); - color: var(--salt-text); -} - -:root[data-theme="dark"] header { - background-color: var(--salt-bg-elevated); - border-top-color: var(--salt-accent); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18); -} - -:root[data-theme="dark"] .logo, -:root[data-theme="dark"] .docu, -:root[data-theme="dark"] h1, -:root[data-theme="dark"] pre .minion-id, -:root[data-theme="dark"] table tr th, -:root[data-theme="dark"] .small-button:hover, -:root[data-theme="dark"] .menu-item:hover, -:root[data-theme="dark"] .run-command-button:hover, -:root[data-theme="dark"] .docu:hover { - color: var(--salt-accent); -} - -:root[data-theme="dark"] #button-manual-run { - fill: var(--salt-accent); -} - -:root[data-theme="dark"] .docu, -:root[data-theme="dark"] .msg, -:root[data-theme="dark"] .no-job-status, -:root[data-theme="dark"] .no-job-details, -:root[data-theme="dark"] .jobs td .time, -:root[data-theme="dark"] .attribution, -:root[data-theme="dark"] #login-panel h1, -:root[data-theme="dark"] #login-panel select option#eauth-default { - color: var(--salt-text-muted); -} - -:root[data-theme="dark"] .panel, -:root[data-theme="dark"] #login-panel, -:root[data-theme="dark"] .popup .run-command, -:root[data-theme="dark"] .dropdown-content, -:root[data-theme="dark"] .run-command-button .menu-dropdown-content { - background-color: var(--salt-bg-panel); - color: var(--salt-text); - border: 1px solid var(--salt-border); - box-shadow: var(--salt-shadow); -} - -:root[data-theme="dark"] .small-button, -:root[data-theme="dark"] .dropdown-content, -:root[data-theme="dark"] .run-command-button .menu-dropdown-content, -:root[data-theme="dark"] input[type="text"], -:root[data-theme="dark"] input[type="password"], -:root[data-theme="dark"] select { - background-color: var(--salt-bg-soft); - color: var(--salt-text); - border-color: var(--salt-border); -} - -:root[data-theme="dark"] input[type="text"]:focus, -:root[data-theme="dark"] input[type="password"]:focus, -:root[data-theme="dark"] select:focus { - border-color: var(--salt-accent); - outline: none; -} - -:root[data-theme="dark"] input[type="submit"] { - background-color: var(--salt-accent); - color: #052228; - box-shadow: 0 10px 30px rgba(34, 199, 189, 0.24); -} - -:root[data-theme="dark"] input[type="submit"]:hover { - background-color: var(--salt-accent-strong); -} - -:root[data-theme="dark"] .menu-item:hover, -:root[data-theme="dark"] .run-command-button .menu-dropdown-content div.run-command-button:hover, -:root[data-theme="dark"] .highlight-rows tbody tr:hover, -:root[data-theme="dark"] .tooltip:hover, -:root[data-theme="dark"] pre.output .tooltip:hover { - background: var(--salt-bg-hover); -} - -:root[data-theme="dark"] #warning { - background: rgba(244, 193, 79, 0.18); - border-top: 1px solid rgba(244, 193, 79, 0.35); - color: #ffe3a2; -} - -:root[data-theme="dark"] #motd, -:root[data-theme="dark"] #motdtxt, -:root[data-theme="dark"] #motdhtml { - color: var(--salt-text-soft); -} - -:root[data-theme="dark"] table thead th, -:root[data-theme="dark"] table tbody td { - color: var(--salt-text-soft); - border-bottom-color: var(--salt-border); -} - -:root[data-theme="dark"] table thead th { - border-bottom-color: rgba(34, 199, 189, 0.45); -} - -:root[data-theme="dark"] td.address > span, -:root[data-theme="dark"] pre.output a, -:root[data-theme="dark"] form a, -:root[data-theme="dark"] pre a { - color: var(--salt-link); -} - -:root[data-theme="dark"] .run-command-button, -:root[data-theme="dark"] .jobs td .target, -:root[data-theme="dark"] .jobs td .function, -:root[data-theme="dark"] .search-error, -:root[data-theme="dark"] .offline { - color: var(--salt-text); -} - -:root[data-theme="dark"] pre.output, -:root[data-theme="dark"] code, -:root[data-theme="dark"] tt { - background-color: var(--salt-code); - color: var(--salt-text); -} - -:root[data-theme="dark"] .tooltip > .tooltip-text, -:root[data-theme="dark"] pre.output .tooltip > .tooltip-text { - background-color: var(--salt-accent); - color: #062126; -} - -:root[data-theme="dark"] .tooltip > .tooltip-text::after, -:root[data-theme="dark"] pre.output .tooltip > .tooltip-text::after { - border-color: var(--salt-accent) transparent transparent transparent; -} - -:root[data-theme="dark"] .tooltip > .tooltip-text-error-bottom-left { - background-color: var(--salt-danger); - color: #24050a; -} - -:root[data-theme="dark"] .tooltip > .tooltip-text-error-bottom-left::after { - border-color: var(--salt-danger) transparent transparent; -} diff --git a/saltgui/static/stylesheets/job.css b/saltgui/static/stylesheets/job.css index 6a38a4e0d..8c3adf262 100644 --- a/saltgui/static/stylesheets/job.css +++ b/saltgui/static/stylesheets/job.css @@ -17,7 +17,7 @@ } .highlight-task { - background-color: gray; + background-color: var(--color-background-highlight); } #summary-list-job { diff --git a/saltgui/static/stylesheets/login.css b/saltgui/static/stylesheets/login.css index d5529bb35..d857802dc 100644 --- a/saltgui/static/stylesheets/login.css +++ b/saltgui/static/stylesheets/login.css @@ -9,14 +9,14 @@ @media not print { #page-login { - background-color: #263238; + background-color: var(--color-background-page); } } #login-panel { align-self: center; - background-color: white; - box-shadow: 0 0 24px rgba(0, 0, 0, 70%); + background-color: var(--color-background-panel); + box-shadow: var(--color-shadow-panel); border-radius: 2px; /* 1px needed to prevent bottom margin to disappear when using small screens */ @@ -30,7 +30,7 @@ font-weight: lighter; font-size: 60px; width: 100%; - color: #505050; + color: var(--color-text-primary); } #login-panel input { @@ -45,7 +45,7 @@ } #login-panel select option#eauth-default { - color: gray; + color: var(--color-text-muted); } .attribution { @@ -64,7 +64,7 @@ #notice { height: 0; overflow-y: hidden; - color: white; + color: var(--color-text-inverse); padding: 0; border-radius: 2px; text-align: center; diff --git a/saltgui/static/stylesheets/main.css b/saltgui/static/stylesheets/main.css index fccba7f1a..afe12c081 100644 --- a/saltgui/static/stylesheets/main.css +++ b/saltgui/static/stylesheets/main.css @@ -10,23 +10,25 @@ body { margin: 0; padding: 0; font-family: Roboto, Helvetica, Arial, sans-serif; + color: var(--color-text-primary); } @media not print { body { - background-color: #263238; + background-color: var(--color-background-page); } } header { - border-top: 2px solid #4caf50; - background-color: white; + border-top: 2px solid var(--color-border-accent); + background-color: var(--color-background-header); + box-shadow: var(--color-shadow-header); margin-top: 0; } .logo { cursor: pointer; - color: #4caf50; + color: var(--color-text-accent); font-size: 30px; font-weight: normal; display: inline-block; @@ -42,7 +44,7 @@ header { .docu { cursor: pointer; - color: gray; + color: var(--color-text-muted); font-size: 30px; font-weight: normal; display: inline-block; @@ -58,11 +60,11 @@ header { } .docu:hover { - color: #4caf50; + color: var(--color-text-accent); } h1 { - color: #4caf50; + color: var(--color-text-accent); font-weight: lighter; font-size: 20px; margin: 0 10px 0 0; @@ -70,12 +72,13 @@ h1 { } .msg { - color: #505050; + color: var(--color-text-primary); padding-top: 5px; } .panel { - background-color: white; + background-color: var(--color-background-panel); + color: var(--color-text-primary); padding: 20px; border-radius: 1px; @@ -114,11 +117,11 @@ h1 { display: inline-block; min-width: 50px; text-align: center; - background-color: #eee; + background-color: var(--color-background-control-muted); margin: 0; cursor: pointer; font-size: 18px; - color: #666; + color: var(--color-text-quiet); height: 24px; vertical-align: middle; padding-left: 10px; @@ -150,7 +153,7 @@ h1 { } .small-button:hover { - color: #4caf50; + color: var(--color-text-accent); } .small-button-for-hover { @@ -174,13 +177,14 @@ h1 { .search-error { display: block; - color: red; + color: var(--color-status-failure); margin-bottom: 10px; margin-left: 10px; } .menu-item { display: inline-block; + color: var(--color-text-default); font-size: 18px; font-weight: lighter; padding: 15px 30px; @@ -191,8 +195,8 @@ h1 { } .menu-item:hover { - background: rgba(0, 0, 0, 15%); - color: #4caf50; + background: var(--color-background-hover); + color: var(--color-text-accent); cursor: pointer; } @@ -203,7 +207,7 @@ h1 { } .menu-item-first-letter:hover::first-letter { - text-decoration: underline #4caf50 double; + text-decoration: underline var(--color-text-accent) double; text-decoration-skip-ink: none; } @@ -218,7 +222,8 @@ h1 { } #warning { - background: yellow; + background: var(--color-background-warning); + color: var(--color-text-warning); margin-top: 5px; padding: 5px 10px 5px 20px; } @@ -253,7 +258,7 @@ h1 { width: 100%; height: 100%; z-index: 2; - background-color: rgba(0, 0, 0, 86%); + background-color: var(--color-background-overlay); } .popup h1 { @@ -265,7 +270,8 @@ h1 { .run-command { padding: 30px; z-index: 3; - background-color: white; + background-color: var(--color-background-popup); + color: var(--color-text-primary); position: relative; top: 5px; margin-left: 15px; @@ -278,8 +284,8 @@ h1 { } pre.output { - background-color: #272727; - color: white; + background-color: var(--color-background-code); + color: var(--color-text-inverse); margin: 0; padding: 10px; border-radius: 2px; @@ -304,10 +310,10 @@ pre.output { } .warning-button:hover { - color: #4caf50; + color: var(--color-text-accent); cursor: pointer; } .state-details-compressed { - color: gray; + color: var(--color-text-muted); } diff --git a/saltgui/static/stylesheets/page.css b/saltgui/static/stylesheets/page.css index 492a50ecb..5b03cc8e8 100644 --- a/saltgui/static/stylesheets/page.css +++ b/saltgui/static/stylesheets/page.css @@ -20,7 +20,7 @@ pre a.disabled:hover { } pre.output a { - color: yellow; + color: var(--color-text-code-link); cursor: pointer; } @@ -44,16 +44,16 @@ pre.output div:first-of-type { pre .minion-id.host-success, pre #summary-jobs-active .host-success { - color: lime; + color: var(--color-status-success); } pre .minion-id.host-failure, pre #summary-jobs-active .host-failure { - color: red; + color: var(--color-status-failure); } td.address > span { - color: #3f51b5; + color: var(--color-text-link); cursor: copy; position: relative; } @@ -73,15 +73,20 @@ td.tasks span.tasksummary { padding-right: 2px; } +td.tasks span.task, +td.tasks span.tasksummary { + background-color: var(--color-background-code); +} + pre .minion-id.host-skips, pre #summary-jobs-active .host-skips, pre .minion-id.host-no-response, pre #summary-jobs-active .host-no-response { - color: yellow; + color: var(--color-status-warning); } pre .minion-id { - color: #4caf50; + color: var(--color-text-accent); } .task-summary { @@ -90,7 +95,7 @@ pre .minion-id { } pre span.active { - color: greenyellow; + color: var(--color-status-success-soft); font-weight: bold; } @@ -101,7 +106,7 @@ table { } table tr th { - border-bottom: 3px double #4caf50; + border-bottom: 3px double var(--color-border-accent); padding-right: 20px; padding-top: 5px; padding-bottom: 5px; @@ -119,23 +124,23 @@ table tr td { } .no-job-status { - color: gray; + color: var(--color-text-muted); } .no-job-details { - color: gray; + color: var(--color-text-muted); } table thead th, table tbody td { padding: 8px; text-align: left; - border-bottom: 1px solid #ddd; - color: #505050; + border-bottom: 1px solid var(--color-border-default); + color: var(--color-text-primary); } table thead th { - border-bottom: 2px solid #ddd; + border-bottom: 2px solid var(--color-border-default); } table tr th:last-child { @@ -163,12 +168,12 @@ table tr td:last-of-type { } .run-command-button { - color: #263238; + color: var(--color-text-strong); cursor: pointer; } .run-command-button:hover { - color: #2e7d32; + color: var(--color-text-accent); } #template-catmenu-here, @@ -182,24 +187,24 @@ table tr td:last-of-type { } .accepted { - color: #00a000; + color: var(--color-status-accepted); } .denied { - color: #f0f; + color: var(--color-status-denied); } .unaccepted, .keyunknown { - color: #f00; + color: var(--color-status-unaccepted); } .rejected { - color: #00f; + color: var(--color-status-rejected); } .offline { - color: red; + color: var(--color-status-offline); } .prefiximage { @@ -217,14 +222,14 @@ table tr td:last-of-type { .jobs td .target { font-weight: 500; font-size: 18px; - color: #505050; + color: var(--color-text-primary); white-space: nowrap; } .jobs td .function { font-weight: 500; font-size: 14px; - color: #3a3a3a; + color: var(--color-text-secondary); } .jobs td .time { @@ -250,7 +255,7 @@ table tr td:last-of-type { } .highlight-rows tbody tr:hover { - background-color: whitesmoke; + background-color: var(--color-background-row-hover); cursor: pointer; } @@ -279,21 +284,21 @@ table tr td:last-of-type { /* tasks */ .task-success { - color: lime; + color: var(--color-status-success); } .task-success-changes { - color: aqua; + color: var(--color-status-info); } .task-failure, .task-failure-changes { - color: red; + color: var(--color-status-failure); } .task-skipped, .task-skipped-changes { - color: yellow; + color: var(--color-status-warning); } pre .task-success, diff --git a/saltgui/static/stylesheets/schedules.css b/saltgui/static/stylesheets/schedules.css index cfcbfdebb..56287d85e 100644 --- a/saltgui/static/stylesheets/schedules.css +++ b/saltgui/static/stylesheets/schedules.css @@ -7,5 +7,5 @@ td.schedule-value { } td.schedule-disabled { - color: gray; + color: var(--color-text-muted); } diff --git a/saltgui/static/stylesheets/theme.css b/saltgui/static/stylesheets/theme.css new file mode 100644 index 000000000..2369a83ed --- /dev/null +++ b/saltgui/static/stylesheets/theme.css @@ -0,0 +1,153 @@ +:root { + color-scheme: light; + --color-background-page: #263238; + --color-background-header: #fff; + --color-background-panel: #fff; + --color-background-popup: #fff; + --color-background-overlay: rgba(0, 0, 0, 0.86); + --color-background-code: #272727; + --color-background-control: #fff; + --color-background-control-muted: #eee; + --color-background-control-soft: #f9f9f9; + --color-background-hover: rgba(0, 0, 0, 0.15); + --color-background-row-hover: whitesmoke; + --color-background-tooltip: rgba(76, 175, 80, 0.8); + --color-background-tooltip-error: rgba(244, 67, 54, 0.92); + --color-background-tooltip-hover: #e0e0e0; + --color-background-tooltip-hover-code: #484848; + --color-background-warning: #ffeb3b; + --color-background-highlight: rgba(128, 128, 128, 0.6); + --color-text-default: #000; + --color-text-strong: #263238; + --color-text-primary: #505050; + --color-text-secondary: #3a3a3a; + --color-text-muted: gray; + --color-text-quiet: #666; + --color-text-accent: #4caf50; + --color-text-link: #3f51b5; + --color-text-inverse: #fff; + --color-text-code-link: #ffeb3b; + --color-text-tooltip: #fff; + --color-text-tooltip-error: #fff; + --color-text-warning: #263238; + --color-border-accent: #4caf50; + --color-border-default: #ddd; + --color-border-control: #e2e2e2; + --color-shadow-panel: 0 0 24px rgba(0, 0, 0, 0.7); + --color-shadow-dropdown: 0 3px 10px -2px rgba(0, 0, 0, 0.3); + --color-shadow-dropdown-large: 0 8px 16px 0 rgba(0, 0, 0, 0.2); + --color-shadow-button: 0 0 5px rgba(33, 33, 33, 0.5); + --color-shadow-header: none; + --color-menu-dimmed: lightgray; + --color-status-accepted: #00a000; + --color-status-denied: #f0f; + --color-status-unaccepted: #f00; + --color-status-rejected: #00f; + --color-status-offline: #f00; + --color-status-success: lime; + --color-status-success-soft: greenyellow; + --color-status-warning: yellow; + --color-status-caution: orange; + --color-status-failure: red; + --color-status-info: aqua; + --color-notice-muted: gray; + --color-notice-danger: #f44336; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --color-background-page: #071318; + --color-background-header: #0d1f26; + --color-background-panel: #122a33; + --color-background-popup: #122a33; + --color-background-overlay: rgba(0, 0, 0, 0.86); + --color-background-code: #0b171d; + --color-background-control: #173540; + --color-background-control-muted: #173540; + --color-background-control-soft: #173540; + --color-background-hover: rgba(34, 199, 189, 0.12); + --color-background-row-hover: rgba(34, 199, 189, 0.08); + --color-background-tooltip: rgba(34, 199, 189, 0.82); + --color-background-tooltip-error: rgba(255, 127, 138, 0.9); + --color-background-tooltip-hover: rgba(34, 199, 189, 0.12); + --color-background-tooltip-hover-code: rgba(34, 199, 189, 0.12); + --color-background-warning: rgba(244, 193, 79, 0.18); + --color-background-highlight: rgba(11, 23, 29, 0.9); + --color-text-default: #e6f7f5; + --color-text-strong: #e6f7f5; + --color-text-primary: #c4d7d5; + --color-text-secondary: #9ab8b5; + --color-text-muted: #9ab8b5; + --color-text-quiet: #9ab8b5; + --color-text-accent: #22c7bd; + --color-text-link: #8fc4ff; + --color-text-inverse: #062126; + --color-text-code-link: #ffe38f; + --color-text-tooltip: #062126; + --color-text-tooltip-error: #24050a; + --color-text-warning: #ffe3a2; + --color-border-accent: rgba(34, 199, 189, 0.45); + --color-border-default: rgba(154, 184, 181, 0.18); + --color-border-control: rgba(154, 184, 181, 0.18); + --color-shadow-panel: 0 18px 40px rgba(0, 0, 0, 0.38); + --color-shadow-dropdown: 0 18px 40px rgba(0, 0, 0, 0.38); + --color-shadow-dropdown-large: 0 18px 40px rgba(0, 0, 0, 0.38); + --color-shadow-button: 0 10px 30px rgba(34, 199, 189, 0.24); + --color-shadow-header: 0 10px 30px rgba(0, 0, 0, 0.18); + --color-menu-dimmed: #85a3a0; + --color-status-accepted: #60f29d; + --color-status-denied: #ff8df7; + --color-status-unaccepted: #ff8e8e; + --color-status-rejected: #7aa7ff; + --color-status-offline: #ff8e8e; + --color-status-success: #78ffaf; + --color-status-success-soft: #bfff72; + --color-status-warning: #ffe38f; + --color-status-caution: #ffb26b; + --color-status-failure: #ff8e8e; + --color-status-info: #66e7ff; + --color-notice-muted: #5a7980; + --color-notice-danger: #b93a48; +} + +.menu-item-dimmed { + color: var(--color-menu-dimmed); +} + +.button-in-text { + background-color: var(--color-background-control-muted); + border-radius: 2px; + color: var(--color-text-strong); + padding: 0 0.25em; +} + +.job-details-success, +.text-success { + color: var(--color-status-accepted); +} + +.job-details-warning, +.text-warning { + color: var(--color-status-caution); +} + +.job-details-failure, +.text-error { + color: var(--color-status-failure); +} + +.job-details-info, +.text-info { + color: var(--color-status-info); +} + +.doc-inline-code { + background-color: var(--color-background-control-muted); + border-radius: 2px; + color: var(--color-text-primary); + padding: 0 0.25em; +} + +.doc-inline-highlight { + color: var(--color-text-code-link); +} diff --git a/saltgui/static/stylesheets/tooltip.css b/saltgui/static/stylesheets/tooltip.css index faf2944b1..0da1b1ab0 100644 --- a/saltgui/static/stylesheets/tooltip.css +++ b/saltgui/static/stylesheets/tooltip.css @@ -5,8 +5,8 @@ .tooltip > .tooltip-text { display: none; font-size: 14px; - background-color: rgba(76, 175, 80, 80%); /* #4caf50 */ - color: white; + background-color: var(--color-background-tooltip); + color: var(--color-text-tooltip); padding: 7px; border-radius: 3px; position: absolute; @@ -36,7 +36,8 @@ .tooltip > .tooltip-text-error-bottom-left { text-align: left; transform: translate(-5%, 0); - background-color: red; + background-color: var(--color-background-tooltip-error); + color: var(--color-text-tooltip-error); font-weight: bold; } @@ -56,8 +57,7 @@ } .tooltip:hover { - /* only slightly darker than 'whitesmoke(#f5f5f5)' */ - background-color: #e0e0e0; + background-color: var(--color-background-tooltip-hover); } .tooltip:hover > .tooltip-text { @@ -65,12 +65,11 @@ } pre.output .tooltip > .tooltip-text { - background-color: rgba(76, 175, 80, 80%); /* #4caf50 */ + background-color: var(--color-background-tooltip); } pre.output .tooltip:hover { - /* only slightly lighter than '#272727' */ - background-color: #484848; + background-color: var(--color-background-tooltip-hover-code); } /* The arrow/triangle of the tooltip */ @@ -83,7 +82,7 @@ pre.output .tooltip:hover { border-width: 5px; border-style: solid; top: 100%; - border-color: rgba(76, 175, 80, 80%) transparent transparent transparent; + border-color: var(--color-background-tooltip) transparent transparent transparent; } .tooltip > .tooltip-text-logo { @@ -108,7 +107,7 @@ pre.output .tooltip:hover { .tooltip > .tooltip-text-error-bottom-left::after { left: calc(5% - 2.5px); - border-color: red transparent transparent; + border-color: var(--color-background-tooltip-error) transparent transparent; } .tooltip > .tooltip-text-bottom-center::after { @@ -120,5 +119,5 @@ pre.output .tooltip:hover { } pre.output .tooltip > .tooltip-text::after { - border-color: rgba(76, 175, 80, 80%) transparent transparent transparent; /* #4caf50 */ + border-color: var(--color-background-tooltip) transparent transparent transparent; } From b8a5716d3685a6b67610b9fcef5b82ee5bb77e30 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Tue, 14 Apr 2026 16:53:52 -0600 Subject: [PATCH 03/16] Polish theme lint and browser sync --- saltgui/static/scripts/panels/Login.js | 4 +- saltgui/static/scripts/panels/Options.js | 4 +- saltgui/static/scripts/theme.js | 84 +++++++++++++----------- saltgui/static/stylesheets/page.css | 10 +-- saltgui/static/stylesheets/theme.css | 54 +++++++-------- 5 files changed, 82 insertions(+), 74 deletions(-) diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 5db672aeb..45d6d25b8 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -394,9 +394,7 @@ export class LoginPanel extends Panel { Utils.setStorageItem("session", "theme", theme); Utils.setStorageItem("local", "theme_default", theme); - if (globalThis.SaltGUITheme && globalThis.SaltGUITheme.applyTheme) { - globalThis.SaltGUITheme.applyTheme(); - } + globalThis.SaltGUITheme?.applyTheme?.(); // store for later use diff --git a/saltgui/static/scripts/panels/Options.js b/saltgui/static/scripts/panels/Options.js index eb16c9522..2ab6007b3 100644 --- a/saltgui/static/scripts/panels/Options.js +++ b/saltgui/static/scripts/panels/Options.js @@ -602,8 +602,6 @@ export class OptionsPanel extends Panel { const themeTd = this.div.querySelector("#option-theme-value"); themeTd.innerText = value; Utils.setStorageItem("session", "theme", value); - if (globalThis.SaltGUITheme && globalThis.SaltGUITheme.applyTheme) { - globalThis.SaltGUITheme.applyTheme(); - } + globalThis.SaltGUITheme?.applyTheme?.(); } } diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index 82e2575ea..b06dd3bb8 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -1,9 +1,13 @@ -(function () { +(function initializeTheme () { const context = globalThis; const root = document.documentElement; const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; - function getStoredTheme() { + function reportIgnoredError (message, error) { + context.console?.debug?.(message, error); + } + + function getStoredTheme () { try { const sessionTheme = context.sessionStorage ? context.sessionStorage.getItem("theme") : null; if (sessionTheme) { @@ -13,14 +17,15 @@ if (defaultTheme) { return defaultTheme; } - } catch (_error) { + } catch (error) { // Storage access can fail in restricted browser environments. + reportIgnoredError("SaltGUI theme: storage unavailable", error); } return "auto"; } - function getConfiguredTheme() { + function getConfiguredTheme () { const theme = (getStoredTheme() || "auto").toLowerCase(); if (theme === "light" || theme === "dark") { return theme; @@ -28,25 +33,34 @@ return "auto"; } - function getParentHints() { - const values = []; - + function getParentDocument () { + if (context.self === context.top) { + return null; + } try { - const parentDoc = context.parent && context.parent !== context ? context.parent.document : null; - if (parentDoc) { - values.push(parentDoc.documentElement.dataset.theme || ""); - values.push(parentDoc.documentElement.getAttribute("theme") || ""); - values.push(parentDoc.documentElement.className || ""); - values.push(parentDoc.body ? parentDoc.body.className || "" : ""); - } - } catch (_error) { + return context.parent.document; + } catch (error) { // Access to the parent frame can fail outside an embedded environment. + reportIgnoredError("SaltGUI theme: parent frame unavailable", error); + return null; + } + } + + function getParentHints () { + const parentDoc = getParentDocument(); + if (!parentDoc) { + return ""; } - return values.join(" ").toLowerCase(); + return [ + parentDoc.documentElement.dataset.theme || "", + parentDoc.documentElement.getAttribute("theme") || "", + parentDoc.documentElement.className || "", + parentDoc.body?.className || "", + ].join(" ").toLowerCase(); } - function wantsDarkTheme(configuredTheme) { + function wantsDarkTheme (configuredTheme) { if (configuredTheme === "dark") { return true; } @@ -65,7 +79,7 @@ return mediaQuery ? mediaQuery.matches : false; } - function applyTheme() { + function applyTheme () { const configuredTheme = getConfiguredTheme(); root.dataset.themePreference = configuredTheme; root.dataset.theme = wantsDarkTheme(configuredTheme) ? "dark" : "light"; @@ -77,31 +91,27 @@ }; applyTheme(); + mediaQuery?.addEventListener("change", applyTheme); + context.addEventListener("storage", applyTheme); - if (mediaQuery && mediaQuery.addEventListener) { - mediaQuery.addEventListener("change", applyTheme); - } else if (mediaQuery && mediaQuery.addListener) { - mediaQuery.addListener(applyTheme); + const parentDoc = getParentDocument(); + if (!parentDoc) { + return; } - context.addEventListener("storage", applyTheme); - try { - const parentDoc = context.parent && context.parent !== context ? context.parent.document : null; - if (parentDoc) { - const observer = new MutationObserver(applyTheme); - observer.observe(parentDoc.documentElement, { - attributes: true, + const observer = new MutationObserver(applyTheme); + observer.observe(parentDoc.documentElement, { + attributeFilter: ["class", "data-theme", "theme"], + attributes: true, + }); + if (parentDoc.body) { + observer.observe(parentDoc.body, { attributeFilter: ["class", "data-theme", "theme"], + attributes: true, }); - if (parentDoc.body) { - observer.observe(parentDoc.body, { - attributes: true, - attributeFilter: ["class", "data-theme", "theme"], - }); - } } - } catch (_error) { - // Ignore parent observer failures outside embedded use. + } catch (error) { + reportIgnoredError("SaltGUI theme: parent observer unavailable", error); } })(); diff --git a/saltgui/static/stylesheets/page.css b/saltgui/static/stylesheets/page.css index 5b03cc8e8..d638381ef 100644 --- a/saltgui/static/stylesheets/page.css +++ b/saltgui/static/stylesheets/page.css @@ -63,6 +63,11 @@ td.tasks span { padding-bottom: 3px; } +td.tasks span.task, +td.tasks span.tasksummary { + background-color: var(--color-background-code); +} + td.tasks span.task:first-child, td.tasks span.tasksummary { padding-left: 2px; @@ -73,11 +78,6 @@ td.tasks span.tasksummary { padding-right: 2px; } -td.tasks span.task, -td.tasks span.tasksummary { - background-color: var(--color-background-code); -} - pre .minion-id.host-skips, pre #summary-jobs-active .host-skips, pre .minion-id.host-no-response, diff --git a/saltgui/static/stylesheets/theme.css b/saltgui/static/stylesheets/theme.css index 2369a83ed..5ac8d6d36 100644 --- a/saltgui/static/stylesheets/theme.css +++ b/saltgui/static/stylesheets/theme.css @@ -1,22 +1,23 @@ :root { color-scheme: light; + --color-background-page: #263238; --color-background-header: #fff; --color-background-panel: #fff; --color-background-popup: #fff; - --color-background-overlay: rgba(0, 0, 0, 0.86); + --color-background-overlay: rgba(0, 0, 0, 86%); --color-background-code: #272727; --color-background-control: #fff; --color-background-control-muted: #eee; --color-background-control-soft: #f9f9f9; - --color-background-hover: rgba(0, 0, 0, 0.15); + --color-background-hover: rgba(0, 0, 0, 15%); --color-background-row-hover: whitesmoke; - --color-background-tooltip: rgba(76, 175, 80, 0.8); - --color-background-tooltip-error: rgba(244, 67, 54, 0.92); + --color-background-tooltip: rgba(76, 175, 80, 80%); + --color-background-tooltip-error: rgba(244, 67, 54, 92%); --color-background-tooltip-hover: #e0e0e0; --color-background-tooltip-hover-code: #484848; --color-background-warning: #ffeb3b; - --color-background-highlight: rgba(128, 128, 128, 0.6); + --color-background-highlight: rgba(128, 128, 128, 60%); --color-text-default: #000; --color-text-strong: #263238; --color-text-primary: #505050; @@ -33,10 +34,10 @@ --color-border-accent: #4caf50; --color-border-default: #ddd; --color-border-control: #e2e2e2; - --color-shadow-panel: 0 0 24px rgba(0, 0, 0, 0.7); - --color-shadow-dropdown: 0 3px 10px -2px rgba(0, 0, 0, 0.3); - --color-shadow-dropdown-large: 0 8px 16px 0 rgba(0, 0, 0, 0.2); - --color-shadow-button: 0 0 5px rgba(33, 33, 33, 0.5); + --color-shadow-panel: 0 0 24px rgba(0, 0, 0, 70%); + --color-shadow-dropdown: 0 3px 10px -2px rgba(0, 0, 0, 30%); + --color-shadow-dropdown-large: 0 8px 16px 0 rgba(0, 0, 0, 20%); + --color-shadow-button: 0 0 5px rgba(33, 33, 33, 50%); --color-shadow-header: none; --color-menu-dimmed: lightgray; --color-status-accepted: #00a000; @@ -56,23 +57,24 @@ :root[data-theme="dark"] { color-scheme: dark; + --color-background-page: #071318; --color-background-header: #0d1f26; --color-background-panel: #122a33; --color-background-popup: #122a33; - --color-background-overlay: rgba(0, 0, 0, 0.86); + --color-background-overlay: rgba(0, 0, 0, 86%); --color-background-code: #0b171d; --color-background-control: #173540; --color-background-control-muted: #173540; --color-background-control-soft: #173540; - --color-background-hover: rgba(34, 199, 189, 0.12); - --color-background-row-hover: rgba(34, 199, 189, 0.08); - --color-background-tooltip: rgba(34, 199, 189, 0.82); - --color-background-tooltip-error: rgba(255, 127, 138, 0.9); - --color-background-tooltip-hover: rgba(34, 199, 189, 0.12); - --color-background-tooltip-hover-code: rgba(34, 199, 189, 0.12); - --color-background-warning: rgba(244, 193, 79, 0.18); - --color-background-highlight: rgba(11, 23, 29, 0.9); + --color-background-hover: rgba(34, 199, 189, 12%); + --color-background-row-hover: rgba(34, 199, 189, 8%); + --color-background-tooltip: rgba(34, 199, 189, 82%); + --color-background-tooltip-error: rgba(255, 127, 138, 90%); + --color-background-tooltip-hover: rgba(34, 199, 189, 12%); + --color-background-tooltip-hover-code: rgba(34, 199, 189, 12%); + --color-background-warning: rgba(244, 193, 79, 18%); + --color-background-highlight: rgba(11, 23, 29, 90%); --color-text-default: #e6f7f5; --color-text-strong: #e6f7f5; --color-text-primary: #c4d7d5; @@ -86,14 +88,14 @@ --color-text-tooltip: #062126; --color-text-tooltip-error: #24050a; --color-text-warning: #ffe3a2; - --color-border-accent: rgba(34, 199, 189, 0.45); - --color-border-default: rgba(154, 184, 181, 0.18); - --color-border-control: rgba(154, 184, 181, 0.18); - --color-shadow-panel: 0 18px 40px rgba(0, 0, 0, 0.38); - --color-shadow-dropdown: 0 18px 40px rgba(0, 0, 0, 0.38); - --color-shadow-dropdown-large: 0 18px 40px rgba(0, 0, 0, 0.38); - --color-shadow-button: 0 10px 30px rgba(34, 199, 189, 0.24); - --color-shadow-header: 0 10px 30px rgba(0, 0, 0, 0.18); + --color-border-accent: rgba(34, 199, 189, 45%); + --color-border-default: rgba(154, 184, 181, 18%); + --color-border-control: rgba(154, 184, 181, 18%); + --color-shadow-panel: 0 18px 40px rgba(0, 0, 0, 38%); + --color-shadow-dropdown: 0 18px 40px rgba(0, 0, 0, 38%); + --color-shadow-dropdown-large: 0 18px 40px rgba(0, 0, 0, 38%); + --color-shadow-button: 0 10px 30px rgba(34, 199, 189, 24%); + --color-shadow-header: 0 10px 30px rgba(0, 0, 0, 18%); --color-menu-dimmed: #85a3a0; --color-status-accepted: #60f29d; --color-status-denied: #ff8df7; From 4284b0632eb12b8e94d4cd66fb1bba2e000c3115 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Tue, 14 Apr 2026 17:11:45 -0600 Subject: [PATCH 04/16] Bootstrap SaltGUI config for ingress sessions --- saltgui/static/scripts/Router.js | 6 ++- saltgui/static/scripts/panels/Login.js | 53 ++++++++++++++------------ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/saltgui/static/scripts/Router.js b/saltgui/static/scripts/Router.js index 60fc871a3..30e4ab561 100644 --- a/saltgui/static/scripts/Router.js +++ b/saltgui/static/scripts/Router.js @@ -40,7 +40,7 @@ export class Router { this.pages = []; Router.currentPage = undefined; - this._registerPage(new LoginPage(this)); + this._registerPage(Router.loginPage = new LoginPage(this)); this._registerPage(Router.minionsPage = new MinionsPage(this)); this._registerPage(Router.keysPage = new KeysPage(this)); this._registerPage(Router.grainsPage = new GrainsPage(this)); @@ -73,6 +73,10 @@ export class Router { Router.updateMainMenu(); + if (Utils.getStorageItem("session", "login_response") !== null) { + Router.loginPage.login.bootstrapSession(); + } + const hash = window.location.hash.replace(/^#/, ""); const search = window.location.search; /* eslint-disable compat/compat */ diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 45d6d25b8..318f1111b 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -308,6 +308,35 @@ export class LoginPanel extends Panel { Utils.setStorageItem("local", "salt-motd-txt", ""); Utils.setStorageItem("local", "salt-motd-html", ""); + this.bootstrapSession(); + + // allow the success message to be seen + window.setTimeout(() => { + // erase credentials since we don't do page-refresh + this.usernameField.value = ""; + this.passwordField.value = ""; + if (Utils.getStorageItem("session", "login_response") !== null) { + // we might have been logged out in this first second + // e.g. when clock between client and server differs more than the session timout + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("page")) { + // a redirect page is specified + const params = {}; + for (const pair of urlParams.entries()) { + params[pair[0]] = pair[1]; + } + const page = params["page"]; + delete params["page"]; + this.router.goTo(page, params); + } else { + this.router.goTo(""); + } + } + }, 1000); + + } + + bootstrapSession () { // We need these functions to populate the dropdown boxes const wheelConfigValuesPromise = this.api.getWheelConfigValues(); const runnerStateOrchestrateShowSlsPromise = this.api.getRunnerStateOrchestrateShowSls(); @@ -339,30 +368,6 @@ export class LoginPanel extends Panel { }); /* eslint-enable no-unused-vars */ - // allow the success message to be seen - window.setTimeout(() => { - // erase credentials since we don't do page-refresh - this.usernameField.value = ""; - this.passwordField.value = ""; - if (Utils.getStorageItem("session", "login_response") !== null) { - // we might have been logged out in this first second - // e.g. when clock between client and server differs more than the session timout - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.get("page")) { - // a redirect page is specified - const params = {}; - for (const pair of urlParams.entries()) { - params[pair[0]] = pair[1]; - } - const page = params["page"]; - delete params["page"]; - this.router.goTo(page, params); - } else { - this.router.goTo(""); - } - } - }, 1000); - BeaconsMinionPanel.getAvailableBeacons(this.api); } From 39d586f9239649c6a0e5b89e5be779fca7c1a814 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 08:40:19 -0600 Subject: [PATCH 05/16] Keep SaltGUI login fields readable in dark mode --- saltgui/static/stylesheets/login.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/saltgui/static/stylesheets/login.css b/saltgui/static/stylesheets/login.css index d857802dc..77e44881b 100644 --- a/saltgui/static/stylesheets/login.css +++ b/saltgui/static/stylesheets/login.css @@ -39,6 +39,19 @@ font-size: 18px; } +#login-panel input, +#login-panel select { + background-color: var(--color-background-control); + color: var(--color-text-strong); + caret-color: var(--color-text-strong); + -webkit-text-fill-color: var(--color-text-strong); +} + +#login-panel input::placeholder { + color: var(--color-text-muted); + opacity: 1; +} + #login-panel select { width: 100%; font-size: 14px; From e852d54cce0d9ae2f159b557e9f7b649eff40f89 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 08:42:28 -0600 Subject: [PATCH 06/16] Detect embedded dark theme from parent styles --- saltgui/static/scripts/theme.js | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index b06dd3bb8..6cf6d370a 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -60,6 +60,65 @@ ].join(" ").toLowerCase(); } + function parseColor (value) { + if (!value || value === "transparent") { + return null; + } + + const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i); + if (!rgbMatch) { + return null; + } + + const channels = rgbMatch[1].split(",").map((channel) => Number.parseFloat(channel.trim())); + if (channels.length < 3 || channels.slice(0, 3).some((channel) => Number.isNaN(channel))) { + return null; + } + + const alpha = channels.length > 3 ? channels[3] : 1; + if (Number.isNaN(alpha) || alpha <= 0) { + return null; + } + + return channels.slice(0, 3); + } + + function isDarkColor (value) { + const channels = parseColor(value); + if (!channels) { + return null; + } + + const [red, green, blue] = channels; + const brightness = (red * 299 + green * 587 + blue * 114) / 1000; + return brightness < 140; + } + + function getParentComputedTheme () { + const parentDoc = getParentDocument(); + if (!parentDoc || !context.getComputedStyle) { + return null; + } + + try { + const candidates = [context.getComputedStyle(parentDoc.documentElement).backgroundColor]; + if (parentDoc.body) { + candidates.push(context.getComputedStyle(parentDoc.body).backgroundColor); + } + + for (const candidate of candidates) { + const isDark = isDarkColor(candidate); + if (isDark !== null) { + return isDark; + } + } + } catch (error) { + reportIgnoredError("SaltGUI theme: parent style unavailable", error); + } + + return null; + } + function wantsDarkTheme (configuredTheme) { if (configuredTheme === "dark") { return true; @@ -76,6 +135,11 @@ return true; } + const computedTheme = getParentComputedTheme(); + if (computedTheme !== null) { + return computedTheme; + } + return mediaQuery ? mediaQuery.matches : false; } From a079960f5021111e2f7d3a3fbb2601a64dd2b7d1 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 09:26:07 -0600 Subject: [PATCH 07/16] Scope dark_mode PR to theme changes and keep ES6-safe theme apply call --- saltgui/static/scripts/Router.js | 6 +-- saltgui/static/scripts/panels/Login.js | 57 ++++++++++++-------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/saltgui/static/scripts/Router.js b/saltgui/static/scripts/Router.js index 30e4ab561..60fc871a3 100644 --- a/saltgui/static/scripts/Router.js +++ b/saltgui/static/scripts/Router.js @@ -40,7 +40,7 @@ export class Router { this.pages = []; Router.currentPage = undefined; - this._registerPage(Router.loginPage = new LoginPage(this)); + this._registerPage(new LoginPage(this)); this._registerPage(Router.minionsPage = new MinionsPage(this)); this._registerPage(Router.keysPage = new KeysPage(this)); this._registerPage(Router.grainsPage = new GrainsPage(this)); @@ -73,10 +73,6 @@ export class Router { Router.updateMainMenu(); - if (Utils.getStorageItem("session", "login_response") !== null) { - Router.loginPage.login.bootstrapSession(); - } - const hash = window.location.hash.replace(/^#/, ""); const search = window.location.search; /* eslint-disable compat/compat */ diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 318f1111b..2b5d86a97 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -308,35 +308,6 @@ export class LoginPanel extends Panel { Utils.setStorageItem("local", "salt-motd-txt", ""); Utils.setStorageItem("local", "salt-motd-html", ""); - this.bootstrapSession(); - - // allow the success message to be seen - window.setTimeout(() => { - // erase credentials since we don't do page-refresh - this.usernameField.value = ""; - this.passwordField.value = ""; - if (Utils.getStorageItem("session", "login_response") !== null) { - // we might have been logged out in this first second - // e.g. when clock between client and server differs more than the session timout - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.get("page")) { - // a redirect page is specified - const params = {}; - for (const pair of urlParams.entries()) { - params[pair[0]] = pair[1]; - } - const page = params["page"]; - delete params["page"]; - this.router.goTo(page, params); - } else { - this.router.goTo(""); - } - } - }, 1000); - - } - - bootstrapSession () { // We need these functions to populate the dropdown boxes const wheelConfigValuesPromise = this.api.getWheelConfigValues(); const runnerStateOrchestrateShowSlsPromise = this.api.getRunnerStateOrchestrateShowSls(); @@ -368,6 +339,30 @@ export class LoginPanel extends Panel { }); /* eslint-enable no-unused-vars */ + // allow the success message to be seen + window.setTimeout(() => { + // erase credentials since we don't do page-refresh + this.usernameField.value = ""; + this.passwordField.value = ""; + if (Utils.getStorageItem("session", "login_response") !== null) { + // we might have been logged out in this first second + // e.g. when clock between client and server differs more than the session timout + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("page")) { + // a redirect page is specified + const params = {}; + for (const pair of urlParams.entries()) { + params[pair[0]] = pair[1]; + } + const page = params["page"]; + delete params["page"]; + this.router.goTo(page, params); + } else { + this.router.goTo(""); + } + } + }, 1000); + BeaconsMinionPanel.getAvailableBeacons(this.api); } @@ -399,7 +394,9 @@ export class LoginPanel extends Panel { Utils.setStorageItem("session", "theme", theme); Utils.setStorageItem("local", "theme_default", theme); - globalThis.SaltGUITheme?.applyTheme?.(); + if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { + globalThis.SaltGUITheme.applyTheme(); + } // store for later use From 77135c0fdbccd6536b4098386edabf2725f60008 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 09:49:58 -0600 Subject: [PATCH 08/16] Fix ingress dark-theme bootstrap and robust embedded theme detection --- saltgui/static/scripts/Router.js | 8 +++- saltgui/static/scripts/panels/Login.js | 53 ++++++++++++++------------ saltgui/static/scripts/theme.js | 24 ++++++++++-- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/saltgui/static/scripts/Router.js b/saltgui/static/scripts/Router.js index 60fc871a3..304400ff9 100644 --- a/saltgui/static/scripts/Router.js +++ b/saltgui/static/scripts/Router.js @@ -40,7 +40,7 @@ export class Router { this.pages = []; Router.currentPage = undefined; - this._registerPage(new LoginPage(this)); + this._registerPage(Router.loginPage = new LoginPage(this)); this._registerPage(Router.minionsPage = new MinionsPage(this)); this._registerPage(Router.keysPage = new KeysPage(this)); this._registerPage(Router.grainsPage = new GrainsPage(this)); @@ -73,6 +73,12 @@ export class Router { Router.updateMainMenu(); + // In embedded ingress mode, the login callback path may not run. + // Load session-dependent settings (including theme preference) when already authenticated. + if (Utils.getStorageItem("session", "login_response") !== null) { + Router.loginPage.login.bootstrapSession(); + } + const hash = window.location.hash.replace(/^#/, ""); const search = window.location.search; /* eslint-disable compat/compat */ diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 2b5d86a97..d9db2ffbc 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -308,6 +308,35 @@ export class LoginPanel extends Panel { Utils.setStorageItem("local", "salt-motd-txt", ""); Utils.setStorageItem("local", "salt-motd-html", ""); + this.bootstrapSession(); + + // allow the success message to be seen + window.setTimeout(() => { + // erase credentials since we don't do page-refresh + this.usernameField.value = ""; + this.passwordField.value = ""; + if (Utils.getStorageItem("session", "login_response") !== null) { + // we might have been logged out in this first second + // e.g. when clock between client and server differs more than the session timout + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("page")) { + // a redirect page is specified + const params = {}; + for (const pair of urlParams.entries()) { + params[pair[0]] = pair[1]; + } + const page = params["page"]; + delete params["page"]; + this.router.goTo(page, params); + } else { + this.router.goTo(""); + } + } + }, 1000); + + } + + bootstrapSession () { // We need these functions to populate the dropdown boxes const wheelConfigValuesPromise = this.api.getWheelConfigValues(); const runnerStateOrchestrateShowSlsPromise = this.api.getRunnerStateOrchestrateShowSls(); @@ -339,30 +368,6 @@ export class LoginPanel extends Panel { }); /* eslint-enable no-unused-vars */ - // allow the success message to be seen - window.setTimeout(() => { - // erase credentials since we don't do page-refresh - this.usernameField.value = ""; - this.passwordField.value = ""; - if (Utils.getStorageItem("session", "login_response") !== null) { - // we might have been logged out in this first second - // e.g. when clock between client and server differs more than the session timout - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.get("page")) { - // a redirect page is specified - const params = {}; - for (const pair of urlParams.entries()) { - params[pair[0]] = pair[1]; - } - const page = params["page"]; - delete params["page"]; - this.router.goTo(page, params); - } else { - this.router.goTo(""); - } - } - }, 1000); - BeaconsMinionPanel.getAvailableBeacons(this.api); } diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index 6cf6d370a..94b9201ab 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -4,7 +4,9 @@ const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; function reportIgnoredError (message, error) { - context.console?.debug?.(message, error); + if (context.console && typeof context.console.debug === "function") { + context.console.debug(message, error); + } } function getStoredTheme () { @@ -56,7 +58,7 @@ parentDoc.documentElement.dataset.theme || "", parentDoc.documentElement.getAttribute("theme") || "", parentDoc.documentElement.className || "", - parentDoc.body?.className || "", + parentDoc.body ? parentDoc.body.className : "", ].join(" ").toLowerCase(); } @@ -65,12 +67,22 @@ return null; } + // Newer engines can return space-separated rgb syntax such as: + // rgb(12 34 56 / 0.9) + value = value.replace(/\s*\/\s*/g, ", ").replace(/\s+/g, " "); + const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i); if (!rgbMatch) { return null; } - const channels = rgbMatch[1].split(",").map((channel) => Number.parseFloat(channel.trim())); + let channelParts = rgbMatch[1].split(",").map((channel) => channel.trim()).filter((channel) => channel !== ""); + if (channelParts.length === 1) { + // Legacy fallback for plain space-separated rgb without commas. + channelParts = channelParts[0].split(" ").map((channel) => channel.trim()).filter((channel) => channel !== ""); + } + + const channels = channelParts.map((channel) => Number.parseFloat(channel)); if (channels.length < 3 || channels.slice(0, 3).some((channel) => Number.isNaN(channel))) { return null; } @@ -155,7 +167,11 @@ }; applyTheme(); - mediaQuery?.addEventListener("change", applyTheme); + if (mediaQuery && typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", applyTheme); + } else if (mediaQuery && typeof mediaQuery.addListener === "function") { + mediaQuery.addListener(applyTheme); + } context.addEventListener("storage", applyTheme); const parentDoc = getParentDocument(); From e3d58cc850539a6421d01c8a126b511086136756 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 18:55:24 -0600 Subject: [PATCH 09/16] Keep Options theme helper pure and apply theme in change handler --- saltgui/static/scripts/panels/Options.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/saltgui/static/scripts/panels/Options.js b/saltgui/static/scripts/panels/Options.js index 2ab6007b3..9ef75e369 100644 --- a/saltgui/static/scripts/panels/Options.js +++ b/saltgui/static/scripts/panels/Options.js @@ -205,6 +205,9 @@ export class OptionsPanel extends Panel { } else if (pName === "theme") { radio.addEventListener("change", () => { this._newTheme(); + if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { + globalThis.SaltGUITheme.applyTheme(); + } }); } else if (pName === "use-cache-for-grains") { radio.addEventListener("change", () => { @@ -602,6 +605,5 @@ export class OptionsPanel extends Panel { const themeTd = this.div.querySelector("#option-theme-value"); themeTd.innerText = value; Utils.setStorageItem("session", "theme", value); - globalThis.SaltGUITheme?.applyTheme?.(); } } From 2c1863e357fc16fc51a8e5c9c8289f1db1f40d6f Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Wed, 15 Apr 2026 19:11:50 -0600 Subject: [PATCH 10/16] Revert unrelated Jobs parsing tweak and apply Login review nit --- saltgui/static/scripts/panels/Jobs.js | 4 ++-- saltgui/static/scripts/panels/Login.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/saltgui/static/scripts/panels/Jobs.js b/saltgui/static/scripts/panels/Jobs.js index bf4c9b0d9..0de70a741 100644 --- a/saltgui/static/scripts/panels/Jobs.js +++ b/saltgui/static/scripts/panels/Jobs.js @@ -296,8 +296,8 @@ export class JobsPanel extends Panel { // This element only exists when the user happens to look at the output of that jobId. const spans = this.div.querySelectorAll("#status" + jid); for (const span of spans) { - let oldLevel = Number(span.dataset.level); - if (Number.isNaN(oldLevel)) { + let oldLevel = span.dataset.level; + if (oldLevel === undefined) { oldLevel = 0; } if (newLevel > oldLevel) { diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index d9db2ffbc..c48626a74 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -395,6 +395,7 @@ export class LoginPanel extends Panel { static _handleLoginWheelConfigValues (pWheelConfigValuesData) { const wheelConfigValuesData = pWheelConfigValuesData.return[0].data.return; + const theme = wheelConfigValuesData.saltgui_theme || "auto"; Utils.setStorageItem("session", "theme", theme); From 9d267af1b5b2465c2065872b2727593d334b5415 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Thu, 16 Apr 2026 08:48:59 -0600 Subject: [PATCH 11/16] Move theme utility rules to existing CSS files and trim docs theme note --- docs/README.md | 3 -- saltgui/static/stylesheets/main.css | 4 +++ saltgui/static/stylesheets/page.css | 38 +++++++++++++++++++++++++ saltgui/static/stylesheets/theme.css | 42 ---------------------------- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/docs/README.md b/docs/README.md index aee197dd7..2ade6848d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -211,9 +211,6 @@ Allowed values are `auto`, `light`, and `dark`. With `auto`, SaltGUI follows the browser color-scheme preference and also uses theme hints from an embedding parent frame when those are available. With `light` and `dark`, SaltGUI uses the selected theme unconditionally. -The current value is also visible on the Settings page. -Changes made there are session-only and do not modify `/etc/salt/master`. - ## Templates SaltGUI supports command templates for easier command entry into the command-box. diff --git a/saltgui/static/stylesheets/main.css b/saltgui/static/stylesheets/main.css index afe12c081..ee8e39ede 100644 --- a/saltgui/static/stylesheets/main.css +++ b/saltgui/static/stylesheets/main.css @@ -190,6 +190,10 @@ h1 { padding: 15px 30px; } +.menu-item-dimmed { + color: var(--color-menu-dimmed); +} + .menu-item-active { font-weight: bold; } diff --git a/saltgui/static/stylesheets/page.css b/saltgui/static/stylesheets/page.css index d638381ef..c4fe4ab98 100644 --- a/saltgui/static/stylesheets/page.css +++ b/saltgui/static/stylesheets/page.css @@ -163,6 +163,13 @@ table tr td:last-of-type { opacity: 0.4; } +.button-in-text { + background-color: var(--color-background-control-muted); + border-radius: 2px; + color: var(--color-text-strong); + padding: 0 0.25em; +} + .menu-item-hidden { display: none; } @@ -186,6 +193,26 @@ table tr td:last-of-type { position: relative; } +.job-details-success, +.text-success { + color: var(--color-status-accepted); +} + +.job-details-warning, +.text-warning { + color: var(--color-status-caution); +} + +.job-details-failure, +.text-error { + color: var(--color-status-failure); +} + +.job-details-info, +.text-info { + color: var(--color-status-info); +} + .accepted { color: var(--color-status-accepted); } @@ -310,6 +337,17 @@ pre .task-failure-changes { cursor: pointer; } +.doc-inline-code { + background-color: var(--color-background-control-muted); + border-radius: 2px; + color: var(--color-text-primary); + padding: 0 0.25em; +} + +.doc-inline-highlight { + color: var(--color-text-code-link); +} + @media print { .no-print, .no-print * { diff --git a/saltgui/static/stylesheets/theme.css b/saltgui/static/stylesheets/theme.css index 5ac8d6d36..fae8c8b8b 100644 --- a/saltgui/static/stylesheets/theme.css +++ b/saltgui/static/stylesheets/theme.css @@ -111,45 +111,3 @@ --color-notice-muted: #5a7980; --color-notice-danger: #b93a48; } - -.menu-item-dimmed { - color: var(--color-menu-dimmed); -} - -.button-in-text { - background-color: var(--color-background-control-muted); - border-radius: 2px; - color: var(--color-text-strong); - padding: 0 0.25em; -} - -.job-details-success, -.text-success { - color: var(--color-status-accepted); -} - -.job-details-warning, -.text-warning { - color: var(--color-status-caution); -} - -.job-details-failure, -.text-error { - color: var(--color-status-failure); -} - -.job-details-info, -.text-info { - color: var(--color-status-info); -} - -.doc-inline-code { - background-color: var(--color-background-control-muted); - border-radius: 2px; - color: var(--color-text-primary); - padding: 0 0.25em; -} - -.doc-inline-highlight { - color: var(--color-text-code-link); -} From 6165bb6a5636a7008da8b5d82692b3a37ab788a1 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Thu, 16 Apr 2026 16:11:01 -0600 Subject: [PATCH 12/16] Fix failing test, move applyTheme out of config handler, fix dark mode text contrast and subtle nodegroup divider --- saltgui/static/scripts/panels/Login.js | 6 +++--- saltgui/static/stylesheets/theme.css | 4 ++-- tests/unit/Output.test.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index c48626a74..16bdfd704 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -349,6 +349,9 @@ export class LoginPanel extends Panel { // or determine visibility of menu items wheelConfigValuesPromise.then((pWheelConfigValuesData) => { LoginPanel._handleLoginWheelConfigValues(pWheelConfigValuesData); + if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { + globalThis.SaltGUITheme.applyTheme(); + } Router.updateMainMenu(); return true; }, () => false); @@ -400,9 +403,6 @@ export class LoginPanel extends Panel { Utils.setStorageItem("session", "theme", theme); Utils.setStorageItem("local", "theme_default", theme); - if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { - globalThis.SaltGUITheme.applyTheme(); - } // store for later use diff --git a/saltgui/static/stylesheets/theme.css b/saltgui/static/stylesheets/theme.css index fae8c8b8b..fe2b1fad1 100644 --- a/saltgui/static/stylesheets/theme.css +++ b/saltgui/static/stylesheets/theme.css @@ -83,12 +83,12 @@ --color-text-quiet: #9ab8b5; --color-text-accent: #22c7bd; --color-text-link: #8fc4ff; - --color-text-inverse: #062126; + --color-text-inverse: #c8dde0; --color-text-code-link: #ffe38f; --color-text-tooltip: #062126; --color-text-tooltip-error: #24050a; --color-text-warning: #ffe3a2; - --color-border-accent: rgba(34, 199, 189, 45%); + --color-border-accent: rgba(34, 199, 189, 20%); --color-border-default: rgba(154, 184, 181, 18%); --color-border-control: rgba(154, 184, 181, 18%); --color-shadow-panel: 0 18px 40px rgba(0, 0, 0, 38%); diff --git a/tests/unit/Output.test.js b/tests/unit/Output.test.js index 9112d5598..41bc23ad0 100644 --- a/tests/unit/Output.test.js +++ b/tests/unit/Output.test.js @@ -433,7 +433,7 @@ describe("Unittests for Output.js", () => { OutputDocumentation.addDocumentationOutput(container, output); assert.isTrue( container.innerHTML.includes( - "systemd-run(1)")); + "systemd-run(1)")); done(); }); From bbd832d5774869c15b027fad08b1ac9cf6e3e11f Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Thu, 16 Apr 2026 16:25:43 -0600 Subject: [PATCH 13/16] Increase dark output text contrast and further soften nodegroup divider --- saltgui/static/stylesheets/theme.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/saltgui/static/stylesheets/theme.css b/saltgui/static/stylesheets/theme.css index fe2b1fad1..a9e4855e5 100644 --- a/saltgui/static/stylesheets/theme.css +++ b/saltgui/static/stylesheets/theme.css @@ -83,12 +83,12 @@ --color-text-quiet: #9ab8b5; --color-text-accent: #22c7bd; --color-text-link: #8fc4ff; - --color-text-inverse: #c8dde0; + --color-text-inverse: #e6f7f5; --color-text-code-link: #ffe38f; --color-text-tooltip: #062126; --color-text-tooltip-error: #24050a; --color-text-warning: #ffe3a2; - --color-border-accent: rgba(34, 199, 189, 20%); + --color-border-accent: rgba(34, 199, 189, 12%); --color-border-default: rgba(154, 184, 181, 18%); --color-border-control: rgba(154, 184, 181, 18%); --color-shadow-panel: 0 18px 40px rgba(0, 0, 0, 38%); From 4f660aff9bf459c89230e4aa51e19dad58714d60 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Thu, 16 Apr 2026 19:10:15 -0600 Subject: [PATCH 14/16] Remove in-app theme setting and follow browser theme only --- docs/README.md | 11 ++----- saltgui/static/scripts/panels/Login.js | 8 ----- saltgui/static/scripts/panels/Options.js | 23 -------------- saltgui/static/scripts/theme.js | 38 ++---------------------- 4 files changed, 5 insertions(+), 75 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2ade6848d..55a74d9e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -201,15 +201,8 @@ When using very old browsers, the required date/time functions may not be presen ## Theme -SaltGUI can follow the browser preference automatically, or it can be forced to a specific theme by adding the following parameter to salt master configuration file `/etc/salt/master`. -e.g.: -``` -saltgui_theme: auto -``` - -Allowed values are `auto`, `light`, and `dark`. -With `auto`, SaltGUI follows the browser color-scheme preference and also uses theme hints from an embedding parent frame when those are available. -With `light` and `dark`, SaltGUI uses the selected theme unconditionally. +SaltGUI follows the browser color-scheme preference. +When SaltGUI is embedded, it also uses theme hints from the parent frame when those are available. ## Templates diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 16bdfd704..ebdf726a6 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -349,9 +349,6 @@ export class LoginPanel extends Panel { // or determine visibility of menu items wheelConfigValuesPromise.then((pWheelConfigValuesData) => { LoginPanel._handleLoginWheelConfigValues(pWheelConfigValuesData); - if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { - globalThis.SaltGUITheme.applyTheme(); - } Router.updateMainMenu(); return true; }, () => false); @@ -399,11 +396,6 @@ export class LoginPanel extends Panel { static _handleLoginWheelConfigValues (pWheelConfigValuesData) { const wheelConfigValuesData = pWheelConfigValuesData.return[0].data.return; - const theme = wheelConfigValuesData.saltgui_theme || "auto"; - - Utils.setStorageItem("session", "theme", theme); - Utils.setStorageItem("local", "theme_default", theme); - // store for later use const templates = wheelConfigValuesData.saltgui_templates; diff --git a/saltgui/static/scripts/panels/Options.js b/saltgui/static/scripts/panels/Options.js index 9ef75e369..7b331c7dc 100644 --- a/saltgui/static/scripts/panels/Options.js +++ b/saltgui/static/scripts/panels/Options.js @@ -108,10 +108,6 @@ export class OptionsPanel extends Panel { "tooltip-mode", "saltgui", "full", [["mode", "full", "simple", "none"]] ], - [ - "theme", "saltgui", "auto", - [["theme", "auto", "light", "dark"]] - ], /* last because it might be very long */ ["custom-command-help", "saltgui", "(none)"] @@ -202,13 +198,6 @@ export class OptionsPanel extends Panel { radio.addEventListener("change", () => { this._newFullReturn(); }); - } else if (pName === "theme") { - radio.addEventListener("change", () => { - this._newTheme(); - if (globalThis.SaltGUITheme && typeof globalThis.SaltGUITheme.applyTheme === "function") { - globalThis.SaltGUITheme.applyTheme(); - } - }); } else if (pName === "use-cache-for-grains") { radio.addEventListener("change", () => { this._newUseCacheForGrains(); @@ -594,16 +583,4 @@ export class OptionsPanel extends Panel { fullReturnTd.innerText = value; Utils.setStorageItem("session", "full_return", value); } - - _newTheme () { - let value = ""; - /* eslint-disable curly */ - if (this._isSelected("theme", "theme", "auto")) value = "auto"; - if (this._isSelected("theme", "theme", "light")) value = "light"; - if (this._isSelected("theme", "theme", "dark")) value = "dark"; - /* eslint-enable curly */ - const themeTd = this.div.querySelector("#option-theme-value"); - themeTd.innerText = value; - Utils.setStorageItem("session", "theme", value); - } } diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index 94b9201ab..0d57c50ce 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -2,6 +2,7 @@ const context = globalThis; const root = document.documentElement; const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; + const configuredTheme = "auto"; function reportIgnoredError (message, error) { if (context.console && typeof context.console.debug === "function") { @@ -9,32 +10,6 @@ } } - function getStoredTheme () { - try { - const sessionTheme = context.sessionStorage ? context.sessionStorage.getItem("theme") : null; - if (sessionTheme) { - return sessionTheme; - } - const defaultTheme = context.localStorage ? context.localStorage.getItem("theme_default") : null; - if (defaultTheme) { - return defaultTheme; - } - } catch (error) { - // Storage access can fail in restricted browser environments. - reportIgnoredError("SaltGUI theme: storage unavailable", error); - } - - return "auto"; - } - - function getConfiguredTheme () { - const theme = (getStoredTheme() || "auto").toLowerCase(); - if (theme === "light" || theme === "dark") { - return theme; - } - return "auto"; - } - function getParentDocument () { if (context.self === context.top) { return null; @@ -131,7 +106,7 @@ return null; } - function wantsDarkTheme (configuredTheme) { + function wantsDarkTheme () { if (configuredTheme === "dark") { return true; } @@ -156,23 +131,16 @@ } function applyTheme () { - const configuredTheme = getConfiguredTheme(); root.dataset.themePreference = configuredTheme; - root.dataset.theme = wantsDarkTheme(configuredTheme) ? "dark" : "light"; + root.dataset.theme = wantsDarkTheme() ? "dark" : "light"; } - context.SaltGUITheme = { - applyTheme, - getConfiguredTheme, - }; - applyTheme(); if (mediaQuery && typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", applyTheme); } else if (mediaQuery && typeof mediaQuery.addListener === "function") { mediaQuery.addListener(applyTheme); } - context.addEventListener("storage", applyTheme); const parentDoc = getParentDocument(); if (!parentDoc) { From 73d47bd81ee8e9148401ba60d57ec0f6ca0e80e5 Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Thu, 16 Apr 2026 21:02:42 -0600 Subject: [PATCH 15/16] Address final review nits: nodegroup border token, themed login logo, theme cleanup --- saltgui/static/scripts/panels/Login.js | 10 +++++++++- saltgui/static/scripts/panels/Nodegroups.js | 2 +- saltgui/static/scripts/theme.js | 9 --------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index ebdf726a6..844d70c59 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -66,7 +66,15 @@ export class LoginPanel extends Panel { aa.rel = "noopener"; const img = Utils.createElem("img"); - img.src = "static/images/GitHub_Invertocat_Black.png"; + const setGitHubLogo = () => { + const isDarkTheme = document.documentElement.dataset.theme === "dark"; + img.src = isDarkTheme ? "static/images/GitHub_Invertocat_White.png" : "static/images/GitHub_Invertocat_Black.png"; + }; + setGitHubLogo(); + new MutationObserver(setGitHubLogo).observe(document.documentElement, { + attributeFilter: ["data-theme"], + attributes: true, + }); img.style = "width: 1em; margin-right: 5px"; aa.append(img); diff --git a/saltgui/static/scripts/panels/Nodegroups.js b/saltgui/static/scripts/panels/Nodegroups.js index 4506571f9..c141207a4 100644 --- a/saltgui/static/scripts/panels/Nodegroups.js +++ b/saltgui/static/scripts/panels/Nodegroups.js @@ -308,7 +308,7 @@ export class NodegroupsPanel extends Panel { _addNodegroupRow (pNodegroup, pAllNodegroups) { const tr = Utils.createTr("no-search", null, "ng-" + pNodegroup); - tr.style.borderTop = "4px double #ddd"; + tr.style.borderTop = "4px double var(--color-border-default)"; const menuTd = Utils.createTd(); tr.dropdownmenu = new DropDownMenu(menuTd, "smaller"); diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index 0d57c50ce..904653df3 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -2,7 +2,6 @@ const context = globalThis; const root = document.documentElement; const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; - const configuredTheme = "auto"; function reportIgnoredError (message, error) { if (context.console && typeof context.console.debug === "function") { @@ -107,13 +106,6 @@ } function wantsDarkTheme () { - if (configuredTheme === "dark") { - return true; - } - if (configuredTheme === "light") { - return false; - } - const hints = getParentHints(); if (/(^|\s)(light)(\s|$)/.test(hints)) { return false; @@ -131,7 +123,6 @@ } function applyTheme () { - root.dataset.themePreference = configuredTheme; root.dataset.theme = wantsDarkTheme() ? "dark" : "light"; } From e33aacd0d8088697dce2bdb76ffed683db2e935c Mon Sep 17 00:00:00 2001 From: Tyler Levy Conde Date: Fri, 17 Apr 2026 20:36:06 -0600 Subject: [PATCH 16/16] Simplify dark_mode branch per reviewer feedback: remove parent frame theme detection (HA-specific), simplify login logo, remove ingress bootstrapSession --- saltgui/static/scripts/Router.js | 6 -- saltgui/static/scripts/panels/Login.js | 10 +- saltgui/static/scripts/theme.js | 136 ------------------------- 3 files changed, 1 insertion(+), 151 deletions(-) diff --git a/saltgui/static/scripts/Router.js b/saltgui/static/scripts/Router.js index 304400ff9..86b43cc94 100644 --- a/saltgui/static/scripts/Router.js +++ b/saltgui/static/scripts/Router.js @@ -73,12 +73,6 @@ export class Router { Router.updateMainMenu(); - // In embedded ingress mode, the login callback path may not run. - // Load session-dependent settings (including theme preference) when already authenticated. - if (Utils.getStorageItem("session", "login_response") !== null) { - Router.loginPage.login.bootstrapSession(); - } - const hash = window.location.hash.replace(/^#/, ""); const search = window.location.search; /* eslint-disable compat/compat */ diff --git a/saltgui/static/scripts/panels/Login.js b/saltgui/static/scripts/panels/Login.js index 844d70c59..ebdf726a6 100644 --- a/saltgui/static/scripts/panels/Login.js +++ b/saltgui/static/scripts/panels/Login.js @@ -66,15 +66,7 @@ export class LoginPanel extends Panel { aa.rel = "noopener"; const img = Utils.createElem("img"); - const setGitHubLogo = () => { - const isDarkTheme = document.documentElement.dataset.theme === "dark"; - img.src = isDarkTheme ? "static/images/GitHub_Invertocat_White.png" : "static/images/GitHub_Invertocat_Black.png"; - }; - setGitHubLogo(); - new MutationObserver(setGitHubLogo).observe(document.documentElement, { - attributeFilter: ["data-theme"], - attributes: true, - }); + img.src = "static/images/GitHub_Invertocat_Black.png"; img.style = "width: 1em; margin-right: 5px"; aa.append(img); diff --git a/saltgui/static/scripts/theme.js b/saltgui/static/scripts/theme.js index 904653df3..a3c383b39 100644 --- a/saltgui/static/scripts/theme.js +++ b/saltgui/static/scripts/theme.js @@ -3,122 +3,7 @@ const root = document.documentElement; const mediaQuery = context.matchMedia ? context.matchMedia("(prefers-color-scheme: dark)") : null; - function reportIgnoredError (message, error) { - if (context.console && typeof context.console.debug === "function") { - context.console.debug(message, error); - } - } - - function getParentDocument () { - if (context.self === context.top) { - return null; - } - try { - return context.parent.document; - } catch (error) { - // Access to the parent frame can fail outside an embedded environment. - reportIgnoredError("SaltGUI theme: parent frame unavailable", error); - return null; - } - } - - function getParentHints () { - const parentDoc = getParentDocument(); - if (!parentDoc) { - return ""; - } - - return [ - parentDoc.documentElement.dataset.theme || "", - parentDoc.documentElement.getAttribute("theme") || "", - parentDoc.documentElement.className || "", - parentDoc.body ? parentDoc.body.className : "", - ].join(" ").toLowerCase(); - } - - function parseColor (value) { - if (!value || value === "transparent") { - return null; - } - - // Newer engines can return space-separated rgb syntax such as: - // rgb(12 34 56 / 0.9) - value = value.replace(/\s*\/\s*/g, ", ").replace(/\s+/g, " "); - - const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i); - if (!rgbMatch) { - return null; - } - - let channelParts = rgbMatch[1].split(",").map((channel) => channel.trim()).filter((channel) => channel !== ""); - if (channelParts.length === 1) { - // Legacy fallback for plain space-separated rgb without commas. - channelParts = channelParts[0].split(" ").map((channel) => channel.trim()).filter((channel) => channel !== ""); - } - - const channels = channelParts.map((channel) => Number.parseFloat(channel)); - if (channels.length < 3 || channels.slice(0, 3).some((channel) => Number.isNaN(channel))) { - return null; - } - - const alpha = channels.length > 3 ? channels[3] : 1; - if (Number.isNaN(alpha) || alpha <= 0) { - return null; - } - - return channels.slice(0, 3); - } - - function isDarkColor (value) { - const channels = parseColor(value); - if (!channels) { - return null; - } - - const [red, green, blue] = channels; - const brightness = (red * 299 + green * 587 + blue * 114) / 1000; - return brightness < 140; - } - - function getParentComputedTheme () { - const parentDoc = getParentDocument(); - if (!parentDoc || !context.getComputedStyle) { - return null; - } - - try { - const candidates = [context.getComputedStyle(parentDoc.documentElement).backgroundColor]; - if (parentDoc.body) { - candidates.push(context.getComputedStyle(parentDoc.body).backgroundColor); - } - - for (const candidate of candidates) { - const isDark = isDarkColor(candidate); - if (isDark !== null) { - return isDark; - } - } - } catch (error) { - reportIgnoredError("SaltGUI theme: parent style unavailable", error); - } - - return null; - } - function wantsDarkTheme () { - const hints = getParentHints(); - if (/(^|\s)(light)(\s|$)/.test(hints)) { - return false; - } - if (/(^|\s)(dark|night)(\s|$)/.test(hints)) { - return true; - } - - const computedTheme = getParentComputedTheme(); - if (computedTheme !== null) { - return computedTheme; - } - return mediaQuery ? mediaQuery.matches : false; } @@ -132,25 +17,4 @@ } else if (mediaQuery && typeof mediaQuery.addListener === "function") { mediaQuery.addListener(applyTheme); } - - const parentDoc = getParentDocument(); - if (!parentDoc) { - return; - } - - try { - const observer = new MutationObserver(applyTheme); - observer.observe(parentDoc.documentElement, { - attributeFilter: ["class", "data-theme", "theme"], - attributes: true, - }); - if (parentDoc.body) { - observer.observe(parentDoc.body, { - attributeFilter: ["class", "data-theme", "theme"], - attributes: true, - }); - } - } catch (error) { - reportIgnoredError("SaltGUI theme: parent observer unavailable", error); - } })();