Revamp CommonwealthOnline browser UI and styles

Rework the CommonwealthOnline browser UI: restructure HTML into a nav + subpanel layout, add SVG/PNG/font assets and new sprites, and overhaul styles with a new font-face and refreshed color/spacing variables. Refactor components: tabs use a left nav list; filters extraction (renderRow) and reset focus handling; direct-connect renders into a dedicated container; server-list switched to compact row layout and adds a scroll indicator; server-details now uses a preview area and simplified preview/stats rendering. Update app logic: adjust focus zone handling (nextFocusZone), limit zones per tab, update connection status/count display, and avoid rendering details for direct-connect. Overall changes improve responsiveness, visual hierarchy, and focus/navigation behavior.
This commit is contained in:
2026-06-08 14:40:00 +12:00
parent fe620020be
commit cfd1fab6b2
13 changed files with 555 additions and 506 deletions
@@ -6,12 +6,13 @@
var pane = root.querySelector('[data-pane="direct-connect"]');
if (!pane) return;
var container = pane.querySelector(".co-direct-connect");
if (!container) return;
var address = state.directConnect.address || "127.0.0.1";
var port = state.directConnect.port || "7777";
pane.innerHTML =
'<div class="co-direct-connect">' +
"<h2>Direct Connect</h2>" +
container.innerHTML =
'<div class="co-direct-row">' +
'<label for="co-dc-address">Server Address</label>' +
'<input class="co-filter-input" id="co-dc-address" type="text" value="' + escapeAttr(address) + '">' +
@@ -21,8 +22,7 @@
'<input class="co-filter-input" id="co-dc-port" type="text" value="' + escapeAttr(port) + '">' +
"</div>" +
'<button type="button" class="co-btn co-dc-connect">Connect</button>' +
'<p class="co-detail-desc" style="margin-top:16px">Enter a server address to connect directly without browsing the list.</p>' +
"</div>";
'<p class="co-detail-desc" style="margin-top:16px;color:var(--co-text-dim);font-size:var(--co-font-sm)">Enter a server address to connect directly without browsing the list.</p>';
},
bind: function (root, onConnect) {
@@ -12,6 +12,37 @@
{ key: "modded", type: "checkbox", label: "Modded" }
];
function renderRow(row, index, state) {
var rowFocused = state.focusZone === "filters" && state.selectedFilterIndex === index ? " focused" : "";
if (row.type === "text") {
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<input class="co-filter-input" type="text" data-filter="' + row.key + '" placeholder="' + row.placeholder + '" value="' + escapeAttr(state.filters[row.key] || "") + '">' +
"</div>"
);
}
if (row.type === "select") {
var opts = row.options.map(function (opt) {
var sel = (state.filters[row.key] || row.options[0]) === opt ? " selected" : "";
return '<option value="' + escapeAttr(opt) + '"' + sel + ">" + opt + "</option>";
}).join("");
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<select class="co-filter-select" data-filter="' + row.key + '">' + opts + "</select>" +
"</div>"
);
}
var checked = state.filters[row.key] ? " checked" : "";
return (
'<label class="co-checkbox-row co-filter-row co-filter-checks' + rowFocused + '" data-filter-index="' + index + '">' +
'<span class="co-checkbox' + checked + '" data-filter="' + row.key + '"></span>' +
row.label +
"</label>"
);
}
window.CO_Filters = {
FILTER_ROWS: FILTER_ROWS,
@@ -20,40 +51,23 @@
if (!el) return;
var focusedClass = state.focusZone === "filters" ? " focused" : "";
var html = FILTER_ROWS.map(function (row, index) {
var rowFocused = state.focusZone === "filters" && state.selectedFilterIndex === index ? " focused" : "";
if (row.type === "text") {
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<input class="co-filter-input" type="text" data-filter="' + row.key + '" placeholder="' + row.placeholder + '" value="' + escapeAttr(state.filters[row.key] || "") + '">' +
"</div>"
);
}
if (row.type === "select") {
var opts = row.options.map(function (opt) {
var sel = (state.filters[row.key] || row.options[0]) === opt ? " selected" : "";
return '<option value="' + escapeAttr(opt) + '"' + sel + ">" + opt + "</option>";
}).join("");
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<select class="co-filter-select" data-filter="' + row.key + '">' + opts + "</select>" +
"</div>"
);
}
var checked = state.filters[row.key] ? " checked" : "";
return (
'<label class="co-checkbox-row co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<span class="co-checkbox' + checked + '" data-filter="' + row.key + '"></span>' +
row.label +
"</label>"
);
var searchHtml = renderRow(FILTER_ROWS[0], 0, state);
var selectHtml = FILTER_ROWS.slice(1, 5).map(function (row, i) {
return renderRow(row, i + 1, state);
}).join("");
var checkboxHtml = FILTER_ROWS.slice(5).map(function (row, i) {
return renderRow(row, i + 5, state);
}).join("");
var resetFocused = state.focusZone === "filters" && state.selectedFilterIndex === FILTER_ROWS.length ? " focused" : "";
html += '<button type="button" class="co-btn co-reset-btn">Reset Filters</button>';
el.className = "co-filters" + focusedClass;
el.innerHTML = html;
el.innerHTML =
'<div class="co-filters-row">' + searchHtml + "</div>" +
'<div class="co-filters-row co-filters-row-selects">' + selectHtml + "</div>" +
'<div class="co-filters-row co-filters-row-options">' +
checkboxHtml +
'<button type="button" class="co-btn co-reset-btn' + resetFocused + '">Reset Filters</button>' +
"</div>";
},
bind: function (root, onFilterChange, onReset) {
@@ -3,50 +3,35 @@
window.CO_ServerDetails = {
render: function (root, state) {
var el = root.querySelector(".co-details");
var activePane = root.querySelector('.co-tab-pane[data-pane="' + state.activeTab + '"]');
var el = activePane && activePane.querySelector(".co-preview-area");
if (!el) return;
el.className = "co-details" + (state.focusZone === "details" ? " focused" : "");
var server = state.servers[state.selectedServerIndex];
if (!server) {
el.innerHTML = '<div class="co-details-title">Server Details</div><div class="co-empty">Select a server</div>';
if (!server || !server.previewImage) {
el.innerHTML = "";
el.classList.add("co-preview-hidden");
return;
}
var rulesHtml = "";
if (server.rules && server.rules.length) {
rulesHtml = "<ul class=\"co-detail-rules\">" + server.rules.map(function (r) {
return "<li>" + escapeHtml(r) + "</li>";
}).join("") + "</ul>";
}
el.classList.remove("co-preview-hidden");
var previewHtml = server.previewImage
? '<img src="' + escapeAttr(server.previewImage) + '" alt="Preview">'
: "Preview Unavailable";
var stats = [
server.worldspace,
server.mode,
server.players + "/" + server.maxPlayers,
server.ping + " ms"
].filter(Boolean).join(" \u00b7 ");
el.innerHTML =
'<div class="co-details-title">Server Details</div>' +
'<div class="co-preview">' + previewHtml + "</div>" +
'<div class="co-detail-name">' + escapeHtml(server.name) + "</div>" +
detailRow("Region", server.region) +
detailRow("Players", server.players + "/" + server.maxPlayers) +
detailRow("Ping", server.ping + " ms") +
detailRow("Worldspace", server.worldspace) +
detailRow("Mode", server.mode) +
detailRow("Status", server.status) +
detailRow("Modded", server.modded ? "Yes" : "No") +
detailRow("Password", server.passworded ? "Required" : "None") +
'<div class="co-detail-desc">' + escapeHtml(server.description || "") + "</div>" +
rulesHtml +
'<div class="co-detail-desc" style="margin-top:8px;color:var(--co-text-dim)">Mod list: coming soon</div>';
'<div class="co-preview"><img src="' + escapeAttr(server.previewImage) + '" alt="Preview"></div>' +
'<div class="co-preview-caption">' +
'<div class="co-preview-name">' + escapeHtml(server.name) + "</div>" +
'<div class="co-preview-stats">' + escapeHtml(stats) + "</div>" +
"</div>";
}
};
function detailRow(label, value) {
return '<div class="co-detail-row"><span>' + label + '</span><span>' + escapeHtml(String(value)) + "</span></div>";
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
@@ -17,6 +17,7 @@
if (!state.servers.length) {
list.innerHTML = '<div class="co-empty">No servers found</div>';
CO_ServerList.updateScrollIndicator(wrap, list);
return;
}
@@ -26,16 +27,26 @@
var pingCls = CO_ServerList.pingClass(server.ping);
return (
'<div class="co-server-row' + selected + '" data-index="' + index + '" data-id="' + escapeAttr(server.id) + '">' +
'<div class="co-server-row-left">' +
'<span class="co-fav' + fav + '">' + (server.favorite ? "\u2605" : "\u2606") + "</span>" +
"<span>" + escapeHtml(server.name) + "</span>" +
'<span class="co-server-row-name">' + escapeHtml(server.name) + "</span>" +
"</div>" +
'<div class="co-server-row-right">' +
"<span>" + server.players + "/" + server.maxPlayers + "</span>" +
'<span class="co-ping ' + pingCls + '">' + server.ping + " ms</span>" +
"<span>" + escapeHtml(server.worldspace) + "</span>" +
"<span>" + escapeHtml(server.mode) + "</span>" +
"<span>" + escapeHtml(server.status) + "</span>" +
"</div>" +
"</div>"
);
}).join("");
CO_ServerList.updateScrollIndicator(wrap, list);
},
updateScrollIndicator: function (wrap, list) {
var indicator = wrap && wrap.querySelector(".co-scroll-indicator");
if (!indicator || !list) return;
var hasOverflow = list.scrollHeight > list.clientHeight + 2;
indicator.classList.toggle("hidden", !hasOverflow);
},
bind: function (root, onSelect) {
@@ -64,6 +75,10 @@
if (row) {
row.scrollIntoView({ block: "nearest" });
}
var wrap = root.querySelector(".co-server-list-wrap");
if (wrap && list) {
CO_ServerList.updateScrollIndicator(wrap, list);
}
}
};
@@ -7,33 +7,35 @@
TAB_ORDER: TAB_ORDER,
render: function (root, state) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
var navEl = root.querySelector(".co-nav-list");
if (!navEl) return;
var labels = {
"direct-connect": "Direct Connect",
"server-browser": "Server Browser",
"server-browser": "Browse",
favorites: "Favorites",
recent: "Recent"
};
tabsEl.innerHTML = TAB_ORDER.map(function (tabId, index) {
navEl.innerHTML = TAB_ORDER.map(function (tabId, index) {
var active = state.activeTab === tabId ? " active" : "";
var focused = state.focusZone === "tabs" && state.selectedTabIndex === index ? " focused" : "";
return (
'<button type="button" class="co-tab' + active + focused + '" data-tab="' + tabId + '" data-index="' + index + '">' +
'<button type="button" class="co-nav-item' + active + focused + '" data-tab="' + tabId + '" data-index="' + index + '">' +
labels[tabId] +
"</button>"
);
}).join("");
navEl.className = "co-nav-list" + (state.focusZone === "tabs" ? " focused" : "");
},
bind: function (root, onTabClick) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
var navEl = root.querySelector(".co-nav-list");
if (!navEl) return;
tabsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".co-tab");
navEl.addEventListener("click", function (e) {
var btn = e.target.closest(".co-nav-item");
if (!btn) return;
onTabClick(btn.getAttribute("data-tab"));
});