Files
OpenFrontIO/index.html
T
Evan 815f1de67b Update control panel UI (#3357)
Relates to #2260

## Description:

Inspired by https://github.com/openfrontio/OpenFrontIO/pull/3359

This PR centers the control panel and combines it with the units
display. The reasoning is that the control panel contains the most
critical info so it should be in the center of the screen. Combining it
with the units display reduces the number of UI components on screen.

Also made the attack ratio bar persistent on mobile

<img width="618" height="216" alt="Screenshot 2026-03-06 at 2 06 34 PM"
src="https://github.com/user-attachments/assets/34b041c1-d78b-46b5-a7ab-f2a44145a7e2"
/>


<img width="941" height="343" alt="Screenshot 2026-03-06 at 2 06 55 PM"
src="https://github.com/user-attachments/assets/1e3b026c-8eb2-407c-be38-0e71e1ae426c"
/>

<img width="562" height="228" alt="Screenshot 2026-03-06 at 4 11 20 PM"
src="https://github.com/user-attachments/assets/56eac49f-c8a4-4ac1-a60a-f1bcb2fad2d0"
/>

<img width="939" height="357" alt="Screenshot 2026-03-06 at 4 11 32 PM"
src="https://github.com/user-attachments/assets/eb5591d5-3cc2-4182-944b-3a4b0b76852a"
/>


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan

Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com>
2026-03-06 18:32:01 -08:00

423 lines
14 KiB
HTML

<!doctype html>
<html lang="en" class="h-full preload" translate="no">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title data-i18n="main.title">OpenFront (ALPHA)</title>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/svg+xml" href="/images/Favicon.svg" />
<!-- Preload styles -->
<style>
.preload {
visibility: hidden;
opacity: 0;
transition: opacity 0.5s ease-out;
}
/* iOS safe area support */
body {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
/* Ensure full viewport height on iOS */
html,
body {
height: 100%;
height: -webkit-fill-available;
min-height: 100%;
min-height: -webkit-fill-available;
}
</style>
<!-- SEO -->
<link rel="canonical" href="https://openfront.io/" />
<meta
name="description"
content="Conquer the world in this multiplayer battle royale! Expand your nation, eliminate opponents, and dominate the map in this fast-paced IO game."
/>
<!-- Open Graph -->
<meta property="og:url" content="https://openfront.io/" />
<meta property="og:title" content="OpenFront - Battle Royale" />
<meta
property="og:description"
content="Conquer the world in this multiplayer battle royale! Expand your nation, eliminate opponents, and dominate the map in this fast-paced IO game."
/>
<meta
property="og:image"
content="https://openfront.io/images/GameplayScreenshot.png"
/>
<meta property="og:type" content="game" />
<!-- Injected from Server env -->
<script>
window.GIT_COMMIT = <%- gitCommit %>;
window.INSTANCE_ID = <%- instanceId %>;
</script>
<!-- CrazyGames SDK -->
<script
src="https://sdk.crazygames.com/crazygames-sdk-v3.js"
async
></script>
<!-- Cloudflare Turnstile -->
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
<script data-cfasync="false">
window.ramp = window.ramp || {};
window.ramp.que = window.ramp.que || [];
window.ramp.passiveMode = true;
</script>
<script>
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function () {
googletag.pubads().set("page_url", "http://openfront.io ");
});
</script>
<!-- Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "AW-16702609763");
</script>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-WQGQQ8RDN4"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-WQGQQ8RDN4");
</script>
</head>
<body
class="h-full select-none font-sans min-h-screen bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-row overflow-hidden"
>
<div id="hex-grid" class="fixed inset-0 -z-50 pointer-events-none">
<div
id="background-layer"
class="absolute inset-0 bg-cover bg-center opacity-60 [filter:brightness(0.5)_saturate(1.4)] dark:[filter:sepia(0.2)_saturate(1.2)_hue-rotate(180deg)_brightness(0.4)]"
style="
background-image: url(&quot;/resources/images/background.webp&quot;);
"
></div>
<div
class="absolute inset-0 bg-center bg-no-repeat bg-contain hidden lg:block"
style="
background-image: url(&quot;/resources/images/OpenFront.webp&quot;);
opacity: 0.25;
"
></div>
<div
class="absolute inset-0 bg-center bg-no-repeat bg-contain lg:hidden"
style="
background-image: url(&quot;/resources/images/OF.webp&quot;);
opacity: 0.25;
"
></div>
</div>
<!-- LEFT SIDEBAR MENU -->
<div
id="mobile-menu-backdrop"
class="lg:hidden! in-[.in-game]:hidden hidden pointer-events-none [&.open]:block [&.open]:pointer-events-auto [&.open]:fixed [&.open]:inset-0 [&.open]:bg-black/60 [&.open]:z-[40000] transition-opacity"
role="presentation"
aria-hidden="true"
></div>
<mobile-nav-bar
id="sidebar-menu"
class="peer [.in-game_&]:hidden z-[40001] fixed left-0 top-0 h-full flex flex-col justify-start overflow-visible bg-black/70 backdrop-blur-xl border-r border-white/10 transition-transform duration-500 ease-out transform -translate-x-full w-[70%] [&.open]:translate-x-0 lg:hidden"
role="dialog"
data-i18n-aria-label="main.menu"
></mobile-nav-bar>
<!-- MAIN CONTENT AREA -->
<div
class="in-[.in-game]:hidden flex-1 relative overflow-hidden h-full transition-[margin] duration-500 ease-out will-change-[margin-left] flex flex-col"
>
<!-- Desktop Top Bar -->
<desktop-nav-bar></desktop-nav-bar>
<div
id="turnstile-container"
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-99999"
></div>
<gutter-ads></gutter-ads>
<!-- Main container with responsive padding -->
<main-layout class="contents">
<play-page class="contents"></play-page>
<matchmaking-modal
id="page-matchmaking"
inline
class="hidden w-full h-full page-content relative z-50"
></matchmaking-modal>
<news-modal
id="page-news"
inline
class="hidden w-full h-full page-content relative z-50"
></news-modal>
<single-player-modal
id="page-single-player"
inline
class="hidden w-full h-full page-content relative z-50"
></single-player-modal>
<host-lobby-modal
id="page-host-lobby"
inline
class="hidden w-full h-full page-content relative z-50"
></host-lobby-modal>
<join-lobby-modal
id="page-join-lobby"
inline
class="hidden w-full h-full page-content relative z-50"
></join-lobby-modal>
<territory-patterns-modal
id="page-item-store"
inline
class="hidden w-full h-full page-content relative z-50"
></territory-patterns-modal>
<user-setting
id="page-settings"
inline
class="hidden w-full h-full page-content relative z-50"
></user-setting>
<leaderboard-modal
id="page-leaderboard"
inline
class="hidden w-full h-full page-content relative z-50"
></leaderboard-modal>
<troubleshooting-modal
id="page-troubleshooting"
inline
class="hidden w-full h-full page-content relative z-50"
></troubleshooting-modal>
<account-modal
id="page-account"
inline
class="hidden w-full h-full page-content relative z-50"
></account-modal>
<help-modal
id="page-help"
inline
class="hidden w-full h-full page-content relative z-50"
></help-modal>
<language-modal
id="page-language"
inline
class="hidden w-full h-full page-content relative z-50"
></language-modal>
<flag-input-modal
id="flag-input-modal"
inline
class="hidden w-full h-full page-content relative z-50"
></flag-input-modal>
<ranked-modal
id="page-ranked"
inline
class="hidden w-full h-full page-content relative z-50"
></ranked-modal>
</main-layout>
<!-- Desktop Footer -->
<page-footer></page-footer>
<!-- Global Modals -->
<territory-patterns-modal
id="territory-patterns-modal"
></territory-patterns-modal>
</div>
<!-- Game components -->
<div id="app"></div>
<!-- Bottom HUD: <sm=column, sm..lg=2col (HUD left | events right), lg+=3col grid centered -->
<div
class="fixed bottom-0 left-0 w-full z-[200] flex flex-col pointer-events-none sm:flex-row sm:items-end lg:grid lg:grid-cols-[1fr_460px_1fr] lg:items-end min-[1200px]:bottom-4 min-[1200px]:px-4"
style="
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
"
>
<!-- HUD: <sm full-width, sm..lg left side 460px, lg+ col-2 -->
<div
class="flex flex-col pointer-events-none w-full sm:w-[460px] lg:col-start-2 z-10"
>
<attacks-display class="w-full pointer-events-auto"></attacks-display>
<div
class="pointer-events-auto bg-gray-800/70 backdrop-blur-xs rounded-lg shadow-lg"
>
<control-panel class="w-full"></control-panel>
<unit-display class="hidden lg:block w-full"></unit-display>
</div>
</div>
<!-- events+chat: <sm above HUD (order-first), sm..lg right side, lg+ col-3 -->
<div
class="flex flex-col pointer-events-none items-end order-first sm:order-none sm:flex-1 lg:col-start-3 lg:self-end lg:justify-end"
>
<chat-display
class="w-full sm:w-auto pointer-events-auto"
></chat-display>
<events-display
class="w-full sm:w-auto pointer-events-auto"
></events-display>
</div>
</div>
<!-- Game modals and overlays -->
<emoji-table></emoji-table>
<build-menu></build-menu>
<win-modal></win-modal>
<game-starting-modal></game-starting-modal>
<div
class="flex flex-col items-end fixed top-0 right-0 min-[1200px]:top-4 min-[1200px]:right-4 z-1000 gap-2"
>
<game-right-sidebar></game-right-sidebar>
<replay-panel></replay-panel>
</div>
<settings-modal></settings-modal>
<player-panel></player-panel>
<spawn-timer></spawn-timer>
<immunity-timer></immunity-timer>
<in-game-header-ad></in-game-header-ad>
<spawn-video-ad></spawn-video-ad>
<game-info-modal></game-info-modal>
<alert-frame></alert-frame>
<chat-modal></chat-modal>
<multi-tab-modal></multi-tab-modal>
<game-left-sidebar></game-left-sidebar>
<performance-overlay></performance-overlay>
<player-info-overlay></player-info-overlay>
<leader-board></leader-board>
<team-stats></team-stats>
<heads-up-message></heads-up-message>
<!-- Scripts -->
<script>
// Remove preload class after everything is loaded
window.addEventListener("load", function () {
requestAnimationFrame(() => {
document.documentElement.classList.remove("preload");
});
});
// Fallback: remove preload class after timeout in case DOMContentLoaded never fires
setTimeout(function () {
document.documentElement.classList.remove("preload");
}, 3000);
</script>
<script>
// Fallback sidebar toggle so hamburger works even if module bundle fails to load
window.__toggleSidebar = function (e) {
try {
const sidebar = document.getElementById("sidebar-menu");
const backdrop = document.getElementById("mobile-menu-backdrop");
if (!sidebar || !backdrop) return;
const isOpen = sidebar.classList.contains("open");
if (isOpen) {
sidebar.classList.remove("open");
backdrop.classList.remove("open");
document.documentElement.classList.remove("overflow-hidden");
sidebar.setAttribute("aria-hidden", "true");
sidebar.removeAttribute("aria-modal");
backdrop.setAttribute("aria-hidden", "true");
} else {
sidebar.classList.add("open");
backdrop.classList.add("open");
document.documentElement.classList.add("overflow-hidden");
sidebar.setAttribute("aria-hidden", "false");
sidebar.setAttribute("aria-modal", "true");
backdrop.setAttribute("aria-hidden", "false");
}
const hb = document.getElementById("hamburger-btn");
if (hb) hb.setAttribute("aria-expanded", (!isOpen).toString());
} catch (err) {
console.error("Toggle failed", err);
}
};
// Wire up the hamburger button inline
const hamburger = document.getElementById("hamburger-btn");
if (hamburger) {
hamburger.onclick = window.__toggleSidebar;
hamburger.setAttribute("aria-expanded", "false");
}
// Wire up backdrop click to close menu
const backdrop = document.getElementById("mobile-menu-backdrop");
if (backdrop) {
backdrop.onclick = function (e) {
const sidebar = document.getElementById("sidebar-menu");
if (sidebar && sidebar.classList.contains("open")) {
window.__toggleSidebar(e);
}
};
}
// Wire up Escape key to close menu
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" || e.key === "Esc") {
const sidebar = document.getElementById("sidebar-menu");
if (sidebar && sidebar.classList.contains("open")) {
window.__toggleSidebar(e);
}
}
});
</script>
<!-- Analytics -->
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'
></script>
<script type="module" src="/src/client/Main.ts"></script>
<footer>
<script
data-cfasync="false"
async
src="//cdn.intergient.com/1025558/75940/ramp.js"
></script>
</footer>
</body>
</html>