diff --git a/src/client/ClanModal.ts b/src/client/ClanModal.ts
index f838f7dba..a0989811b 100644
--- a/src/client/ClanModal.ts
+++ b/src/client/ClanModal.ts
@@ -60,8 +60,20 @@ export class ClanModal extends BaseModal {
} | null = null;
render() {
- const content = this.renderInner();
- if (this.inline) return content;
+ const onListView = this.view === "list" && !this.selectedClanTag;
+ const tabs = onListView
+ ? [
+ { key: "my-clans", label: translateText("clan_modal.my_clans") },
+ { key: "browse", label: translateText("clan_modal.browse") },
+ ]
+ : [];
+ const header = onListView
+ ? modalHeader({
+ title: translateText("clan_modal.title"),
+ onBack: () => this.close(),
+ ariaLabel: translateText("common.back"),
+ })
+ : null;
return html`
this.handleTabChange(key as Tab)}
>
- ${content}
+ ${header ? html`${header}
` : ""}
+ ${this.renderInner()}
`;
}
+ private handleTabChange(tab: Tab) {
+ this.activeTab = tab;
+ this.view = "list";
+ this.selectedClan = null;
+ this.selectedClanTag = "";
+ if (tab === "my-clans") {
+ this.loadMyClans();
+ }
+ }
+
protected onOpen(): void {
this.loadMyClans();
}
@@ -131,16 +157,7 @@ export class ClanModal extends BaseModal {
private renderInner() {
if (this.loading) {
- return html`
-
- ${modalHeader({
- title: translateText("clan_modal.title"),
- onBack: () => this.close(),
- ariaLabel: translateText("common.back"),
- })}
- ${this.renderLoadingSpinner()}
-
- `;
+ return this.renderLoadingSpinner();
}
if (this.view === "my-requests") {
@@ -289,30 +306,20 @@ export class ClanModal extends BaseModal {
>`;
}
- // List view (tabs + my clans / browse)
+ // List view (my clans / browse) — header + tabs are rendered by o-modal
return html`
-
- ${modalHeader({
- title: translateText("clan_modal.title"),
- onBack: () => this.close(),
- ariaLabel: translateText("common.back"),
- })}
- ${this.renderTabs()}
-
- ${this.activeTab === "my-clans"
- ? this.renderMyClans()
- : html`) => {
- this.browseCache = e.detail;
- }}
- @clan-select=${(e: CustomEvent<{ tag: string }>) =>
- this.openDetail(e.detail.tag)}
- >`}
-
-
+ ${this.activeTab === "my-clans"
+ ? this.renderMyClans()
+ : html`) => {
+ this.browseCache = e.detail;
+ }}
+ @clan-select=${(e: CustomEvent<{ tag: string }>) =>
+ this.openDetail(e.detail.tag)}
+ >`}
`;
}
@@ -321,44 +328,6 @@ export class ClanModal extends BaseModal {
this.view = "detail";
}
- private renderTabs() {
- const tabs: { key: Tab; label: string }[] = [
- { key: "my-clans", label: translateText("clan_modal.my_clans") },
- { key: "browse", label: translateText("clan_modal.browse") },
- ];
-
- return html`
-
- ${tabs.map(
- (tab) => html`
-
- `,
- )}
-
- `;
- }
-
private renderMyClans() {
const hasClans = this.myClans.length > 0;
const hasRequests = this.myPendingRequests.length > 0;
diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts
index 6e2d1b00a..97d369d68 100644
--- a/src/client/LangSelector.ts
+++ b/src/client/LangSelector.ts
@@ -210,7 +210,6 @@ export class LangSelector extends LitElement {
"join-lobby-modal",
"emoji-table",
"leader-board",
- "leaderboard-tabs",
"leaderboard-player-list",
"leaderboard-clan-table",
"build-menu",
diff --git a/src/client/LeaderboardModal.ts b/src/client/LeaderboardModal.ts
index d0de01e11..c167f8708 100644
--- a/src/client/LeaderboardModal.ts
+++ b/src/client/LeaderboardModal.ts
@@ -5,7 +5,6 @@ import "./components/leaderboard/LeaderboardClanTable";
import type { LeaderboardClanTable } from "./components/leaderboard/LeaderboardClanTable";
import "./components/leaderboard/LeaderboardPlayerList";
import type { LeaderboardPlayerList } from "./components/leaderboard/LeaderboardPlayerList";
-import "./components/leaderboard/LeaderboardTabs";
import { modalHeader } from "./components/ui/ModalHeader";
import { translateText } from "./Utils";
@@ -81,46 +80,13 @@ export class LeaderboardModal extends BaseModal {
>(${translateText("leaderboard_modal.refresh_time")})`;
- const content = html`
-
- ${modalHeader({
- titleContent: html`
-
-
- ${translateText("leaderboard_modal.title")}
-
- ${this.activeTab === "clans" ? dateRange : ""}
- ${this.activeTab === "players" ? refreshTime : ""}
-
- `,
- onBack: () => this.close(),
- ariaLabel: translateText("common.close"),
- })}
-
-
-
) =>
- this.handleTabChange(event.detail)}
- >
-
-
- ,
- ) => this.handleClanDateRangeChange(event)}
- >
-
-
-
- `;
-
- if (this.inline) return content;
+ const tabs = [
+ {
+ key: "players",
+ label: translateText("leaderboard_modal.ranked_tab"),
+ },
+ { key: "clans", label: translateText("leaderboard_modal.clans_tab") },
+ ];
return html`
+ this.handleTabChange(key as "players" | "clans")}
>
- ${content}
+
+ ${modalHeader({
+ titleContent: html`
+
+
+ ${translateText("leaderboard_modal.title")}
+
+ ${this.activeTab === "clans" ? dateRange : ""}
+ ${this.activeTab === "players" ? refreshTime : ""}
+
+ `,
+ onBack: () => this.close(),
+ ariaLabel: translateText("common.close"),
+ })}
+
+
+
+ ,
+ ) => this.handleClanDateRangeChange(event)}
+ >
+
`;
}
diff --git a/src/client/Store.ts b/src/client/Store.ts
index f4e80b444..460c15a47 100644
--- a/src/client/Store.ts
+++ b/src/client/Store.ts
@@ -41,43 +41,12 @@ export class StoreModal extends BaseModal {
}
private renderHeader(): TemplateResult {
- return html`
- ${modalHeader({
- title: translateText("store.title"),
- onBack: () => this.close(),
- ariaLabel: translateText("common.back"),
- rightContent: html``,
- })}
-
-
-
-
-
- `;
+ return modalHeader({
+ title: translateText("store.title"),
+ onBack: () => this.close(),
+ ariaLabel: translateText("common.back"),
+ rightContent: html``,
+ });
}
private renderPatternGrid(): TemplateResult {
@@ -188,22 +157,18 @@ export class StoreModal extends BaseModal {
render() {
if (!this.isActive && !this.inline) return html``;
- const content = html`
-
- ${this.renderHeader()}
-
- ${this.activeTab === "patterns"
- ? this.renderPatternGrid()
- : this.activeTab === "flags"
- ? this.renderFlagGrid()
- : this.renderPackGrid()}
-
-
- `;
+ const tabs = [
+ { key: "packs", label: translateText("store.packs") },
+ { key: "patterns", label: translateText("store.patterns") },
+ { key: "flags", label: translateText("store.flags") },
+ ];
- if (this.inline) {
- return content;
- }
+ const grid =
+ this.activeTab === "patterns"
+ ? this.renderPatternGrid()
+ : this.activeTab === "flags"
+ ? this.renderFlagGrid()
+ : this.renderPackGrid();
return html`
+ (this.activeTab = key as "patterns" | "flags" | "packs")}
>
- ${content}
+ ${this.renderHeader()}
+ ${grid}
`;
}
diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts
index 1d599ad0d..7860b9296 100644
--- a/src/client/UserSettingModal.ts
+++ b/src/client/UserSettingModal.ts
@@ -319,51 +319,10 @@ export class UserSettingModal extends BaseModal {
? this.renderBasicSettings()
: this.renderKeybindSettings();
- const content = html`
-
-
- ${modalHeader({
- title: translateText("user_setting.title"),
- onBack: () => this.close(),
- ariaLabel: translateText("common.back"),
- showDivider: true,
- })}
-
-
-
-
-
-
-
-
-
- `;
-
- if (this.inline) {
- return content;
- }
+ const tabs = [
+ { key: "basic", label: translateText("user_setting.tab_basic") },
+ { key: "keybinds", label: translateText("user_setting.tab_keybinds") },
+ ];
return html`
+ (this.activeTab = key as "basic" | "keybinds")}
>
- ${content}
+
+ ${modalHeader({
+ title: translateText("user_setting.title"),
+ onBack: () => this.close(),
+ ariaLabel: translateText("common.back"),
+ showDivider: true,
+ })}
+
+ ${activeContent}
`;
}
diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts
index 33bc7e861..fc4801064 100644
--- a/src/client/components/baseComponents/Modal.ts
+++ b/src/client/components/baseComponents/Modal.ts
@@ -2,6 +2,8 @@ import { LitElement, html, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import tailwindStyles from "../../styles.css?inline";
+export type OModalTab = { key: string; label: string };
+
@customElement("o-modal")
export class OModal extends LitElement {
static styles = [unsafeCSS(tailwindStyles)];
@@ -28,6 +30,15 @@ export class OModal extends LitElement {
@property({ type: String })
public maxWidth = "";
+ @property({ type: Array })
+ public tabs: OModalTab[] = [];
+
+ @property({ type: String })
+ public activeTab = "";
+
+ @property({ attribute: false })
+ public onTabChange?: (key: string) => void;
+
public onClose?: () => void;
public open() {
@@ -60,7 +71,48 @@ export class OModal extends LitElement {
super.disconnectedCallback();
}
+ private handleTabClick(key: string) {
+ this.onTabChange?.(key);
+ }
+
+ private renderTabs() {
+ return html`
+
+ ${this.tabs.map((tab) => {
+ const active = this.activeTab === tab.key;
+ return html`
+
+ `;
+ })}
+
+ `;
+ }
+
render() {
+ const shouldRender = this.isModalOpen || this.inline;
+ if (!shouldRender) {
+ return html``;
+ }
+
const backdropClass = this.inline
? "relative z-10 w-full h-full flex items-stretch bg-transparent"
: "fixed inset-0 z-[9999] bg-black/60 flex items-center justify-center overflow-hidden";
@@ -73,42 +125,45 @@ export class OModal extends LitElement {
const wrapperStyle =
!this.inline && this.maxWidth ? `max-width: ${this.maxWidth};` : "";
+ const hasTabs = this.tabs.length > 0;
+ const sectionClass = hasTabs
+ ? "relative flex-1 min-h-0 flex flex-col text-white bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10 overflow-hidden"
+ : "relative flex-1 min-h-0 flex flex-col text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-hidden";
+
return html`
- ${this.isModalOpen
- ? html`
-
`;
}
}
diff --git a/src/client/components/leaderboard/LeaderboardTabs.ts b/src/client/components/leaderboard/LeaderboardTabs.ts
deleted file mode 100644
index 9401e200d..000000000
--- a/src/client/components/leaderboard/LeaderboardTabs.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { html, LitElement } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { translateText } from "../../Utils";
-
-export type LeaderboardTab = "players" | "clans";
-
-@customElement("leaderboard-tabs")
-export class LeaderboardTabs extends LitElement {
- @property({ type: String }) activeTab: LeaderboardTab = "players";
-
- createRenderRoot() {
- return this;
- }
-
- private baseTabClass =
- "px-6 py-2 rounded-full text-sm font-bold uppercase tracking-wider transition-all cursor-pointer select-none";
- private activeTabClass = "bg-blue-600 text-white";
- private inactiveTabClass =
- "text-white/40 hover:text-white/60 hover:bg-white/5";
-
- private getTabClass(active: boolean) {
- return [
- this.baseTabClass,
- active ? this.activeTabClass : this.inactiveTabClass,
- ].join(" ");
- }
-
- @state()
- private playerClass = this.getTabClass(this.activeTab === "players");
- @state()
- private clanClass = this.getTabClass(this.activeTab === "clans");
-
- private handleTabChange(tab: LeaderboardTab) {
- this.dispatchEvent(
- new CustomEvent("tab-change", {
- detail: tab,
- bubbles: true,
- composed: true,
- }),
- );
-
- this.playerClass = this.getTabClass(tab === "players");
- this.clanClass = this.getTabClass(tab === "clans");
- }
-
- render() {
- return html`
-
-
-
-
- `;
- }
-}
diff --git a/tests/client/LeaderboardModal.test.ts b/tests/client/LeaderboardModal.test.ts
index a46ef3f5e..2bef31f1b 100644
--- a/tests/client/LeaderboardModal.test.ts
+++ b/tests/client/LeaderboardModal.test.ts
@@ -101,6 +101,7 @@ beforeEach(() => {
);
});
+import "../../src/client/components/baseComponents/Modal";
import { LeaderboardModal } from "../../src/client/LeaderboardModal";
describe("LeaderboardModal", () => {
@@ -334,7 +335,14 @@ describe("LeaderboardModal", () => {
}),
});
- const tab = modal.querySelector("#clan-leaderboard-tab");
+ modal.inline = true;
+ await modal.updateComplete;
+ const oModal = modal.querySelector("o-modal");
+ await (oModal as unknown as { updateComplete: Promise })
+ .updateComplete;
+ const tab = oModal!.shadowRoot!.querySelector(
+ 'button[role="tab"][data-key="clans"]',
+ );
expect(tab).toBeTruthy();
tab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));