Add dynamic UI theme for server browser

Introduce runtime-driven UI theming for the server browser

- Expose PushUITheme() in F4TServerBrowserBridge and implement ColorToJson + PushUIThemeInternal to serialize HUD colors and invoke a JS hook.
- Call PushUITheme on UI initialization and when the browser is shown so the webview receives current HUD colors.
- Add frontend handler for "uiTheme" in app.js and a new components/theme.js that implements window.applyCoTheme to set CSS vars and tint the cursor SVG.
- Update styles.css to use RGB component CSS variables (--co-r/--co-g/--co-b and background equivalents) so colors can be overridden at runtime.
- Update index.html to include the new theme script and tweak cursor SVG stroke width; cursor asset updated as well.

This enables the plugin to synchronize the in-game HUD colors with the browser UI dynamically.
This commit is contained in:
2026-06-09 15:19:03 +12:00
parent 1f80b8ea49
commit fb4e238fcc
9 changed files with 131 additions and 17 deletions
@@ -482,6 +482,11 @@
case "connectionStatusChanged":
state.connectionStatus = event.status || "Online";
break;
case "uiTheme":
if (typeof window.applyCoTheme === "function") {
window.applyCoTheme(event);
}
break;
case "menuOpened":
break;
case "menuClosed":
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 911 B

@@ -6,6 +6,7 @@
.st0 {
fill: #fff;
stroke: #000;
stroke-width: 3.2px;
stroke-miterlimit: 10;
}
</style>

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 479 B

@@ -0,0 +1,55 @@
(function () {
"use strict";
function clampByte(value) {
return Math.max(0, Math.min(255, Math.round(value)));
}
function applyHudRgb(root, prefix, color) {
if (!color) {
return;
}
root.style.setProperty(prefix + "-r", String(clampByte(color.r)));
root.style.setProperty(prefix + "-g", String(clampByte(color.g)));
root.style.setProperty(prefix + "-b", String(clampByte(color.b)));
}
function applyCursorTint(root, color) {
if (!color) {
return;
}
var r = clampByte(color.r);
var g = clampByte(color.g);
var b = clampByte(color.b);
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.3 31.24">' +
'<polygon fill="rgb(' + r + "," + g + "," + b + ')" stroke="#000" stroke-width="3.2" ' +
'stroke-miterlimit="10" points="17.4 30.41 31.46 15.9 1.2 1.14 17.4 30.41"/></svg>';
var url =
'url("data:image/svg+xml,' +
encodeURIComponent(svg) +
'") 1 1, default';
root.style.setProperty("--co-cursor", url);
}
window.applyCoTheme = function (theme) {
if (!theme) {
return;
}
var root = document.documentElement;
var hud = theme.hud || theme;
applyHudRgb(root, "--co", hud);
applyHudRgb(root, "--co-bg", theme.hudBackground);
applyCursorTint(root, hud);
console.log(
"[CO] Applied HUD theme rgb(" +
clampByte(hud.r) + "," +
clampByte(hud.g) + "," +
clampByte(hud.b) + ")"
);
};
})();
@@ -79,6 +79,7 @@
</div>
</div>
<script src="./components/theme.js"></script>
<script src="./components/mock-data.js"></script>
<script src="./components/tabs.js"></script>
<script src="./components/filters.js"></script>
+28 -17
View File
@@ -7,17 +7,28 @@
}
:root {
--co-green: #2dff28;
--co-green-soft: #2dff28;
--co-green-dim: rgba(45, 255, 40, 0.22);
--co-bg: rgba(0, 12, 0, 0.62);
--co-bg-dark: rgba(0, 10, 0, 0.72);
/* RGB components — overridden at runtime from Fallout4Prefs.ini via plugin. */
--co-r: 18;
--co-g: 255;
--co-b: 21;
--co-bg-r: 0;
--co-bg-g: 10;
--co-bg-b: 0;
--co-green: rgb(var(--co-r), var(--co-g), var(--co-b));
--co-green-soft: rgb(var(--co-r), var(--co-g), var(--co-b));
--co-green-dim: rgba(var(--co-r), var(--co-g), var(--co-b), 0.22);
--co-bg: rgba(var(--co-bg-r), var(--co-bg-g), var(--co-bg-b), 0.62);
--co-bg-dark: rgba(var(--co-bg-r), var(--co-bg-g), var(--co-bg-b), 0.72);
--co-bg-list: transparent;
--co-selected: #2dff28;
--co-selected-text: #001400;
--co-border: #2dff28;
--co-text-dim: rgba(45, 255, 40, 0.5);
--co-ping-good: #2dff28;
--co-selected: rgb(var(--co-r), var(--co-g), var(--co-b));
--co-selected-text: rgb(
calc(var(--co-r) * 0.05),
calc(var(--co-g) * 0.05),
calc(var(--co-b) * 0.05)
);
--co-border: rgb(var(--co-r), var(--co-g), var(--co-b));
--co-text-dim: rgba(var(--co-r), var(--co-g), var(--co-b), 0.5);
--co-ping-good: rgb(var(--co-r), var(--co-g), var(--co-b));
--co-ping-mid: #c8ff28;
--co-ping-bad: #ffd428;
--co-danger: #ff6a6a;
@@ -162,7 +173,7 @@ body {
}
.co-nav-item:hover:not(.active) {
background: rgba(45, 255, 40, 0.08);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
}
.co-nav-item.active {
@@ -171,11 +182,11 @@ body {
}
.co-nav-item.focused:not(.active) {
background: rgba(45, 255, 40, 0.12);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.12);
}
.co-nav-list.focused .co-nav-item.focused:not(.active) {
background: rgba(45, 255, 40, 0.12);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.12);
}
/* Right content */
@@ -418,11 +429,11 @@ body {
}
.co-btn:hover {
background: rgba(45, 255, 40, 0.1);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.1);
}
.co-btn.focused {
background: rgba(45, 255, 40, 0.18);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.18);
outline: 1px solid var(--co-green);
}
@@ -436,7 +447,7 @@ body {
}
.co-server-list-wrap.focused {
background: rgba(45, 255, 40, 0.03);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.03);
}
.co-server-list {
@@ -468,7 +479,7 @@ body {
}
.co-server-row:hover:not(.selected) {
background: rgba(45, 255, 40, 0.06);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
}
.co-server-row.selected {