mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
homepage UI improvements (#3352)
## Description: A bunch of small UI improvements: * Make the content width a bit smaller so gutter ads fit * remove the "duos" "trios" "quads" description on the game card since it's redundant * update UI in game card * minor footer layout changes * update z-index to ensure content appears above ads * removed hasUnusualThumbnailSize, instead just check the map ratio * Use "object cover" for non-irregular maps to the entire game card is filed * remove white ouline from the version * changed solo button to sky blue * make timer "s" lowercase I think we may need to change the openfront logo color a bit too to match the color palette, but we can do that in a follow up. <img width="1591" height="969" alt="Screenshot 2026-03-05 at 2 04 48 PM" src="https://github.com/user-attachments/assets/7bb9ea4c-5a17-47e1-bdad-9d6437b363b3" /> ## 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
This commit is contained in:
+15
-15
@@ -125,7 +125,7 @@
|
||||
<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-50 [filter:brightness(1.0)] dark:[filter:sepia(0.2)_saturate(1.2)_hue-rotate(180deg)_brightness(0.9)]"
|
||||
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("/resources/images/background.webp");
|
||||
"
|
||||
@@ -182,73 +182,73 @@
|
||||
<matchmaking-modal
|
||||
id="page-matchmaking"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
class="hidden w-full h-full page-content relative z-50"
|
||||
></ranked-modal>
|
||||
</main-layout>
|
||||
|
||||
|
||||
@@ -362,13 +362,10 @@
|
||||
},
|
||||
"public_lobby": {
|
||||
"title": "Waiting for Game Start...",
|
||||
"teams_Duos": "{team_count} teams of 2 (Duos)",
|
||||
"teams_Trios": "{team_count} teams of 3 (Trios)",
|
||||
"teams_Quads": "{team_count} teams of 4 (Quads)",
|
||||
"waiting_for_players": "Waiting for players",
|
||||
"connecting": "Connecting to lobby...",
|
||||
"starting_in": "Starting in {time}",
|
||||
"starting_game": "Starting game…",
|
||||
"starting_game": "Starting…",
|
||||
"teams_hvn": "Humans vs Nations",
|
||||
"teams_hvn_detailed": "{num} Humans vs {num} Nations",
|
||||
"teams": "{num} teams",
|
||||
@@ -464,7 +461,6 @@
|
||||
"teams": "Teams"
|
||||
},
|
||||
"mode_selector": {
|
||||
"special_title": "Special Mix",
|
||||
"teams_title": "Teams",
|
||||
"teams_count": "{teamCount} teams",
|
||||
"teams_of": "{teamCount} teams of {playersPerTeam}",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { GameEndInfo } from "../core/Schemas";
|
||||
import { GameMapType, hasUnusualThumbnailSize } from "../core/game/Game";
|
||||
import { GameMapType } from "../core/game/Game";
|
||||
import { fetchGameById } from "./Api";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
@@ -107,7 +107,6 @@ export class GameInfoModal extends LitElement {
|
||||
if (!info) {
|
||||
return html``;
|
||||
}
|
||||
const isUnusualThumbnailSize = hasUnusualThumbnailSize(info.config.gameMap);
|
||||
return html`
|
||||
<div
|
||||
class="h-37.5 flex relative justify-between rounded-xl bg-black/20 items-center"
|
||||
@@ -115,9 +114,7 @@ export class GameInfoModal extends LitElement {
|
||||
${this.mapImage
|
||||
? html`<img
|
||||
src="${this.mapImage}"
|
||||
class="absolute place-self-start col-span-full row-span-full h-full rounded-xl mask-[linear-gradient(to_left,transparent,#fff)] ${isUnusualThumbnailSize
|
||||
? "object-cover object-center"
|
||||
: ""}"
|
||||
class="absolute place-self-start col-span-full row-span-full h-full rounded-xl mask-[linear-gradient(to_left,transparent,#fff)] object-cover object-center"
|
||||
/>`
|
||||
: html`<div
|
||||
class="place-self-start col-span-full row-span-full h-full rounded-xl bg-gray-300"
|
||||
|
||||
+199
-115
@@ -1,5 +1,6 @@
|
||||
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { getServerConfigFromClient } from "src/core/configuration/ConfigLoader";
|
||||
import {
|
||||
Duos,
|
||||
GameMapType,
|
||||
@@ -27,7 +28,9 @@ const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]";
|
||||
@customElement("game-mode-selector")
|
||||
export class GameModeSelector extends LitElement {
|
||||
@state() private lobbies: PublicGames | null = null;
|
||||
@state() private mapAspectRatios: Map<GameMapType, number> = new Map();
|
||||
private serverTimeOffset: number = 0;
|
||||
private defaultLobbyTime: number = 0;
|
||||
|
||||
private lobbySocket = new PublicLobbySocket((lobbies) =>
|
||||
this.handleLobbiesUpdate(lobbies),
|
||||
@@ -61,6 +64,9 @@ export class GameModeSelector extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.lobbySocket.start();
|
||||
getServerConfigFromClient().then((config) => {
|
||||
this.defaultLobbyTime = config.gameCreationRate() / 1000;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -81,6 +87,30 @@ export class GameModeSelector extends LitElement {
|
||||
}),
|
||||
);
|
||||
this.requestUpdate();
|
||||
|
||||
const allGames = Object.values(lobbies.games ?? {}).flat();
|
||||
for (const game of allGames) {
|
||||
const mapType = game.gameConfig?.gameMap as GameMapType;
|
||||
if (mapType && !this.mapAspectRatios.has(mapType)) {
|
||||
// New Map reference triggers Lit reactivity; placeholder ratio 1 lets
|
||||
// has() guard against duplicate in-flight fetches.
|
||||
this.mapAspectRatios = new Map(this.mapAspectRatios).set(mapType, 1);
|
||||
terrainMapFileLoader
|
||||
.getMapData(mapType)
|
||||
.manifest()
|
||||
.then((m: any) => {
|
||||
if (m?.map?.width && m?.map?.height) {
|
||||
this.mapAspectRatios = new Map(this.mapAspectRatios).set(
|
||||
mapType,
|
||||
m.map.width / m.map.height,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error(`Failed to load manifest for ${mapType}`, e),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -89,74 +119,110 @@ export class GameModeSelector extends LitElement {
|
||||
const special = this.lobbies?.games?.["special"]?.[0];
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-4 w-[84%] lg:w-full mx-auto pb-4 lg:pb-0">
|
||||
<div class="order-first lg:order-none h-14 lg:hidden">
|
||||
${this.renderSoloButton()}
|
||||
<div class="flex flex-col gap-4 w-[84%] sm:w-full mx-auto pb-4 sm:pb-0">
|
||||
<!-- Solo: mobile only, top -->
|
||||
<div class="sm:hidden h-14">
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.solo"),
|
||||
this.openSinglePlayerModal,
|
||||
"bg-sky-600",
|
||||
)}
|
||||
</div>
|
||||
<!-- Create/ranked/join: mobile only, below solo -->
|
||||
<div class="sm:hidden grid grid-cols-3 gap-4 h-14">
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.create"),
|
||||
this.openHostLobby,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.join"),
|
||||
this.openJoinLobby,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
</div>
|
||||
<!-- Game cards grid -->
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-[3fr_2fr] lg:grid-rows-2 gap-4 lg:h-[28rem]"
|
||||
class="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-4 sm:h-[min(24rem,40vh)]"
|
||||
>
|
||||
${ffa
|
||||
? html`<div class="lg:row-span-2">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
<!-- Left col: main card (desktop only) -->
|
||||
${special
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderSpecialLobbyCard(special)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${teams
|
||||
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
|
||||
: nothing}
|
||||
${special ? this.renderSpecialLobbyCard(special) : nothing}
|
||||
: ffa
|
||||
? html`<div class="hidden sm:block">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<!-- Right col: FFA + teams (desktop only) -->
|
||||
<div class="hidden sm:flex sm:flex-col sm:gap-4">
|
||||
${special && ffa
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${teams
|
||||
? html`<div class="flex-1 min-h-0">
|
||||
${this.renderLobbyCard(teams, this.getLobbyTitle(teams))}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<!-- Mobile: special, ffa, teams inline -->
|
||||
<div class="sm:hidden">
|
||||
${special ? this.renderSpecialLobbyCard(special) : nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${ffa
|
||||
? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="sm:hidden">
|
||||
${teams
|
||||
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Solo: full width, desktop only -->
|
||||
<div class="hidden sm:block h-14">
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.solo"),
|
||||
this.openSinglePlayerModal,
|
||||
"bg-sky-600",
|
||||
)}
|
||||
</div>
|
||||
<!-- Bottom row: create + ranked + join (desktop only) -->
|
||||
<div class="hidden sm:grid grid-cols-3 gap-4 h-14">
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.create"),
|
||||
this.openHostLobby,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.join"),
|
||||
this.openJoinLobby,
|
||||
"bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)]",
|
||||
)}
|
||||
</div>
|
||||
${this.renderQuickActionsSection()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSpecialLobbyCard(lobby: PublicGameInfo) {
|
||||
const subtitle = this.getLobbyTitle(lobby);
|
||||
const mainTitle = translateText("mode_selector.special_title");
|
||||
const titleContent = subtitle
|
||||
? html`
|
||||
<span class="block">${mainTitle}</span>
|
||||
<span class="block text-[10px] leading-tight text-white/70">
|
||||
${subtitle}
|
||||
</span>
|
||||
`
|
||||
: mainTitle;
|
||||
return this.renderLobbyCard(lobby, titleContent);
|
||||
}
|
||||
|
||||
private renderSoloButton() {
|
||||
const title = translateText("main.solo");
|
||||
return html`
|
||||
<button
|
||||
@click=${this.openSinglePlayerModal}
|
||||
class="flex items-center justify-center w-full h-full rounded-xl bg-blue-600 border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] text-sm lg:text-base font-bold text-white uppercase tracking-wider text-center"
|
||||
>
|
||||
${title}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderQuickActionsSection() {
|
||||
return html`
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="h-14 hidden lg:block">${this.renderSoloButton()}</div>
|
||||
<div class="grid grid-cols-3 gap-2 h-14">
|
||||
${this.renderSmallActionCard(
|
||||
translateText("mode_selector.ranked_title"),
|
||||
this.openRankedMenu,
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.create"),
|
||||
this.openHostLobby,
|
||||
)}
|
||||
${this.renderSmallActionCard(
|
||||
translateText("main.join"),
|
||||
this.openJoinLobby,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return this.renderLobbyCard(lobby, this.getLobbyTitle(lobby));
|
||||
}
|
||||
|
||||
private openRankedMenu = () => {
|
||||
@@ -181,11 +247,15 @@ export class GameModeSelector extends LitElement {
|
||||
(document.querySelector("join-lobby-modal") as JoinLobbyModal)?.open();
|
||||
};
|
||||
|
||||
private renderSmallActionCard(title: string, onClick: () => void) {
|
||||
private renderSmallActionCard(
|
||||
title: string,
|
||||
onClick: () => void,
|
||||
bgClass: string = CARD_BG,
|
||||
) {
|
||||
return html`
|
||||
<button
|
||||
@click=${onClick}
|
||||
class="flex items-center justify-center w-full h-full rounded-xl ${CARD_BG} border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] text-sm lg:text-base font-bold text-white uppercase tracking-wider text-center"
|
||||
class="flex items-center justify-center w-full h-full rounded-xl ${bgClass} border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] text-sm lg:text-base font-bold text-white uppercase tracking-wider text-center"
|
||||
>
|
||||
${title}
|
||||
</button>
|
||||
@@ -198,6 +268,11 @@ export class GameModeSelector extends LitElement {
|
||||
) {
|
||||
const mapType = lobby.gameConfig!.gameMap as GameMapType;
|
||||
const mapImageSrc = terrainMapFileLoader.getMapData(mapType).webpPath;
|
||||
const aspectRatio = this.mapAspectRatios.get(mapType);
|
||||
// Use object-contain for extreme aspect ratios (e.g. Amazon River ~20:1) so
|
||||
// the full map is visible instead of being cropped by object-cover.
|
||||
const useContain =
|
||||
aspectRatio !== undefined && (aspectRatio > 4 || aspectRatio < 0.25);
|
||||
const timeRemaining = lobby.startsAt
|
||||
? Math.max(
|
||||
0,
|
||||
@@ -208,12 +283,14 @@ export class GameModeSelector extends LitElement {
|
||||
: undefined;
|
||||
|
||||
let timeDisplay: string = "";
|
||||
let timeDisplayUppercase = false;
|
||||
if (timeRemaining === undefined) {
|
||||
timeDisplay = "-s";
|
||||
timeDisplay = renderDuration(this.defaultLobbyTime);
|
||||
} else if (timeRemaining > 0) {
|
||||
timeDisplay = renderDuration(timeRemaining);
|
||||
} else {
|
||||
timeDisplay = translateText("public_lobby.starting_game");
|
||||
timeDisplayUppercase = true;
|
||||
}
|
||||
|
||||
const mapName = getMapName(lobby.gameConfig?.gameMap);
|
||||
@@ -229,59 +306,78 @@ export class GameModeSelector extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.validateAndJoin(lobby)}
|
||||
class="group flex flex-col w-full h-44 lg:h-full text-white uppercase rounded-2xl overflow-hidden transition-transform duration-200 hover:scale-[1.02] active:scale-[0.98] ${CARD_BG}"
|
||||
class="group relative w-full h-44 sm:h-full text-white uppercase rounded-2xl transition-transform duration-200 hover:scale-[1.02] active:scale-[0.98]"
|
||||
style="background-color: color-mix(in oklab, var(--frenchBlue) 75%, black)"
|
||||
>
|
||||
<div class="relative flex-1 overflow-hidden ${CARD_BG}">
|
||||
<!-- Image clipped separately so overflow-hidden doesn't block absolute children -->
|
||||
<div
|
||||
class="absolute inset-0 rounded-2xl overflow-hidden pointer-events-none"
|
||||
>
|
||||
${mapImageSrc
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${mapName ?? lobby.gameConfig?.gameMap ?? "map"}"
|
||||
draggable="false"
|
||||
class="absolute inset-0 w-full h-full object-contain object-center scale-[1.05] pointer-events-none"
|
||||
class="absolute inset-0 w-full h-full ${useContain
|
||||
? "object-contain"
|
||||
: "object-cover object-center scale-[1.05]"} [image-rendering:auto]"
|
||||
/>`
|
||||
: null}
|
||||
<div
|
||||
class="absolute inset-x-2 bottom-2 flex items-end justify-between gap-2"
|
||||
>
|
||||
${modifierLabels.length > 0
|
||||
? html`<div class="flex flex-col items-start gap-1">
|
||||
${modifierLabels.map(
|
||||
(label) =>
|
||||
html`<span
|
||||
class="px-2 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide bg-teal-600 text-white shadow-[0_0_6px_rgba(13,148,136,0.35)]"
|
||||
>${label}</span
|
||||
>`,
|
||||
)}
|
||||
</div>`
|
||||
: html`<div></div>`}
|
||||
<div class="shrink-0">
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded"
|
||||
>${timeDisplay}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Top row: modifiers + timer -->
|
||||
<div
|
||||
class="absolute inset-x-2 top-2 flex items-start justify-between gap-2"
|
||||
>
|
||||
${modifierLabels.length > 0
|
||||
? html`<div class="flex flex-col items-start gap-1">
|
||||
${modifierLabels.map(
|
||||
(label) =>
|
||||
html`<span
|
||||
class="px-2 py-0.5 rounded text-xs font-bold uppercase tracking-widest bg-teal-600 text-white shadow-[0_0_6px_rgba(13,148,136,0.35)]"
|
||||
>${label}</span
|
||||
>`,
|
||||
)}
|
||||
</div>`
|
||||
: html`<div></div>`}
|
||||
<div class="shrink-0">
|
||||
<span
|
||||
class="text-xs font-bold tracking-widest ${timeDisplayUppercase
|
||||
? "uppercase"
|
||||
: "normal-case"} bg-sky-600 px-2.5 py-1 rounded"
|
||||
>${timeDisplay}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-3 py-2">
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<h3
|
||||
class="text-sm lg:text-base font-bold uppercase tracking-wider text-left leading-tight"
|
||||
>
|
||||
${titleContent}
|
||||
</h3>
|
||||
${mapName
|
||||
? html`<p
|
||||
class="text-[10px] text-white/70 uppercase tracking-wider text-left"
|
||||
>
|
||||
${mapName}
|
||||
</p>`
|
||||
: ""}
|
||||
</div>
|
||||
<!-- Bottom bar: map name + mode, with player count floating above -->
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl"
|
||||
style="overflow: visible;"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-widest shrink-0 ml-2"
|
||||
class="absolute bottom-full right-2 mb-1 flex items-center gap-1 text-xs font-bold tracking-widest bg-black/70 backdrop-blur-sm px-2 py-0.5 rounded"
|
||||
>
|
||||
${lobby.numClients}/${lobby.gameConfig?.maxPlayers}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 inline-block"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
${mapName
|
||||
? html`<p
|
||||
class="text-sm sm:text-base font-bold uppercase tracking-wider text-left leading-tight"
|
||||
>
|
||||
${mapName}
|
||||
</p>`
|
||||
: ""}
|
||||
<h3 class="text-xs text-white/70 uppercase tracking-wider text-left">
|
||||
${titleContent}
|
||||
</h3>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
@@ -334,31 +430,19 @@ export class GameModeSelector extends LitElement {
|
||||
const teamCount = totalPlayers
|
||||
? Math.floor(totalPlayers / 2)
|
||||
: undefined;
|
||||
return teamCount
|
||||
? translateText("public_lobby.teams_Duos", {
|
||||
team_count: String(teamCount),
|
||||
})
|
||||
: formatTeamsOf(undefined, 2);
|
||||
return formatTeamsOf(teamCount, 2);
|
||||
}
|
||||
case Trios: {
|
||||
const teamCount = totalPlayers
|
||||
? Math.floor(totalPlayers / 3)
|
||||
: undefined;
|
||||
return teamCount
|
||||
? translateText("public_lobby.teams_Trios", {
|
||||
team_count: String(teamCount),
|
||||
})
|
||||
: formatTeamsOf(undefined, 3);
|
||||
return formatTeamsOf(teamCount, 3);
|
||||
}
|
||||
case Quads: {
|
||||
const teamCount = totalPlayers
|
||||
? Math.floor(totalPlayers / 4)
|
||||
: undefined;
|
||||
return teamCount
|
||||
? translateText("public_lobby.teams_Quads", {
|
||||
team_count: String(teamCount),
|
||||
})
|
||||
: formatTeamsOf(undefined, 4);
|
||||
return formatTeamsOf(teamCount, 4);
|
||||
}
|
||||
case HumansVsNations: {
|
||||
const humanSlots = config.maxPlayers ?? lobby.numClients;
|
||||
|
||||
@@ -120,8 +120,8 @@ export class GutterAds extends LitElement {
|
||||
return html`
|
||||
<!-- Left Gutter Ad -->
|
||||
<div
|
||||
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-[100] pointer-events-auto items-center justify-center"
|
||||
style="left: calc(50% - 10cm - 230px); top: calc(50% + 10px);"
|
||||
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-40 pointer-events-auto items-center justify-center"
|
||||
style="left: calc(50% - 10.5cm - 208px); top: calc(50% + 10px);"
|
||||
>
|
||||
<div
|
||||
id="${this.leftContainerId}"
|
||||
@@ -131,8 +131,8 @@ export class GutterAds extends LitElement {
|
||||
|
||||
<!-- Right Gutter Ad -->
|
||||
<div
|
||||
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-[100] pointer-events-auto items-center justify-center"
|
||||
style="left: calc(50% + 10cm + 70px); top: calc(50% + 10px);"
|
||||
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-40 pointer-events-auto items-center justify-center"
|
||||
style="left: calc(50% + 10.5cm + 48px); top: calc(50% + 10px);"
|
||||
>
|
||||
<div
|
||||
id="${this.rightContainerId}"
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DesktopNavBar extends LitElement {
|
||||
|
||||
return html`
|
||||
<nav
|
||||
class="hidden lg:flex w-full bg-slate-900 items-center justify-center gap-8 py-4 shrink-0 z-50 relative"
|
||||
class="hidden lg:flex w-full bg-zinc-900/90 backdrop-blur-md items-center justify-center gap-8 py-4 shrink-0 z-50 relative"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="h-8 text-[#2563eb]">
|
||||
|
||||
@@ -10,7 +10,7 @@ export class Footer extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<footer
|
||||
class="[.in-game_&]:hidden bg-slate-950/70 backdrop-blur-md flex flex-col items-center justify-center gap-1 pt-1 pb-3 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
|
||||
class="[.in-game_&]:hidden bg-zinc-900/90 backdrop-blur-md flex flex-col items-center justify-center gap-1 pt-1 pb-3 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto relative z-50"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-4 lg:gap-6 pt-2">
|
||||
<a
|
||||
@@ -73,19 +73,21 @@ export class Footer extends LitElement {
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-xs mt-1 lg:mt-2 grid grid-cols-3 w-full px-4">
|
||||
<div
|
||||
class="text-xs mt-1 lg:mt-2 flex items-center justify-center gap-4 px-4"
|
||||
>
|
||||
<a
|
||||
href="/terms-of-service.html"
|
||||
data-i18n="main.terms_of_service"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors text-left"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
<span data-i18n="main.copyright" class="text-center"></span>
|
||||
<span data-i18n="main.copyright"></span>
|
||||
<a
|
||||
href="/privacy-policy.html"
|
||||
data-i18n="main.privacy_policy"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors text-right"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -22,7 +22,7 @@ export class MainLayout extends LitElement {
|
||||
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-0 lg:px-[clamp(1.5rem,3vw,3rem)] pt-0 lg:pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-0 lg:pb-[clamp(0.75rem,1.5vw,1.5rem)]"
|
||||
>
|
||||
<div
|
||||
class="w-full lg:max-w-[24cm] mx-auto flex flex-col flex-1 gap-0 lg:gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden"
|
||||
class="w-full lg:max-w-[20cm] mx-auto flex flex-col flex-1 gap-0 lg:gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
${this._initialChildren}
|
||||
</div>
|
||||
|
||||
@@ -100,13 +100,16 @@ export class PlayPage extends LitElement {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full pb-4 lg:pb-0 flex flex-col gap-0 lg:grid lg:grid-cols-12 lg:gap-2"
|
||||
class="w-full pb-4 lg:pb-0 flex flex-col gap-4 lg:grid lg:grid-cols-[2fr_1fr] lg:gap-4"
|
||||
>
|
||||
<!-- Mobile: spacer for fixed top bar -->
|
||||
<div class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)]"></div>
|
||||
|
||||
<div
|
||||
class="px-2 py-2 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-y border-white/10 overflow-visible lg:col-span-9 lg:flex lg:items-center lg:gap-x-2 lg:h-[60px] lg:p-3 lg:relative lg:z-20 lg:border-y-0 lg:rounded-xl"
|
||||
class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)] lg:col-span-2 -mb-4"
|
||||
></div>
|
||||
|
||||
<!-- Username: left col -->
|
||||
<div
|
||||
class="px-2 py-2 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-y border-white/10 overflow-visible lg:flex lg:items-center lg:gap-x-2 lg:h-[60px] lg:p-3 lg:relative lg:z-20 lg:border-y-0 lg:rounded-xl"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0 w-full">
|
||||
<username-input
|
||||
@@ -121,7 +124,8 @@ export class PlayPage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex lg:col-span-3 h-[60px] gap-2">
|
||||
<!-- Skin + flag: right col -->
|
||||
<div class="hidden lg:flex h-[60px] gap-2">
|
||||
<pattern-input
|
||||
id="pattern-input-desktop"
|
||||
show-select-label
|
||||
|
||||
@@ -32,8 +32,4 @@
|
||||
.l-header__highlightText {
|
||||
color: #2563eb;
|
||||
font-weight: 700;
|
||||
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px 1px 0px rgb(255, 255, 255));
|
||||
}
|
||||
|
||||
@@ -140,11 +140,6 @@ export enum GameMapType {
|
||||
|
||||
export type GameMapName = keyof typeof GameMapType;
|
||||
|
||||
/** Maps that have unusual thumbnail dimensions requiring object-fit: cover */
|
||||
export function hasUnusualThumbnailSize(map: GameMapType): boolean {
|
||||
return map === GameMapType.AmazonRiver || map === GameMapType.Passage;
|
||||
}
|
||||
|
||||
export const mapCategories: Record<string, GameMapType[]> = {
|
||||
continental: [
|
||||
GameMapType.World,
|
||||
|
||||
Reference in New Issue
Block a user