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
+28
View File
@@ -7,6 +7,34 @@ failed experiments, successful tests, and next steps.
--- ---
## 2026-06-18 - Server browser UI resolution scaling
### Summary
Fixed multiplayer server browser content staying small and drifting at resolutions above 1920×1080. Added a `--co-ui-scale` factor (baseline 1920×1080, scales up only) applied to fonts, spacing, borders, and controller glyph sizes. Panel alignment now uses pure viewport percentages so it tracks the native main menu at any resolution.
### Files Changed
- `ui/views/CommonwealthOnline/browser/styles.css`
- `ui/views/CommonwealthOnline/browser/components/layout.js` (new)
- `ui/views/CommonwealthOnline/browser/components/controller-glyphs.js`
- `ui/views/CommonwealthOnline/browser/app.js`
- `ui/views/CommonwealthOnline/browser/index.html`
### Details
- Previous `clamp()` font caps stopped text growing past ~20px while the panel frame grew with `vw`/`vh`, making 4K and ultrawide layouts hard to read.
- `layout.js` sets `--co-ui-scale` on load, resize, and `onBrowserShown` for Ultralight reliability.
- Removed 1280px/2560px media-query pixel floors that fought aspect-ratio scaling.
### Testing
- Not run in-game. Redeploy UI (`deploy-ui.bat`), open Main Menu → Multiplayer at 1920×1080 (should match prior look) and at your high-res fullscreen (text and prompts should scale up with the panel).
### Known Issues
- None currently known.
### Next Steps
- In-game verify alignment against the ADD-ONS / LOAD submenu panel at 2560×1440 and 3840×2160.
---
## 2026-06-18 - Individual controller icon PNGs from ui/Icons ## 2026-06-18 - Individual controller icon PNGs from ui/Icons
### Summary ### Summary
@@ -604,6 +604,9 @@
}; };
window.onBrowserShown = function () { window.onBrowserShown = function () {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
dispatchCoEvent({ type: "menuOpened" }); dispatchCoEvent({ type: "menuOpened" });
render(); render();
}; };
@@ -628,6 +631,12 @@
state.allServers = (window.CO_MOCK_SERVERS || []).slice(); state.allServers = (window.CO_MOCK_SERVERS || []).slice();
applyLocalFilters(); applyLocalFilters();
bindUi(); bindUi();
window.addEventListener("resize", function () {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
render();
});
render(); render();
console.log("[CO] Server browser loaded"); 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) { function tintCacheKey(url, color) {
return url + "|" + color.r + "," + color.g + "," + color.b; return url + "|" + color.r + "," + color.g + "," + color.b;
} }
@@ -206,6 +212,7 @@
} }
var size = GLYPH_SIZES[glyphId] || GLYPH_SIZES.a; var size = GLYPH_SIZES[glyphId] || GLYPH_SIZES.a;
var scale = getUiScale();
var url = glyphUrl(glyphId); var url = glyphUrl(glyphId);
if (!url) { if (!url) {
return '<span class="co-prompt-key co-prompt-key--fallback">' + key + "</span>"; return '<span class="co-prompt-key co-prompt-key--fallback">' + key + "</span>";
@@ -213,7 +220,7 @@
return ( return (
'<span class="co-prompt-key co-prompt-key--glyph co-prompt-key--' + glyphId + '"' + '<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">' + ' aria-hidden="true">' +
'<img class="co-glyph-img" data-glyph-src="' + url + '" src="' + url + '" alt="" draggable="false" />' + '<img class="co-glyph-img" data-glyph-src="' + url + '" src="' + url + '" alt="" draggable="false" />' +
"</span>" "</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>
</div> </div>
<script src="./components/layout.js"></script>
<script src="./components/theme.js"></script> <script src="./components/theme.js"></script>
<script src="./components/mock-data.js"></script> <script src="./components/mock-data.js"></script>
<script src="./components/tabs.js"></script> <script src="./components/tabs.js"></script>
+80 -93
View File
@@ -34,25 +34,31 @@
--co-danger: #ff6a6a; --co-danger: #ff6a6a;
--co-font: "Roboto Condensed", "Bahnschrift", "Arial Narrow", Arial, sans-serif; --co-font: "Roboto Condensed", "Bahnschrift", "Arial Narrow", Arial, sans-serif;
--co-transition: 80ms linear; --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): * UI scale: 1920×1080 is the design baseline (scale 1). Higher resolutions scale up
* Left — tight gap, no overlap (~454px) * so text and chrome stay readable and aligned with the native Scaleform menu.
* Top — MULTIPLAYER row (~286px) * layout.js also sets --co-ui-scale on resize for Ultralight.
* Bottom — main menu box bottom (~146px inset)
*/ */
--co-panel-left: max(35.6vw, 454px); --co-ui-scale: max(1, min(calc(100vw / 1920), calc(100vh / 1080)));
--co-panel-right: max(2vw, 24px); --co-border-stroke: calc(5px * var(--co-ui-scale));
--co-panel-top: max(39.5vh, 286px); --co-border-strip-height: calc(11px * var(--co-ui-scale));
--co-panel-bottom: max(20.5vh, 146px); --co-border-cap: calc(10px * var(--co-ui-scale));
--co-font-xs: clamp(9px, 0.75vw, 11px); --co-border-edge: calc(4px * var(--co-ui-scale));
--co-font-sm: clamp(10px, 0.85vw, 12px); /*
--co-font-md: clamp(13px, 1.05vw, 16px); * Fallout 4 submenu alignment (1280×720 reference, expressed as viewport %):
--co-font-lg: clamp(16px, 1.3vw, 20px); * Left — tight gap, no overlap (~454px @ 1280)
--co-nav-width: clamp(152px, 14vw, 196px); * 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 * 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. * (deployed from assets/F4MouseCursor.png). CSS cursor only applies in a browser.
@@ -109,6 +115,7 @@ body {
transition: opacity var(--co-transition); transition: opacity var(--co-transition);
padding: var(--co-border-cap) var(--co-border-edge); padding: var(--co-border-cap) var(--co-border-edge);
overflow: visible; overflow: visible;
font-size: calc(16px * var(--co-ui-scale));
} }
.co-panel.co-loading { .co-panel.co-loading {
@@ -156,8 +163,8 @@ body {
flex: 0 0 var(--co-nav-width); flex: 0 0 var(--co-nav-width);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 6px 0; padding: 0.375em 0;
border-right: 1px solid var(--co-green-dim); border-right: 0.0625em solid var(--co-green-dim);
overflow-y: auto; overflow-y: auto;
} }
@@ -172,7 +179,7 @@ body {
letter-spacing: 0.12em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
text-align: left; text-align: left;
padding: 10px 16px; padding: 0.625em 1em;
cursor: var(--co-cursor); cursor: var(--co-cursor);
transition: background var(--co-transition), color var(--co-transition); transition: background var(--co-transition), color var(--co-transition);
} }
@@ -230,8 +237,8 @@ body {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 1em;
padding: 10px 16px 8px; padding: 0.625em 1em 0.5em;
} }
.co-subpanel-title { .co-subpanel-title {
@@ -244,7 +251,7 @@ body {
.co-subpanel-meta { .co-subpanel-meta {
display: flex; display: flex;
gap: 10px; gap: 0.625em;
font-size: var(--co-font-xs); font-size: var(--co-font-xs);
font-weight: 700; font-weight: 700;
letter-spacing: 0.12em; letter-spacing: 0.12em;
@@ -260,8 +267,8 @@ body {
/* Preview — Load-menu thumbnail (favorites / recent only). */ /* Preview — Load-menu thumbnail (favorites / recent only). */
.co-preview-area { .co-preview-area {
flex-shrink: 0; flex-shrink: 0;
margin: 0 12px 8px; margin: 0 0.75em 0.5em;
border: 1px solid var(--co-green-dim); border: 0.0625em solid var(--co-green-dim);
background: var(--co-bg-dark); background: var(--co-bg-dark);
overflow: hidden; overflow: hidden;
} }
@@ -273,7 +280,7 @@ body {
.co-preview { .co-preview {
width: 100%; width: 100%;
height: clamp(72px, 14vh, 120px); height: calc(120px * var(--co-ui-scale));
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -291,8 +298,8 @@ body {
} }
.co-preview-caption { .co-preview-caption {
padding: 6px 10px; padding: 0.375em 0.625em;
border-top: 1px solid var(--co-green-dim); border-top: 0.0625em solid var(--co-green-dim);
font-size: var(--co-font-xs); font-size: var(--co-font-xs);
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
@@ -312,16 +319,16 @@ body {
/* Filters */ /* Filters */
.co-filters { .co-filters {
flex-shrink: 0; flex-shrink: 0;
padding: 0 12px 10px; padding: 0 0.75em 0.625em;
display: flex; display: flex;
flex-direction: column; 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-input,
.co-filters.focused .co-filter-row.focused .co-filter-select, .co-filters.focused .co-filter-row.focused .co-filter-select,
.co-filters.focused .co-filter-row.focused .co-checkbox { .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; outline-offset: 0;
} }
@@ -329,12 +336,12 @@ body {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-end; align-items: flex-end;
gap: 6px 10px; gap: 0.375em 0.625em;
} }
.co-filters-row-selects .co-filter-row { .co-filters-row-selects .co-filter-row {
flex: 1 1 90px; flex: 1 1 5.625em;
min-width: 80px; min-width: 5em;
} }
.co-filters-row-options { .co-filters-row-options {
@@ -344,7 +351,7 @@ body {
.co-filter-row { .co-filter-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 0.1875em;
min-width: 0; min-width: 0;
} }
@@ -355,7 +362,7 @@ body {
.co-filter-row.co-filter-checks { .co-filter-row.co-filter-checks {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 5px; gap: 0.3125em;
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -371,12 +378,12 @@ body {
.co-filter-select { .co-filter-select {
width: 100%; width: 100%;
background: var(--co-bg-dark); background: var(--co-bg-dark);
border: 1px solid var(--co-green-dim); border: 0.0625em solid var(--co-green-dim);
color: var(--co-green); color: var(--co-green);
font: inherit; font: inherit;
font-size: var(--co-font-sm); font-size: var(--co-font-sm);
font-weight: 600; font-weight: 600;
padding: 5px 8px; padding: 0.3125em 0.5em;
border-radius: 0; border-radius: 0;
cursor: var(--co-cursor); cursor: var(--co-cursor);
transition: border-color var(--co-transition); transition: border-color var(--co-transition);
@@ -402,7 +409,7 @@ body {
.co-checkbox-row { .co-checkbox-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 0.3125em;
font-size: var(--co-font-xs); font-size: var(--co-font-xs);
font-weight: 700; font-weight: 700;
letter-spacing: 0.06em; letter-spacing: 0.06em;
@@ -417,9 +424,9 @@ body {
} }
.co-checkbox { .co-checkbox {
width: 13px; width: calc(13px * var(--co-ui-scale));
height: 13px; height: calc(13px * var(--co-ui-scale));
border: 1px solid var(--co-border); border: 0.0625em solid var(--co-border);
background: transparent; background: transparent;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -429,7 +436,7 @@ body {
.co-checkbox.checked::after { .co-checkbox.checked::after {
content: "X"; content: "X";
font-size: 9px; font-size: calc(9px * var(--co-ui-scale));
font-weight: 700; font-weight: 700;
color: var(--co-green); color: var(--co-green);
line-height: 1; line-height: 1;
@@ -437,7 +444,7 @@ body {
.co-btn { .co-btn {
appearance: none; appearance: none;
border: 1px solid var(--co-green-dim); border: 0.0625em solid var(--co-green-dim);
background: transparent; background: transparent;
color: var(--co-green); color: var(--co-green);
font: inherit; font: inherit;
@@ -445,7 +452,7 @@ body {
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
padding: 5px 10px; padding: 0.3125em 0.625em;
cursor: var(--co-cursor); cursor: var(--co-cursor);
margin-left: auto; margin-left: auto;
flex-shrink: 0; flex-shrink: 0;
@@ -460,7 +467,7 @@ body {
.co-btn.focused { .co-btn.focused {
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.18); 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 */ /* Server list — save-file style rows */
@@ -481,11 +488,11 @@ body {
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--co-green-dim) transparent; scrollbar-color: var(--co-green-dim) transparent;
padding: 0 4px; padding: 0 0.25em;
} }
.co-server-list::-webkit-scrollbar { .co-server-list::-webkit-scrollbar {
width: 4px; width: calc(4px * var(--co-ui-scale));
} }
.co-server-list::-webkit-scrollbar-thumb { .co-server-list::-webkit-scrollbar-thumb {
@@ -496,8 +503,8 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 1em;
padding: 8px 12px; padding: 0.5em 0.75em;
font-size: var(--co-font-md); font-size: var(--co-font-md);
line-height: 1.2; line-height: 1.2;
cursor: var(--co-cursor); cursor: var(--co-cursor);
@@ -517,7 +524,7 @@ body {
.co-server-row-left { .co-server-row-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 0.625em;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
} }
@@ -535,7 +542,7 @@ body {
.co-server-row-right { .co-server-row-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 1.25em;
flex-shrink: 0; flex-shrink: 0;
font-size: var(--co-font-md); font-size: var(--co-font-md);
font-weight: 700; font-weight: 700;
@@ -550,7 +557,7 @@ body {
.co-fav { .co-fav {
color: var(--co-text-dim); color: var(--co-text-dim);
font-size: 15px; font-size: calc(15px * var(--co-ui-scale));
flex-shrink: 0; flex-shrink: 0;
} }
@@ -575,7 +582,7 @@ body {
.co-scroll-indicator { .co-scroll-indicator {
flex-shrink: 0; flex-shrink: 0;
text-align: center; text-align: center;
padding: 2px 0 4px; padding: 0.125em 0 0.25em;
font-size: var(--co-font-xs); font-size: var(--co-font-xs);
letter-spacing: 0.15em; letter-spacing: 0.15em;
color: var(--co-text-dim); color: var(--co-text-dim);
@@ -591,13 +598,13 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 6px 14px 8px; padding: 0.375em 0.875em 0.5em;
border-top: 1px solid var(--co-green-dim); border-top: 0.0625em solid var(--co-green-dim);
} }
.co-prompts { .co-prompts {
display: flex; display: flex;
gap: 16px; gap: 1em;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -605,7 +612,7 @@ body {
.co-prompt { .co-prompt {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 0.375em;
font-size: var(--co-font-xs); font-size: var(--co-font-xs);
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
@@ -614,16 +621,16 @@ body {
} }
.co-prompt-key { .co-prompt-key {
min-width: 18px; min-width: calc(18px * var(--co-ui-scale));
height: 18px; height: calc(18px * var(--co-ui-scale));
border: 1px solid var(--co-border); border: 0.0625em solid var(--co-border);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 8px; font-size: calc(8px * var(--co-ui-scale));
font-weight: 700; font-weight: 700;
padding: 0 3px; padding: 0 calc(3px * var(--co-ui-scale));
} }
.co-prompt-key--glyph { .co-prompt-key--glyph {
@@ -644,16 +651,16 @@ body {
} }
.co-direct-connect { .co-direct-connect {
padding: 16px 20px; padding: 1em 1.25em;
max-width: 480px; max-width: calc(480px * var(--co-ui-scale));
} }
.co-direct-row { .co-direct-row {
margin-bottom: 12px; margin-bottom: 0.75em;
} }
.co-direct-row.focused .co-filter-input { .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); background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
} }
@@ -664,11 +671,11 @@ body {
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--co-text-dim); color: var(--co-text-dim);
margin-bottom: 4px; margin-bottom: 0.25em;
} }
.co-empty { .co-empty {
padding: 24px 16px; padding: 1.5em 1em;
text-align: center; text-align: center;
color: var(--co-text-dim); color: var(--co-text-dim);
font-size: var(--co-font-md); font-size: var(--co-font-md);
@@ -696,7 +703,7 @@ body {
font-size: var(--co-font-md); font-size: var(--co-font-md);
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
margin-top: 14px; margin-top: 0.875em;
} }
.co-join-overlay.co-error p { .co-join-overlay.co-error p {
@@ -704,9 +711,9 @@ body {
} }
.co-spinner { .co-spinner {
width: 22px; width: calc(22px * var(--co-ui-scale));
height: 22px; height: calc(22px * var(--co-ui-scale));
border: 2px solid var(--co-green-dim); border: calc(2px * var(--co-ui-scale)) solid var(--co-green-dim);
border-top-color: var(--co-green); border-top-color: var(--co-green);
border-radius: 50%; border-radius: 50%;
animation: co-spin 0.8s linear infinite; animation: co-spin 0.8s linear infinite;
@@ -715,23 +722,3 @@ body {
@keyframes co-spin { @keyframes co-spin {
to { transform: rotate(360deg); } 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);
}
}