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:
@@ -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, "&")
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user