feat: add search bar to effects picker modal

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) <noreply@anthropic.com>
This commit is contained in:
evanpelle
2026-06-29 16:38:37 -07:00
parent dae129c6a3
commit f4b47ce06c
3 changed files with 38 additions and 5 deletions
+1
View File
@@ -394,6 +394,7 @@
},
"effects": {
"button_title": "Pick an effect!",
"search": "Search...",
"title": "Effects",
"type": {
"transportShipTrail": "Boat Trail"
+19
View File
@@ -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`<not-logged-in-warning></not-logged-in-warning>`,
})}
<div class="md:flex items-center gap-2 justify-center mt-4">
<input
class="h-12 w-full max-w-md border border-white/10 bg-black/60
rounded-xl shadow-inner text-xl text-center focus:outline-none
focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 text-white placeholder-white/30 transition-all"
type="text"
placeholder=${translateText("effects.search")}
.value=${this.search}
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
</div>
</div>
`;
}
@@ -66,6 +84,7 @@ export class EffectsModal extends BaseModal {
mode="select"
.cosmetics=${this.cosmetics}
.userMeResponse=${this.userMeResponse}
.search=${this.search}
></effects-grid>
</div>
`;
+18 -5
View File
@@ -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(