Main Menu UI Overhaul (#2829)

## Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see
everything.

## 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:

w.o.n
This commit is contained in:
Ryan
2026-01-10 04:26:34 +00:00
committed by GitHub
parent 848a3a5633
commit 5e6c90d9bb
60 changed files with 7671 additions and 4546 deletions
+71 -74
View File
@@ -1,105 +1,102 @@
import { LitElement, css, html } from "lit";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../Utils";
import tailwindStyles from "../../styles.css?inline";
@customElement("o-modal")
export class OModal extends LitElement {
static styles = [unsafeCSS(tailwindStyles)];
@state() public isModalOpen = false;
@property({ type: String }) title = "";
@property({ type: String }) translationKey = "";
@property({ type: Boolean }) alwaysMaximized = false;
@property({ type: Function }) onClose?: () => void;
static styles = css`
.c-modal {
position: fixed;
padding: 1rem;
z-index: 9999;
left: 0;
bottom: 0;
right: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
}
static openCount = 0;
.c-modal__wrapper {
border-radius: 8px;
min-width: 340px;
max-width: 860px;
}
@property({ type: Boolean })
public inline = false;
.c-modal__wrapper.always-maximized {
width: 100%;
min-width: 340px;
max-width: 860px;
min-height: 320px;
/* Fallback for older browsers */
height: 60vh;
/* Use dvh if supported for dynamic viewport handling */
height: 60dvh;
}
@property({ type: Boolean })
public alwaysMaximized = false;
.c-modal__header {
position: relative;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-size: 18px;
background: #000000a1;
text-align: center;
color: #fff;
padding: 1rem 2.4rem 1rem 1.4rem;
}
@property({ type: Boolean })
public hideCloseButton = false;
.c-modal__close {
cursor: pointer;
position: absolute;
right: 1rem;
top: 1rem;
}
@property({ type: String })
public title = "";
@property({ type: Boolean })
public hideHeader = false;
public onClose?: () => void;
.c-modal__content {
background: #23232382;
position: relative;
color: #fff;
padding: 1.4rem;
max-height: 60dvh;
overflow-y: auto;
backdrop-filter: blur(8px);
}
`;
public open() {
this.isModalOpen = true;
if (!this.isModalOpen) {
if (!this.inline) {
OModal.openCount = OModal.openCount + 1;
if (OModal.openCount === 1) document.body.style.overflow = "hidden";
}
this.isModalOpen = true;
}
}
public close() {
if (this.isModalOpen) {
this.isModalOpen = false;
this.onClose?.();
if (!this.inline) {
OModal.openCount = Math.max(0, OModal.openCount - 1);
if (OModal.openCount === 0) document.body.style.overflow = "";
}
}
}
disconnectedCallback() {
// Ensure global counter is decremented if this modal is removed while open.
if (this.isModalOpen && !this.inline) {
OModal.openCount = Math.max(0, OModal.openCount - 1);
if (OModal.openCount === 0) document.body.style.overflow = "";
}
super.disconnectedCallback();
}
render() {
const backdropClass = this.inline
? "relative z-10 w-full h-full flex items-stretch bg-transparent"
: "fixed inset-0 z-[9999] bg-black/70 flex items-center justify-center overflow-hidden";
const wrapperClass = this.inline
? "relative flex flex-col w-full h-full m-0 max-w-full max-h-none shadow-none"
: `relative flex flex-col w-[90%] min-w-[400px] max-w-[900px] m-8 rounded-lg shadow-[0_20px_60px_rgba(0,0,0,0.8)] max-h-[calc(100vh-4rem)] ${
this.alwaysMaximized ? "h-auto" : ""
}`;
return html`
${this.isModalOpen
? html`
<aside class="c-modal" @click=${this.close}>
<aside
class="${backdropClass}"
@click=${this.inline ? null : this.close}
>
<div
@click=${(e: Event) => e.stopPropagation()}
class="c-modal__wrapper ${this.alwaysMaximized
? "always-maximized"
: ""}"
class="${wrapperClass}"
>
<header class="c-modal__header">
${`${this.translationKey}` === ""
? `${this.title}`
: `${translateText(this.translationKey)}`}
<div class="c-modal__close" @click=${this.close}>✕</div>
</header>
<section class="c-modal__content">
${this.inline || this.hideCloseButton
? html``
: html`<div
class="absolute top-4 right-4 z-10 text-white cursor-pointer"
@click=${this.close}
>
</div>`}
${!this.hideHeader && this.title
? html`<div
class="p-[1.4rem] pb-0 text-2xl font-bold text-white"
>
${this.title}
</div>`
: html``}
<section
class="relative flex-1 min-h-0 p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md rounded-lg overflow-y-auto"
>
<slot></slot>
</section>
</div>