Embed server browser as main menu modal

Refactors the browser UI into a reusable `CO_Browser.create()` module and adds embedded modal styling so MULTIPLAYER opens inside the main menu instead of swapping to a separate view. Updates main-menu JS/CSS to host, animate, and route input for the browser panel while keeping nav and prompts in sync. On the C++ side, PrismaUI now drives browser show/hide and event dispatch through the main menu view, and `ServerBrowserBridge::SetView()` was added so bridge events target the active host view.
This commit is contained in:
2026-07-07 17:35:15 +12:00
parent 2961eb6512
commit 9447bc308f
12 changed files with 1514 additions and 982 deletions
@@ -5,6 +5,8 @@
<title>Commonwealth Online — Main Menu</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="../settings/settings.css">
<link rel="stylesheet" href="../browser/styles.css">
<link rel="stylesheet" href="../browser/browser-modal.css">
</head>
<body>
<div class="main-screen" id="mainScreen">
@@ -56,6 +58,9 @@
<!-- Settings view (content mounted by ../settings/settings.js) -->
<section class="settings-view" id="settingsPanel" aria-label="Settings" hidden></section>
<!-- Server browser view (content mounted by ../browser/browser.js) -->
<section class="browser-view" id="browserPanel" aria-label="Server browser" hidden></section>
<!-- Bottom controller prompts (main menu) -->
<footer class="prompt-bar" id="mainPromptBar" aria-label="Controller prompts">
<div class="prompt">
@@ -103,6 +108,19 @@
<script src="../settings/settings-data.js"></script>
<script src="../settings/settings.js"></script>
<script src="../browser/components/layout.js"></script>
<script src="../browser/components/theme.js"></script>
<script src="../browser/components/mock-data.js"></script>
<script src="../browser/components/tabs.js"></script>
<script src="../browser/components/filters.js"></script>
<script src="../browser/components/server-list.js"></script>
<script src="../browser/components/server-details.js"></script>
<script src="../browser/components/controller-glyphs.js"></script>
<script src="../browser/components/controller-icons.js"></script>
<script src="../browser/components/controller-prompts.js"></script>
<script src="../browser/components/direct-connect.js"></script>
<script src="../browser/components/hover.js"></script>
<script src="../browser/browser.js"></script>
<script src="script.js"></script>
</body>
</html>
+220 -31
View File
@@ -17,34 +17,18 @@
}
};
// ── Pointer mode API for C++ invocation ─────────────────────────
window.CO_App = {
setPointerMode: function (mode) {
console.log("[CO Main Menu] Pointer mode:", mode);
}
};
// ── Pointer mode API for C++ invocation (patched after browser mount) ──
window.CO_App = {};
// ── Controller input handler ─────────────────────────────────────
window.handleControllerInput = function (button, phase) {
if (phase === "down") {
switch (button) {
case "dpad_up":
moveSelection(-1);
break;
case "dpad_down":
moveSelection(1);
break;
case "a":
activateSelected();
break;
case "b":
if (viewMode === "settings") {
exitSettingsMode();
}
break;
}
function sendCoAction(payload) {
var msg = JSON.stringify(payload);
if (typeof window.coAction === "function") {
window.coAction(msg);
return true;
}
};
console.warn("[CO Main Menu] coAction bridge unavailable:", payload);
return false;
}
// ── Compute UI scale ───────────────────────────────────────────
function updateScale() {
@@ -180,24 +164,29 @@
var mainScreen = document.getElementById("mainScreen");
var menu = document.getElementById("menu");
var settingsPanel = document.getElementById("settingsPanel");
var browserPanel = document.getElementById("browserPanel");
var mainPromptBar = document.getElementById("mainPromptBar");
var settingsPromptBar = document.getElementById("settingsPromptBar");
if (!menu || !settingsPanel || !window.CO_Settings) {
if (!menu || !settingsPanel || !browserPanel || !window.CO_Settings || !window.CO_Browser) {
return;
}
var settingsController = null;
var browserController = null;
var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item"));
var MULTIPLAYER_INDEX = 0;
var SETTINGS_INDEX = 2;
// viewMode: "menu" | "settings"
// viewMode: "menu" | "settings" | "browser"
var viewMode = "menu";
var selectedIndex = -1;
var SETTINGS_TRANSITION_MS = 220;
var PANEL_TRANSITION_MS = 220;
var settingsHideFallbackId = 0;
var settingsHideOnTransitionEnd = null;
var browserHideFallbackId = 0;
var browserHideOnTransitionEnd = null;
// ── Main menu selection ────────────────────────────────────────
@@ -252,6 +241,8 @@
if (action === "settings") {
enterSettingsMode();
} else if (action === "multiplayer") {
enterBrowserMode();
} else {
window.CommonwealthOnlineUI.onOpenView(action);
if (action !== "quit") {
@@ -267,6 +258,11 @@
return;
}
exitSettingsMode();
} else if (viewMode === "browser") {
if (index === MULTIPLAYER_INDEX) {
return;
}
exitBrowserMode();
}
if (item.classList.contains("is-disabled")) {
return;
@@ -342,10 +338,151 @@
};
settingsPanel.addEventListener("transitionend", settingsHideOnTransitionEnd);
settingsHideFallbackId = window.setTimeout(finishHide, SETTINGS_TRANSITION_MS + 80);
settingsHideFallbackId = window.setTimeout(finishHide, PANEL_TRANSITION_MS + 80);
}
function cancelBrowserPanelHide() {
if (browserHideFallbackId) {
window.clearTimeout(browserHideFallbackId);
browserHideFallbackId = 0;
}
if (browserHideOnTransitionEnd) {
browserPanel.removeEventListener("transitionend", browserHideOnTransitionEnd);
browserHideOnTransitionEnd = null;
}
}
function showBrowserPanelAnimated() {
cancelBrowserPanelHide();
if (mainScreen) {
mainScreen.classList.add("is-browser-mode");
}
browserPanel.hidden = false;
browserPanel.classList.remove("is-open");
void browserPanel.offsetWidth;
browserPanel.classList.add("is-open");
}
function hideBrowserPanelAnimated(done) {
if (browserPanel.hidden) {
if (done) {
done();
}
return;
}
cancelBrowserPanelHide();
browserPanel.classList.remove("is-open");
var completed = false;
function finishHide() {
if (completed) {
return;
}
completed = true;
cancelBrowserPanelHide();
if (viewMode === "browser" || browserPanel.classList.contains("is-open")) {
if (done) {
done();
}
return;
}
if (mainScreen) {
mainScreen.classList.remove("is-browser-mode");
}
browserPanel.hidden = true;
if (done) {
done();
}
}
browserHideOnTransitionEnd = function (event) {
if (event.target !== browserPanel || event.propertyName !== "opacity") {
return;
}
finishHide();
};
browserPanel.addEventListener("transitionend", browserHideOnTransitionEnd);
browserHideFallbackId = window.setTimeout(finishHide, PANEL_TRANSITION_MS + 80);
}
function enterBrowserMode(fromNative) {
if (viewMode === "browser") {
if (fromNative) {
browserController.onShown();
}
return;
}
if (viewMode === "settings") {
exitSettingsMode();
}
viewMode = "browser";
console.log("[CO Main Menu] Entering browser mode");
window.CommonwealthOnlineUI.onOpenView("multiplayer");
setSelected(MULTIPLAYER_INDEX);
showBrowserPanelAnimated();
if (mainPromptBar) {
mainPromptBar.hidden = true;
}
browserController.show();
browserController.onShown();
if (!fromNative) {
sendCoAction({ action: "openView", view: "multiplayer" });
}
}
function exitBrowserMode(fromNative) {
if (viewMode !== "browser") {
return;
}
viewMode = "menu";
console.log("[CO Main Menu] Exiting browser mode");
window.CommonwealthOnlineUI.onBack();
browserController.hide();
hideBrowserPanelAnimated(function () {
if (mainPromptBar) {
mainPromptBar.hidden = false;
}
if (viewMode === "menu") {
setSelected(MULTIPLAYER_INDEX);
}
});
if (!fromNative) {
sendCoAction({ action: "closeBrowser" });
}
}
window.onBrowserShown = function () {
enterBrowserMode(true);
};
window.closeBrowser = function () {
exitBrowserMode(true);
};
function enterSettingsMode() {
if (viewMode === "browser") {
cancelBrowserPanelHide();
browserController.hide();
browserPanel.classList.remove("is-open");
browserPanel.hidden = true;
if (mainScreen) {
mainScreen.classList.remove("is-browser-mode");
}
}
viewMode = "settings";
console.log("[CO Main Menu] Entering settings mode");
@@ -376,7 +513,9 @@
if (mainPromptBar) {
mainPromptBar.hidden = false;
}
setSelected(SETTINGS_INDEX);
if (viewMode === "menu") {
setSelected(SETTINGS_INDEX);
}
});
}
@@ -390,6 +529,22 @@
}
});
browserController = window.CO_Browser.create({
mount: browserPanel,
layout: "embedded",
exposeGlobals: true,
captureKeys: false,
onBack: function () {
exitBrowserMode();
}
});
window.CO_App.setPointerMode = function (mode) {
if (viewMode === "browser") {
browserController.setPointerMode(mode);
}
};
// ── Keyboard navigation ────────────────────────────────────────
function handleMenuKeydown(event) {
@@ -422,7 +577,41 @@
settingsController.handleKeydown(event);
return;
}
if (viewMode === "browser") {
browserController.handleKeydown(event);
return;
}
handleMenuKeydown(event);
});
window.handleControllerInput = function (button, phase) {
if (phase === "down") {
if (viewMode === "browser") {
browserController.handleControllerInput(button, phase);
return;
}
if (viewMode === "settings") {
return;
}
switch (button) {
case "dpad_up":
moveSelection(-1);
break;
case "dpad_down":
moveSelection(1);
break;
case "a":
activateSelected();
break;
case "b":
if (viewMode === "browser") {
exitBrowserMode();
} else if (viewMode === "settings") {
exitSettingsMode();
}
break;
}
}
};
})();
@@ -406,3 +406,32 @@ body {
),
rgba(4, 8, 6, 0.32);
}
/* Browser mode: keep main nav, dim background more */
.main-screen.is-browser-mode .left-panel {
overflow: hidden;
background:
linear-gradient(
90deg,
rgba(8, 14, 10, 0.9) 0%,
rgba(8, 14, 10, 0.78) 65%,
rgba(8, 14, 10, 0.15) 100%
);
}
.main-screen.is-browser-mode .menu-item__bar {
width: calc(100% + 28px * var(--co-ui-scale));
}
.main-screen.is-browser-mode .scene-overlay {
background:
linear-gradient(
90deg,
rgba(6, 10, 8, 0.88) 0%,
rgba(6, 10, 8, 0.55) 24%,
rgba(6, 10, 8, 0.4) 55%,
rgba(6, 10, 8, 0.48) 100%
),
rgba(4, 8, 6, 0.32);
}