Add responsive UI scaling for server browser

Introduce resolution-aware UI scaling to keep fonts, glyphs, and panel chrome readable and aligned above 1920×1080. Adds a new components/layout.js that sets --co-ui-scale (baseline 1920×1080) on load/resize and exposes CO_Layout.update; index.html now loads it and app.js triggers updates onBrowserShown and on window resize. styles.css converts many fixed px values to calc()/em driven by --co-ui-scale, removes the old pixel-floor media queries and clamp caps, and scales borders, spacing, and font sizes. controller-glyphs.js multiplies glyph dimensions by the UI scale so controller icons grow correctly. Changes address small/drifting server browser content on high-res and ultrawide displays.
This commit is contained in:
2026-06-18 15:51:42 +12:00
parent 102acd3340
commit 7efe407421
6 changed files with 146 additions and 94 deletions
@@ -604,6 +604,9 @@
};
window.onBrowserShown = function () {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
dispatchCoEvent({ type: "menuOpened" });
render();
};
@@ -628,6 +631,12 @@
state.allServers = (window.CO_MOCK_SERVERS || []).slice();
applyLocalFilters();
bindUi();
window.addEventListener("resize", function () {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
render();
});
render();
console.log("[CO] Server browser loaded");
})();
@@ -78,6 +78,12 @@
};
}
function getUiScale() {
var style = getComputedStyle(document.documentElement);
var scale = parseFloat(style.getPropertyValue("--co-ui-scale"));
return isFinite(scale) && scale > 0 ? scale : 1;
}
function tintCacheKey(url, color) {
return url + "|" + color.r + "," + color.g + "," + color.b;
}
@@ -206,6 +212,7 @@
}
var size = GLYPH_SIZES[glyphId] || GLYPH_SIZES.a;
var scale = getUiScale();
var url = glyphUrl(glyphId);
if (!url) {
return '<span class="co-prompt-key co-prompt-key--fallback">' + key + "</span>";
@@ -213,7 +220,7 @@
return (
'<span class="co-prompt-key co-prompt-key--glyph co-prompt-key--' + glyphId + '"' +
' style="width:' + size.w + "px;height:" + size.h + 'px"' +
' style="width:' + (size.w * scale) + "px;height:" + (size.h * scale) + 'px"' +
' aria-hidden="true">' +
'<img class="co-glyph-img" data-glyph-src="' + url + '" src="' + url + '" alt="" draggable="false" />' +
"</span>"
@@ -0,0 +1,20 @@
(function () {
"use strict";
var REF_W = 1920;
var REF_H = 1080;
function updateUiScale() {
var w = window.innerWidth || REF_W;
var h = window.innerHeight || REF_H;
var scale = Math.max(1, Math.min(w / REF_W, h / REF_H));
document.documentElement.style.setProperty("--co-ui-scale", String(scale));
}
window.CO_Layout = {
update: updateUiScale
};
window.addEventListener("resize", updateUiScale);
updateUiScale();
})();
@@ -79,6 +79,7 @@
</div>
</div>
<script src="./components/layout.js"></script>
<script src="./components/theme.js"></script>
<script src="./components/mock-data.js"></script>
<script src="./components/tabs.js"></script>
+80 -93
View File
@@ -34,25 +34,31 @@
--co-danger: #ff6a6a;
--co-font: "Roboto Condensed", "Bahnschrift", "Arial Narrow", Arial, sans-serif;
--co-transition: 80ms linear;
--co-border-stroke: 5px;
--co-border-strip-height: 11px;
--co-border-cap: 10px;
--co-border-edge: 4px;
/*
* Fallout 4 submenu alignment (1280×720, like ADD-ONS panel):
* Left — tight gap, no overlap (~454px)
* Top — MULTIPLAYER row (~286px)
* Bottom — main menu box bottom (~146px inset)
* UI scale: 1920×1080 is the design baseline (scale 1). Higher resolutions scale up
* so text and chrome stay readable and aligned with the native Scaleform menu.
* layout.js also sets --co-ui-scale on resize for Ultralight.
*/
--co-panel-left: max(35.6vw, 454px);
--co-panel-right: max(2vw, 24px);
--co-panel-top: max(39.5vh, 286px);
--co-panel-bottom: max(20.5vh, 146px);
--co-font-xs: clamp(9px, 0.75vw, 11px);
--co-font-sm: clamp(10px, 0.85vw, 12px);
--co-font-md: clamp(13px, 1.05vw, 16px);
--co-font-lg: clamp(16px, 1.3vw, 20px);
--co-nav-width: clamp(152px, 14vw, 196px);
--co-ui-scale: max(1, min(calc(100vw / 1920), calc(100vh / 1080)));
--co-border-stroke: calc(5px * var(--co-ui-scale));
--co-border-strip-height: calc(11px * var(--co-ui-scale));
--co-border-cap: calc(10px * var(--co-ui-scale));
--co-border-edge: calc(4px * var(--co-ui-scale));
/*
* Fallout 4 submenu alignment (1280×720 reference, expressed as viewport %):
* Left — tight gap, no overlap (~454px @ 1280)
* Top — MULTIPLAYER row (~286px @ 720)
* Bottom — main menu box bottom (~146px inset @ 720)
*/
--co-panel-left: 35.6vw;
--co-panel-right: 2vw;
--co-panel-top: 39.5vh;
--co-panel-bottom: 20.5vh;
--co-font-xs: calc(11px * var(--co-ui-scale));
--co-font-sm: calc(12px * var(--co-ui-scale));
--co-font-md: calc(16px * var(--co-ui-scale));
--co-font-lg: calc(20px * var(--co-ui-scale));
--co-nav-width: calc(196px * var(--co-ui-scale));
/*
* In-game cursor is rendered by PrismaUI_F4 from Data/PrismaUI_F4/misc/cursor.png
* (deployed from assets/F4MouseCursor.png). CSS cursor only applies in a browser.
@@ -109,6 +115,7 @@ body {
transition: opacity var(--co-transition);
padding: var(--co-border-cap) var(--co-border-edge);
overflow: visible;
font-size: calc(16px * var(--co-ui-scale));
}
.co-panel.co-loading {
@@ -156,8 +163,8 @@ body {
flex: 0 0 var(--co-nav-width);
display: flex;
flex-direction: column;
padding: 6px 0;
border-right: 1px solid var(--co-green-dim);
padding: 0.375em 0;
border-right: 0.0625em solid var(--co-green-dim);
overflow-y: auto;
}
@@ -172,7 +179,7 @@ body {
letter-spacing: 0.12em;
text-transform: uppercase;
text-align: left;
padding: 10px 16px;
padding: 0.625em 1em;
cursor: var(--co-cursor);
transition: background var(--co-transition), color var(--co-transition);
}
@@ -230,8 +237,8 @@ body {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 10px 16px 8px;
gap: 1em;
padding: 0.625em 1em 0.5em;
}
.co-subpanel-title {
@@ -244,7 +251,7 @@ body {
.co-subpanel-meta {
display: flex;
gap: 10px;
gap: 0.625em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.12em;
@@ -260,8 +267,8 @@ body {
/* Preview — Load-menu thumbnail (favorites / recent only). */
.co-preview-area {
flex-shrink: 0;
margin: 0 12px 8px;
border: 1px solid var(--co-green-dim);
margin: 0 0.75em 0.5em;
border: 0.0625em solid var(--co-green-dim);
background: var(--co-bg-dark);
overflow: hidden;
}
@@ -273,7 +280,7 @@ body {
.co-preview {
width: 100%;
height: clamp(72px, 14vh, 120px);
height: calc(120px * var(--co-ui-scale));
display: flex;
align-items: center;
justify-content: center;
@@ -291,8 +298,8 @@ body {
}
.co-preview-caption {
padding: 6px 10px;
border-top: 1px solid var(--co-green-dim);
padding: 0.375em 0.625em;
border-top: 0.0625em solid var(--co-green-dim);
font-size: var(--co-font-xs);
letter-spacing: 0.08em;
text-transform: uppercase;
@@ -312,16 +319,16 @@ body {
/* Filters */
.co-filters {
flex-shrink: 0;
padding: 0 12px 10px;
padding: 0 0.75em 0.625em;
display: flex;
flex-direction: column;
gap: 6px;
gap: 0.375em;
}
.co-filters.focused .co-filter-row.focused .co-filter-input,
.co-filters.focused .co-filter-row.focused .co-filter-select,
.co-filters.focused .co-filter-row.focused .co-checkbox {
outline: 1px solid var(--co-green);
outline: 0.0625em solid var(--co-green);
outline-offset: 0;
}
@@ -329,12 +336,12 @@ body {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 6px 10px;
gap: 0.375em 0.625em;
}
.co-filters-row-selects .co-filter-row {
flex: 1 1 90px;
min-width: 80px;
flex: 1 1 5.625em;
min-width: 5em;
}
.co-filters-row-options {
@@ -344,7 +351,7 @@ body {
.co-filter-row {
display: flex;
flex-direction: column;
gap: 3px;
gap: 0.1875em;
min-width: 0;
}
@@ -355,7 +362,7 @@ body {
.co-filter-row.co-filter-checks {
flex-direction: row;
align-items: center;
gap: 5px;
gap: 0.3125em;
flex: 0 0 auto;
}
@@ -371,12 +378,12 @@ body {
.co-filter-select {
width: 100%;
background: var(--co-bg-dark);
border: 1px solid var(--co-green-dim);
border: 0.0625em solid var(--co-green-dim);
color: var(--co-green);
font: inherit;
font-size: var(--co-font-sm);
font-weight: 600;
padding: 5px 8px;
padding: 0.3125em 0.5em;
border-radius: 0;
cursor: var(--co-cursor);
transition: border-color var(--co-transition);
@@ -402,7 +409,7 @@ body {
.co-checkbox-row {
display: flex;
align-items: center;
gap: 5px;
gap: 0.3125em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.06em;
@@ -417,9 +424,9 @@ body {
}
.co-checkbox {
width: 13px;
height: 13px;
border: 1px solid var(--co-border);
width: calc(13px * var(--co-ui-scale));
height: calc(13px * var(--co-ui-scale));
border: 0.0625em solid var(--co-border);
background: transparent;
display: inline-flex;
align-items: center;
@@ -429,7 +436,7 @@ body {
.co-checkbox.checked::after {
content: "X";
font-size: 9px;
font-size: calc(9px * var(--co-ui-scale));
font-weight: 700;
color: var(--co-green);
line-height: 1;
@@ -437,7 +444,7 @@ body {
.co-btn {
appearance: none;
border: 1px solid var(--co-green-dim);
border: 0.0625em solid var(--co-green-dim);
background: transparent;
color: var(--co-green);
font: inherit;
@@ -445,7 +452,7 @@ body {
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 5px 10px;
padding: 0.3125em 0.625em;
cursor: var(--co-cursor);
margin-left: auto;
flex-shrink: 0;
@@ -460,7 +467,7 @@ body {
.co-btn.focused {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.18);
outline: 1px solid var(--co-green);
outline: 0.0625em solid var(--co-green);
}
/* Server list — save-file style rows */
@@ -481,11 +488,11 @@ body {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--co-green-dim) transparent;
padding: 0 4px;
padding: 0 0.25em;
}
.co-server-list::-webkit-scrollbar {
width: 4px;
width: calc(4px * var(--co-ui-scale));
}
.co-server-list::-webkit-scrollbar-thumb {
@@ -496,8 +503,8 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 12px;
gap: 1em;
padding: 0.5em 0.75em;
font-size: var(--co-font-md);
line-height: 1.2;
cursor: var(--co-cursor);
@@ -517,7 +524,7 @@ body {
.co-server-row-left {
display: flex;
align-items: center;
gap: 10px;
gap: 0.625em;
min-width: 0;
flex: 1;
}
@@ -535,7 +542,7 @@ body {
.co-server-row-right {
display: flex;
align-items: center;
gap: 20px;
gap: 1.25em;
flex-shrink: 0;
font-size: var(--co-font-md);
font-weight: 700;
@@ -550,7 +557,7 @@ body {
.co-fav {
color: var(--co-text-dim);
font-size: 15px;
font-size: calc(15px * var(--co-ui-scale));
flex-shrink: 0;
}
@@ -575,7 +582,7 @@ body {
.co-scroll-indicator {
flex-shrink: 0;
text-align: center;
padding: 2px 0 4px;
padding: 0.125em 0 0.25em;
font-size: var(--co-font-xs);
letter-spacing: 0.15em;
color: var(--co-text-dim);
@@ -591,13 +598,13 @@ body {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 6px 14px 8px;
border-top: 1px solid var(--co-green-dim);
padding: 0.375em 0.875em 0.5em;
border-top: 0.0625em solid var(--co-green-dim);
}
.co-prompts {
display: flex;
gap: 16px;
gap: 1em;
flex-wrap: wrap;
justify-content: flex-end;
}
@@ -605,7 +612,7 @@ body {
.co-prompt {
display: flex;
align-items: center;
gap: 6px;
gap: 0.375em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.08em;
@@ -614,16 +621,16 @@ body {
}
.co-prompt-key {
min-width: 18px;
height: 18px;
border: 1px solid var(--co-border);
min-width: calc(18px * var(--co-ui-scale));
height: calc(18px * var(--co-ui-scale));
border: 0.0625em solid var(--co-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-size: calc(8px * var(--co-ui-scale));
font-weight: 700;
padding: 0 3px;
padding: 0 calc(3px * var(--co-ui-scale));
}
.co-prompt-key--glyph {
@@ -644,16 +651,16 @@ body {
}
.co-direct-connect {
padding: 16px 20px;
max-width: 480px;
padding: 1em 1.25em;
max-width: calc(480px * var(--co-ui-scale));
}
.co-direct-row {
margin-bottom: 12px;
margin-bottom: 0.75em;
}
.co-direct-row.focused .co-filter-input {
outline: 1px solid var(--co-green);
outline: 0.0625em solid var(--co-green);
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
}
@@ -664,11 +671,11 @@ body {
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--co-text-dim);
margin-bottom: 4px;
margin-bottom: 0.25em;
}
.co-empty {
padding: 24px 16px;
padding: 1.5em 1em;
text-align: center;
color: var(--co-text-dim);
font-size: var(--co-font-md);
@@ -696,7 +703,7 @@ body {
font-size: var(--co-font-md);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-top: 14px;
margin-top: 0.875em;
}
.co-join-overlay.co-error p {
@@ -704,9 +711,9 @@ body {
}
.co-spinner {
width: 22px;
height: 22px;
border: 2px solid var(--co-green-dim);
width: calc(22px * var(--co-ui-scale));
height: calc(22px * var(--co-ui-scale));
border: calc(2px * var(--co-ui-scale)) solid var(--co-green-dim);
border-top-color: var(--co-green);
border-radius: 50%;
animation: co-spin 0.8s linear infinite;
@@ -715,23 +722,3 @@ body {
@keyframes co-spin {
to { transform: rotate(360deg); }
}
@media (max-width: 1280px) {
:root {
--co-panel-left: max(35.8vw, 450px);
--co-panel-right: max(2vw, 16px);
--co-panel-top: max(40vh, 280px);
--co-panel-bottom: max(21vh, 142px);
--co-nav-width: clamp(136px, 15vw, 168px);
}
}
@media (min-width: 2560px) {
:root {
--co-panel-left: max(35.6vw, 908px);
--co-panel-right: max(2.5vw, 48px);
--co-panel-top: max(39.5vh, 572px);
--co-panel-bottom: max(20.5vh, 292px);
}
}