From f4b47ce06c8c741d548bc07c844f9f9c748561c6 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 29 Jun 2026 16:38:37 -0700 Subject: [PATCH] feat: add search bar to effects picker modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the flag/pattern modals with a search input in the EffectsModal header. Filters the effects grid by name — matching either the raw effect id or its displayed label — and hides the Default tile while searching, the same way FlagInputModal hides its no-flag tile. Co-Authored-By: Claude Opus 4.8 (1M context) --- resources/lang/en.json | 1 + src/client/EffectsModal.ts | 19 +++++++++++++++++++ src/client/components/EffectsGrid.ts | 23 ++++++++++++++++++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 46f64046d..936f531f5 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -394,6 +394,7 @@ }, "effects": { "button_title": "Pick an effect!", + "search": "Search...", "title": "Effects", "type": { "transportShipTrail": "Boat Trail" diff --git a/src/client/EffectsModal.ts b/src/client/EffectsModal.ts index f5aa0591d..8a8df80fd 100644 --- a/src/client/EffectsModal.ts +++ b/src/client/EffectsModal.ts @@ -15,6 +15,11 @@ export class EffectsModal extends BaseModal { @state() private cosmetics: Cosmetics | null = null; @state() private userMeResponse: UserMeResponse | false = false; + @state() private search = ""; + + private handleSearch(event: Event) { + this.search = (event.target as HTMLInputElement).value; + } connectedCallback() { super.connectedCallback(); @@ -43,6 +48,19 @@ export class EffectsModal extends BaseModal { ariaLabel: translateText("common.back"), rightContent: html``, })} + +
+ +
`; } @@ -66,6 +84,7 @@ export class EffectsModal extends BaseModal { mode="select" .cosmetics=${this.cosmetics} .userMeResponse=${this.userMeResponse} + .search=${this.search} > `; diff --git a/src/client/components/EffectsGrid.ts b/src/client/components/EffectsGrid.ts index 9b5d391a4..cc9d1550a 100644 --- a/src/client/components/EffectsGrid.ts +++ b/src/client/components/EffectsGrid.ts @@ -16,6 +16,7 @@ import { purchaseCosmetic, resolveCosmetics, ResolvedCosmetic, + translateCosmetic, } from "../Cosmetics"; import { translateText } from "../Utils"; import "./CosmeticButton"; @@ -47,6 +48,7 @@ export class EffectsGrid extends LitElement { false; @property({ type: String }) mode: "select" | "purchase" = "select"; @property({ attribute: false }) affiliateCode: string | null = null; + @property({ type: String }) search = ""; private userSettings = new UserSettings(); private _onChange = () => this.requestUpdate(); @@ -77,6 +79,17 @@ export class EffectsGrid extends LitElement { this.requestUpdate(); } + private matchesSearch(r: ResolvedCosmetic): boolean { + const q = this.search.trim().toLowerCase(); + if (!q) return true; + const name = (r.cosmetic as Effect | null)?.name; + if (!name) return false; + return ( + name.toLowerCase().includes(q) || + translateCosmetic("effects", name).toLowerCase().includes(q) + ); + } + private itemsForType( all: ResolvedCosmetic[], effectType: EffectType, @@ -85,15 +98,15 @@ export class EffectsGrid extends LitElement { (r) => r.type === "effect" && r.cosmetic !== null && - r.effectType === effectType, + r.effectType === effectType && + this.matchesSearch(r), ); if (this.mode === "purchase") { return ofType.filter((r) => r.relationship === "purchasable"); } - return [ - noneTile(effectType), - ...ofType.filter((r) => r.relationship === "owned"), - ]; + const owned = ofType.filter((r) => r.relationship === "owned"); + // The Default tile has no name to match — hide it while searching. + return this.search.trim() ? owned : [noneTile(effectType), ...owned]; } private renderTile(