diff --git a/src/game/WorldMap.jsx b/src/game/WorldMap.jsx
index 04b1ed0..f34387e 100644
--- a/src/game/WorldMap.jsx
+++ b/src/game/WorldMap.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState, useRef } from 'react';
import { WORLD_1 } from './levels/world1.js';
import { WORLD_2 } from './levels/world2.js';
import { WORLD_3 } from './levels/world3.js';
@@ -42,6 +42,21 @@ function isWorldUnlocked(world) {
export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
const totalStars = getTotalStars();
const maxStars = getMaxStars();
+ const [search, setSearch] = useState('');
+ const searchRef = useRef(null);
+
+ const query = search.trim().toLowerCase();
+
+ // Build flat search results when there's a query
+ const searchResults = query ? worlds.flatMap((world, worldIdx) => {
+ return world.levels.map((level, idx) => ({ level, world, worldIdx, idx }))
+ .filter(({ level }) =>
+ level.title.toLowerCase().includes(query) ||
+ level.subtitle.toLowerCase().includes(query) ||
+ level.id.toLowerCase().includes(query) ||
+ world.name.toLowerCase().includes(query)
+ );
+ }) : [];
return (
@@ -69,8 +84,64 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
- {/* All worlds */}
- {worlds.map((world, worldIdx) => {
+ {/* Search bar */}
+
+ 🔍
+ setSearch(e.target.value)}
+ onKeyDown={e => e.key === 'Escape' && (setSearch(''), searchRef.current?.blur())}
+ />
+ {search && (
+
+ )}
+
+
+ {/* Search results */}
+ {query ? (
+
+ {searchResults.length === 0 ? (
+
No se encontraron niveles para "{search}"
+ ) : (
+
{searchResults.length} nivel{searchResults.length !== 1 ? 'es' : ''} encontrado{searchResults.length !== 1 ? 's' : ''}
+ )}
+
+ {searchResults.map(({ level, world, worldIdx, idx }) => {
+ const progress = getLevelProgress(level.id);
+ const levelUnlocked = isLevelUnlocked(level.id, world.levels) && isWorldUnlocked(world);
+ const stars = progress?.stars || 0;
+ const isBoss = idx === world.levels.length - 1;
+
+ return (
+
levelUnlocked && onSelectLevel(level, world)}
+ >
+
{worldIdx + 1}.{idx + 1}
+
+
{level.title}
+
{world.name} — {level.subtitle}
+
+ {levelUnlocked ? (
+
+ ) : (
+
🔒
+ )}
+ {!levelUnlocked &&
}
+
+ );
+ })}
+
+
+ ) : (
+
+ /* All worlds (normal view) */
+ worlds.map((world, worldIdx) => {
const unlocked = isWorldUnlocked(world);
const worldStars = world.levels.reduce((s, l) => {
const p = getLevelProgress(l.id);
@@ -136,7 +207,8 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
);
- })}
+ })
+ )}
);
}
diff --git a/src/index.css b/src/index.css
index bf4a21e..568351d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -323,6 +323,28 @@ html, body, #root {
}
.gm-sandbox-btn:hover { border-color: var(--accent); color: var(--text); }
+/* Level search bar */
+.gm-search-bar {
+ display: flex; align-items: center; gap: 8px;
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
+ padding: 8px 14px; margin-bottom: 24px; transition: border-color 0.15s;
+}
+.gm-search-bar:focus-within { border-color: var(--accent); }
+.gm-search-icon { font-size: 14px; opacity: 0.5; flex-shrink: 0; }
+.gm-search-input {
+ flex: 1; background: none; border: none; outline: none;
+ color: var(--text); font-size: 13px; font-family: inherit;
+}
+.gm-search-input::placeholder { color: var(--text2); }
+.gm-search-clear {
+ background: none; border: none; color: var(--text2); cursor: pointer;
+ font-size: 14px; padding: 2px 4px; border-radius: 4px;
+}
+.gm-search-clear:hover { color: var(--text); background: var(--surface2); }
+.gm-search-results { margin-bottom: 24px; }
+.gm-search-empty { color: var(--text2); font-size: 13px; padding: 16px 0; text-align: center; }
+.gm-search-count { color: var(--text2); font-size: 11px; margin-bottom: 12px; }
+
/* World sections */
.gm-world-section { margin-bottom: 32px; }
.gm-locked-world { opacity: 0.4; }