Add hover sync and CO hover component

Add a hover system to restore :hover-like behavior for custom UI elements in the CommonwealthOnline view. Changes include:

- plugin/src/F4TPrismaUI.cpp: add SyncBrowserHover() which reads the native cursor position, converts it to client coordinates, and invokes a JS update on the server browser view; register a periodic task to call SyncBrowserHover every other tick.
- ui/views/CommonwealthOnline/browser/components/hover.js: new component that tracks pointer position, determines hovered targets using a selector list, and applies/removes a .hovered class; exposes bind(), updateFromPoint(), and clear().
- ui/views/CommonwealthOnline/browser/app.js & index.html: include and bind the new hover component during UI initialization.
- ui/views/CommonwealthOnline/browser/styles.css: add .hovered variants and transitions for nav items, buttons, rows, and filter controls to match hover visuals.
- steam_interfaces.txt: add a list of Steam interface version strings (new file).

Purpose: ensure custom buttons/rows that don't receive native :hover updates from the renderer get visible hover feedback by syncing the native cursor into the web UI and toggling a .hovered class.
This commit is contained in:
2026-06-16 15:55:32 +12:00
parent 7ab3f080f7
commit 31406a1275
6 changed files with 206 additions and 5 deletions
@@ -296,6 +296,10 @@
}
function bindUi() {
if (window.CO_Hover) {
CO_Hover.bind(root);
}
CO_Tabs.bind(root, function (tabName) {
switchTab(tabName);
});
@@ -0,0 +1,118 @@
(function () {
"use strict";
/*
* PrismaUI / Ultralight updates :hover only for native form controls (input, select).
* Custom buttons and rows need a .hovered class driven by cursor position.
*/
var HOVER_SELECTOR = [
".co-nav-item",
".co-server-row",
".co-btn",
".co-checkbox-row"
].join(", ");
var currentHovered = null;
var lastX = null;
var lastY = null;
var rafId = null;
var boundPanel = null;
function findHoverTarget(node) {
if (!node || !node.closest) {
return null;
}
var target = node.closest(HOVER_SELECTOR);
if (!target || !boundPanel || !boundPanel.contains(target)) {
return null;
}
return target;
}
function setHovered(target) {
if (target === currentHovered) {
return;
}
if (currentHovered) {
currentHovered.classList.remove("hovered");
}
currentHovered = target;
if (currentHovered) {
currentHovered.classList.add("hovered");
}
}
function clearHovered() {
setHovered(null);
lastX = null;
lastY = null;
}
function isInsidePanel(x, y) {
if (!boundPanel) {
return false;
}
var rect = boundPanel.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}
function updateFromPoint(x, y) {
if (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) {
return;
}
lastX = x;
lastY = y;
if (currentHovered && !document.body.contains(currentHovered)) {
currentHovered = null;
}
if (!isInsidePanel(x, y)) {
setHovered(null);
return;
}
var node = document.elementFromPoint(x, y);
setHovered(findHoverTarget(node));
}
function onPointerEvent(e) {
updateFromPoint(e.clientX, e.clientY);
}
function tick() {
if (lastX !== null && lastY !== null) {
updateFromPoint(lastX, lastY);
}
rafId = window.requestAnimationFrame(tick);
}
window.CO_Hover = {
bind: function (root) {
boundPanel = root && root.querySelector(".co-panel");
if (!boundPanel) {
return;
}
document.addEventListener("mousemove", onPointerEvent, true);
document.addEventListener("pointermove", onPointerEvent, true);
boundPanel.addEventListener("mouseleave", clearHovered);
if (!rafId) {
rafId = window.requestAnimationFrame(tick);
}
},
updateFromPoint: updateFromPoint,
clear: clearHovered
};
})();
@@ -87,6 +87,7 @@
<script src="./components/server-details.js"></script>
<script src="./components/controller-prompts.js"></script>
<script src="./components/direct-connect.js"></script>
<script src="./components/hover.js"></script>
<script src="./app.js"></script>
</body>
</html>
+27 -5
View File
@@ -172,10 +172,15 @@ body {
transition: background var(--co-transition), color var(--co-transition);
}
.co-nav-item:hover:not(.active) {
.co-nav-item:hover:not(.active),
.co-nav-item.hovered:not(.active) {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
}
.co-nav-item.active.hovered {
filter: brightness(1.08);
}
.co-nav-item.active {
background: var(--co-selected);
color: var(--co-selected-text);
@@ -369,12 +374,20 @@ body {
padding: 5px 8px;
border-radius: 0;
cursor: var(--co-cursor);
transition: border-color var(--co-transition);
}
.co-filter-input::placeholder {
color: var(--co-text-dim);
}
.co-filter-input:hover,
.co-filter-input.hovered,
.co-filter-select:hover,
.co-filter-select.hovered {
border-color: rgba(var(--co-r), var(--co-g), var(--co-b), 0.45);
}
.co-filter-input:focus,
.co-filter-select:focus {
border-color: var(--co-border);
@@ -390,6 +403,12 @@ body {
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: var(--co-cursor);
transition: background var(--co-transition), border-color var(--co-transition);
}
.co-checkbox-row:hover,
.co-checkbox-row.hovered {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
}
.co-checkbox {
@@ -425,11 +444,13 @@ body {
cursor: var(--co-cursor);
margin-left: auto;
flex-shrink: 0;
transition: background var(--co-transition);
transition: background var(--co-transition), border-color var(--co-transition);
}
.co-btn:hover {
.co-btn:hover,
.co-btn.hovered {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.1);
border-color: rgba(var(--co-r), var(--co-g), var(--co-b), 0.45);
}
.co-btn.focused {
@@ -475,10 +496,11 @@ body {
font-size: var(--co-font-md);
line-height: 1.2;
cursor: var(--co-cursor);
transition: background var(--co-transition);
transition: background var(--co-transition), color var(--co-transition);
}
.co-server-row:hover:not(.selected) {
.co-server-row:hover:not(.selected),
.co-server-row.hovered:not(.selected) {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
}