mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-29 12:42:12 +00:00
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:
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../../../../client/Utils";
|
||||
import { formatKeyForDisplay, translateText } from "../../../../client/Utils";
|
||||
|
||||
@customElement("setting-keybind")
|
||||
export class SettingKeybind extends LitElement {
|
||||
@@ -9,6 +9,7 @@ export class SettingKeybind extends LitElement {
|
||||
@property({ type: String, reflect: true }) action = "";
|
||||
@property({ type: String }) defaultKey = "";
|
||||
@property({ type: String }) value = "";
|
||||
@property({ type: String }) display = "";
|
||||
@property({ type: Boolean }) easter = false;
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -18,43 +19,58 @@ export class SettingKeybind extends LitElement {
|
||||
private listening = false;
|
||||
|
||||
render() {
|
||||
const currentValue = this.value === "" ? "" : this.value || this.defaultKey;
|
||||
const canReset = this.value !== undefined && this.value !== this.defaultKey;
|
||||
const displayValue = this.display || currentValue;
|
||||
const rainbowClass = this.easter
|
||||
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="setting-item column${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label block mb-1">${this.label} </label>
|
||||
<div
|
||||
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
||||
<label class="text-white font-bold text-base block mb-1"
|
||||
>${this.label}</label
|
||||
>
|
||||
<div class="text-white/50 text-sm leading-snug">
|
||||
${this.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-keybind-box flex flex-wrap items-start gap-2">
|
||||
<div
|
||||
class="setting-keybind-description flex-1 min-w-60 max-w-full whitespace-normal wrap-break-words text-sm text-gray-300 [word-break:break-word]"
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<div
|
||||
class="relative h-12 min-w-[80px] px-4 flex items-center justify-center bg-black/40 border border-white/20 rounded-lg text-xl font-bold font-mono shadow-inner hover:border-blue-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/50 transition-all cursor-pointer select-none text-white
|
||||
${this.listening
|
||||
? "border-blue-500 text-blue-400 ring-2 ring-blue-500/50"
|
||||
: ""}"
|
||||
role="button"
|
||||
aria-label="${translateText("user_setting.press_a_key")}"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeydown}
|
||||
@click=${this.startListening}
|
||||
@blur=${this.handleBlur}
|
||||
>
|
||||
${this.listening ? "..." : this.displayKey(displayValue)}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<button
|
||||
class="text-[10px] font-bold uppercase tracking-wider bg-white/5 hover:bg-white/20 border border-white/10 px-3 py-1 rounded text-white/60 hover:text-white transition-colors ${canReset
|
||||
? ""
|
||||
: "opacity-50 cursor-not-allowed pointer-events-none"}"
|
||||
@click=${this.resetToDefault}
|
||||
?disabled=${!canReset}
|
||||
>
|
||||
${this.description}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 gap-y-1 basis-full sm:basis-auto min-w-0"
|
||||
${translateText("user_setting.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="text-[10px] font-bold uppercase tracking-wider bg-white/5 hover:bg-red-500/20 border border-white/10 hover:border-red-500/50 px-3 py-1 rounded text-white/60 hover:text-red-200 transition-colors"
|
||||
@click=${this.unbindKey}
|
||||
>
|
||||
<span
|
||||
class="setting-key shrink-0"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeydown}
|
||||
@click=${this.startListening}
|
||||
>
|
||||
${this.displayKey(this.value || this.defaultKey)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded-sm transition whitespace-normal wrap-break-words max-w-full"
|
||||
@click=${this.resetToDefault}
|
||||
>
|
||||
${translateText("user_setting.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-gray-400 hover:text-white border border-gray-500 px-2 py-0.5 rounded-sm transition whitespace-normal wrap-break-words max-w-full"
|
||||
@click=${this.unbindKey}
|
||||
>
|
||||
${translateText("user_setting.unbind")}
|
||||
</button>
|
||||
</div>
|
||||
${translateText("user_setting.unbind")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,13 +78,8 @@ export class SettingKeybind extends LitElement {
|
||||
}
|
||||
|
||||
private displayKey(key: string): string {
|
||||
if (key === " ") return "Space";
|
||||
if (key.startsWith("Key") && key.length === 4) {
|
||||
return key.slice(3);
|
||||
}
|
||||
return key.length
|
||||
? key.charAt(0).toUpperCase() + key.slice(1)
|
||||
: "Press a key";
|
||||
if (!key) return translateText("user_setting.press_a_key");
|
||||
return formatKeyForDisplay(key);
|
||||
}
|
||||
|
||||
private startListening() {
|
||||
@@ -78,20 +89,40 @@ export class SettingKeybind extends LitElement {
|
||||
|
||||
private handleKeydown(e: KeyboardEvent) {
|
||||
if (!this.listening) return;
|
||||
|
||||
// Allow Tab and Escape to work normally (don't trap focus)
|
||||
if (e.key === "Tab" || e.key === "Escape") {
|
||||
if (e.key === "Escape") {
|
||||
// Cancel listening on Escape
|
||||
this.listening = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default only for keys we're actually capturing
|
||||
e.preventDefault();
|
||||
|
||||
const code = e.code;
|
||||
const prevValue = this.value;
|
||||
|
||||
// Temporarily set the value to the new code for validation in parent
|
||||
this.value = code;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: code, key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
const event = new CustomEvent("change", {
|
||||
detail: { action: this.action, value: code, key: e.key, prevValue },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
|
||||
// If parent rejects (restores value), this.value will be set back externally
|
||||
// Otherwise, keep the new value
|
||||
this.listening = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
this.listening = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
@@ -100,7 +131,10 @@ export class SettingKeybind extends LitElement {
|
||||
this.value = this.defaultKey;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: this.defaultKey },
|
||||
detail: {
|
||||
action: this.action,
|
||||
value: this.defaultKey,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
@@ -108,10 +142,14 @@ export class SettingKeybind extends LitElement {
|
||||
}
|
||||
|
||||
private unbindKey() {
|
||||
this.value = "";
|
||||
this.value = "Null";
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { action: this.action, value: "Null" },
|
||||
detail: {
|
||||
action: this.action,
|
||||
value: "Null",
|
||||
key: "",
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
|
||||
@@ -29,18 +29,28 @@ export class SettingNumber extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const rainbowClass = this.easter
|
||||
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="setting-item${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label" for="setting-number-input"
|
||||
<div
|
||||
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
||||
<label
|
||||
class="text-white font-bold text-base block mb-1"
|
||||
for="setting-number-input"
|
||||
>${this.label}</label
|
||||
>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
<div class="text-white/50 text-sm leading-snug">
|
||||
${this.description}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="setting-number-input"
|
||||
class="setting-input number"
|
||||
class="shrink-0 w-[100px] py-2 px-3 border border-white/20 rounded-lg bg-black/40 text-white font-mono text-center focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
||||
.value=${String(this.value ?? 0)}
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
|
||||
@@ -28,20 +28,10 @@ export class SettingSlider extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private handleSliderChange(e: Event) {
|
||||
const detail = (e as CustomEvent)?.detail;
|
||||
if (!detail || detail.value === undefined) {
|
||||
console.warn("Invalid slider change event", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = detail.value;
|
||||
console.log("Slider changed to", value);
|
||||
}
|
||||
|
||||
private updateSliderStyle(slider: HTMLInputElement) {
|
||||
const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
|
||||
slider.style.background = `linear-gradient(to right, #2196f3 ${percent}%, #444 ${percent}%)`;
|
||||
const clamped = Math.max(0, Math.min(100, percent));
|
||||
slider.style.setProperty("--fill", `${clamped}%`);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@@ -52,24 +42,39 @@ export class SettingSlider extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const rainbowClass = this.easter
|
||||
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label" for="setting-slider-input"
|
||||
<div
|
||||
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
||||
<label class="text-white font-bold text-base block mb-1"
|
||||
>${this.label}</label
|
||||
>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
<div class="text-white/50 text-sm leading-snug">
|
||||
${this.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-2 shrink-0 w-[200px]">
|
||||
<span class="text-white font-bold text-sm">${this.value}%</span>
|
||||
<input
|
||||
type="range"
|
||||
class="w-full appearance-none h-2 bg-transparent rounded outline-none
|
||||
[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded [&::-webkit-slider-runnable-track]:bg-[image:linear-gradient(to_right,#3b82f6_0%,#3b82f6_var(--fill),rgba(255,255,255,0.1)_var(--fill),rgba(255,255,255,0.1)_100%)]
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-[18px] [&::-webkit-slider-thumb]:w-[18px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:-mt-[6px] [&::-webkit-slider-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-webkit-slider-thumb]:transition-all active:[&::-webkit-slider-thumb]:scale-110 active:[&::-webkit-slider-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]
|
||||
[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded [&::-moz-range-track]:bg-white/10
|
||||
[&::-moz-range-progress]:h-2 [&::-moz-range-progress]:rounded [&::-moz-range-progress]:bg-blue-500
|
||||
[&::-moz-range-thumb]:h-[18px] [&::-moz-range-thumb]:w-[18px] [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-blue-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-moz-range-thumb]:transition-all active:[&::-moz-range-thumb]:scale-110 active:[&::-moz-range-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]"
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
.value=${String(this.value)}
|
||||
@input=${this.handleInput}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id="setting-slider-input"
|
||||
class="setting-input slider full-width"
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
.value=${String(this.value)}
|
||||
@input=${this.handleInput}
|
||||
/>
|
||||
<div class="slider-value">${this.value}%</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -26,22 +26,39 @@ export class SettingToggle extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const rainbowClass = this.easter
|
||||
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
|
||||
<div class="toggle-row">
|
||||
<label class="setting-label" for=${this.id}>${this.label}</label>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id=${this.id}
|
||||
?checked=${this.checked}
|
||||
@change=${this.handleChange}
|
||||
/>
|
||||
<span class="slider-round"></span>
|
||||
</label>
|
||||
<label
|
||||
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 cursor-pointer ${rainbowClass}"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0 mr-4">
|
||||
<div class="text-white font-bold text-base block mb-1">
|
||||
${this.label}
|
||||
</div>
|
||||
<div class="text-white/50 text-sm leading-snug">
|
||||
${this.description}
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="relative inline-block w-[52px] h-[28px] shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="opacity-0 w-0 h-0 peer"
|
||||
id=${this.id}
|
||||
?checked=${this.checked}
|
||||
@change=${this.handleChange}
|
||||
/>
|
||||
<span
|
||||
class="absolute inset-0 bg-black/40 border border-white/10 transition-all duration-300 rounded-full
|
||||
before:absolute before:content-[''] before:h-5 before:w-5 before:left-[3px] before:top-[3px]
|
||||
before:bg-white/40 before:transition-all before:duration-300 before:rounded-full before:shadow-sm hover:before:bg-white/60
|
||||
peer-checked:bg-blue-600 peer-checked:border-blue-500 peer-checked:before:translate-x-[24px] peer-checked:before:bg-white"
|
||||
></span>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user