feat: Islands v116 - Search bar fixes and island post improvements

- Fix search bar: single X button, form submit for Enter key
- Fix search clearing to properly refetch normal posts
- Hide native browser search cancel button with CSS
- Add island post support with island_username context
- Interactive island terrain with click-to-place posts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-26 00:51:57 +00:00
parent 90e9f79fcd
commit 3e780268ba
9 changed files with 434 additions and 154 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "sociowire-frontend", "name": "sociowire-frontend",
"private": true, "private": true,
"version": "0.0.116", "version": "0.0.117",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",

View File

@ -2200,10 +2200,10 @@ export default function MapView({
<button <button
type="button" type="button"
className="sw-search__iconBtn" className="sw-search__iconBtn"
title="New Post" title="My Position"
onClick={() => handleOpenCreate && handleOpenCreate()} onClick={handleFlyToMe}
> >
<i className="fa-solid fa-plus"></i> <i className="fa-solid fa-location-crosshairs"></i>
</button> </button>
<button <button
type="button" type="button"

View File

@ -1343,71 +1343,91 @@ export function usePostsEngine({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewParams, mapReady, mainFilter, mainFilters.join(","), subFilter, subFilters.join(","), contentFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]); }, [viewParams, mapReady, mainFilter, mainFilters.join(","), subFilter, subFilters.join(","), contentFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]);
// Quand searchQuery change, chercher via API dans le viewport actuel
// Track previous search to detect when search is cleared
const prevSearchRef = useRef("");
useEffect(() => { useEffect(() => {
const q = (searchQuery || "").trim(); const q = (searchQuery || "").trim();
if (!q) return; const prevQ = prevSearchRef.current;
prevSearchRef.current = q;
if (!q) {
// Search was cleared - force refetch of normal posts
if (prevQ) {
// Had a search before, now cleared -> trigger full refetch
console.log('[usePostsEngine] Search cleared, forcing refetch');
searchResultsRef.current = false;
allPostsRef.current = [];
replacePoolRef.current = true;
const map = mapRef.current;
if (map) {
map.__swForceFetchAt = Date.now();
setTimeout(() => {
try { map.fire("moveend"); } catch {}
}, 50);
}
}
return;
}
// Mark that we're showing search results
searchResultsRef.current = true;
let cancelled = false; let cancelled = false;
async function runSearch() { async function runSearch() {
try { try {
const map = mapRef.current; const map = mapRef.current;
let lat; if (!map) return;
let lon;
let radiusKm; // Obtenir le viewport actuel
try { const center = map.getCenter();
const view = map ? getViewFromMap(map) : null; const bounds = map.getBounds();
if (view?.center) { let radiusKm = 50; // default
lon = view.center[0];
lat = view.center[1]; if (bounds) {
} else { const sw = bounds.getSouthWest();
const center = map?.getCenter?.(); const ne = bounds.getNorthEast();
lat = center?.lat; // Calculer le rayon approximatif du viewport
lon = center?.lng; const latDiff = Math.abs(ne.lat - sw.lat);
} const lonDiff = Math.abs(ne.lng - sw.lng);
if (view?.radiusKm) { radiusKm = Math.max(latDiff, lonDiff) * 111 / 2; // ~111km per degree
radiusKm = Math.min(Math.max(view.radiusKm * 1.2, 25), 2500); radiusKm = Math.min(Math.max(radiusKm, 10), 2000);
} }
} catch {}
const data = await fetchUnifiedSearch(q, { const data = await fetchUnifiedSearch(q, {
type: "posts", type: "posts",
lat, lat: center?.lat,
lon, lon: center?.lng,
radius_km: radiusKm, radius_km: radiusKm,
limit: 60, limit: 100,
}); });
if (cancelled) return; if (cancelled) return;
const posts = Array.isArray(data?.posts) ? data.posts : []; const posts = Array.isArray(data?.posts) ? data.posts : [];
const normalized = posts.map((p) => normalizePostId(p)); const normalized = posts.map((p) => normalizePostId(p));
let bounds = null;
try { // Remplacer les posts par les résultats de recherche
const b = map?.getBounds?.(); allPostsRef.current = normalized;
if (b) { refreshVisibleAndMarkers(timeHours, true);
const sw = b.getSouthWest();
const ne = b.getNorthEast(); trackEvent("search_viewport", {
bounds = { minLat: sw?.lat, minLon: sw?.lng, maxLat: ne?.lat, maxLon: ne?.lng };
}
} catch {}
allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS, bounds);
searchResultsRef.current = true;
refreshVisibleAndMarkers(timeHours);
trackEvent("search_filter_apply", {
query_len: q.length, query_len: q.length,
results: posts.length, results: posts.length,
context: "map", radius_km: Math.round(radiusKm),
}); });
} catch {} } catch (err) {
console.error('[usePostsEngine] Search error:', err);
}
} }
runSearch(); runSearch();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeHours]); }, [searchQuery, mapRef, refreshVisibleAndMarkers, timeHours]);
useEffect(() => {
if ((searchQuery || "").trim()) return;
searchResultsRef.current = false;
}, [searchQuery]);
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;

View File

@ -250,9 +250,10 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
onClick={onClose} onClick={onClose}
> >
<motion.div <motion.div
className="w-full sm:max-w-lg max-h-[90vh] overflow-hidden rounded-t-3xl sm:rounded-3xl className="w-full sm:max-w-lg max-h-[85vh] max-h-[85dvh] overflow-hidden rounded-t-3xl sm:rounded-3xl
bg-gradient-to-b from-surface-800 to-surface-900 bg-gradient-to-b from-surface-800 to-surface-900
border border-white/10 shadow-2xl" border border-white/10 shadow-2xl"
style={{ maxHeight: 'min(85vh, 85dvh)' }}
initial={{ y: "100%", opacity: 0 }} initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
exit={{ y: "100%", opacity: 0 }} exit={{ y: "100%", opacity: 0 }}
@ -288,7 +289,7 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
</div> </div>
{/* Content */} {/* Content */}
<div className="p-4 overflow-y-auto max-h-[calc(90vh-140px)]"> <div className="p-4 overflow-y-auto" style={{ maxHeight: 'calc(85dvh - 140px)' }}>
{/* User info */} {/* User info */}
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center text-white text-lg font-bold"> <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center text-white text-lg font-bold">

View File

@ -242,11 +242,25 @@ export default function SmartSearchBar({
onSearch && onSearch(""); // Clear filters onSearch && onSearch(""); // Clear filters
}; };
const handleFormSubmit = (e) => {
e.preventDefault();
if (q.trim()) {
skipSearchRef.current = true;
setOpen(false);
trackEvent("search_submit", { query_len: q.trim().length });
onSearch && onSearch(q.trim());
if (inputRef.current) {
try { inputRef.current.blur(); } catch {}
}
}
};
return ( return (
<div className="sw-search" ref={boxRef}> <div className="sw-search" ref={boxRef}>
<div className="sw-search__bar"> <form className="sw-search__bar" onSubmit={handleFormSubmit}>
<input <input
ref={inputRef} ref={inputRef}
type="search"
className="sw-search__input" className="sw-search__input"
value={q} value={q}
onChange={(e) => { onChange={(e) => {
@ -294,21 +308,7 @@ export default function SmartSearchBar({
</svg> </svg>
</button> </button>
)} )}
</form>
<div className="sw-search__icons" aria-label="Search actions">
<button
type="button"
className="sw-search__iconBtn"
title={t('map.mySpot')}
onClick={() => {
trackEvent("search_my_spot");
onMySpot && onMySpot();
}}
>
<IconPin />
</button>
</div>
</div>
{showList && ( {showList && (
<div className="sw-search__dropdown"> <div className="sw-search__dropdown">

View File

@ -7,6 +7,8 @@
padding: 0; padding: 0;
background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%); background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%);
overflow: hidden; overflow: hidden;
/* Prevent browser zoom on touch devices - only our map should zoom */
touch-action: pan-x pan-y;
} }
.islands-world-canvas { .islands-world-canvas {
@ -403,6 +405,10 @@
z-index: 700; z-index: 700;
background: #0a1628; background: #0a1628;
animation: islandFadeIn 0.4s ease; animation: islandFadeIn 0.4s ease;
/* Prevent browser zoom - only our island terrain should zoom */
touch-action: none;
-webkit-user-select: none;
user-select: none;
} }
@keyframes islandFadeIn { @keyframes islandFadeIn {
@ -506,20 +512,72 @@
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
} }
/* Interactive terrain area - full screen clickable */ /* Interactive terrain area - full screen with map-like navigation */
.island-terrain-interactive { .island-terrain-interactive {
position: absolute; position: absolute;
inset: 0; inset: 0;
top: 70px; top: 70px;
cursor: crosshair; cursor: grab;
overflow: hidden; overflow: hidden;
/* Prevent browser zoom */
touch-action: none;
} }
.island-terrain-interactive .island-terrain-bg { .island-terrain-interactive:active {
cursor: grabbing;
}
/* Transform container for pan/zoom */
.island-terrain-transform {
position: absolute; position: absolute;
inset: -10%; inset: 0;
width: 120%; transform-origin: center center;
height: 120%; will-change: transform;
transition: none; /* No transition for smooth dragging */
}
.island-terrain-transform .island-terrain-bg {
position: absolute;
inset: -50%;
width: 200%;
height: 200%;
}
/* Zoom controls (like map controls) */
.island-terrain-zoom-controls {
position: absolute;
bottom: 24px;
right: 24px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 10;
}
.island-zoom-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
background: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(56, 189, 248, 0.3);
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(8px);
}
.island-zoom-btn:hover {
background: rgba(56, 189, 248, 0.3);
border-color: rgba(56, 189, 248, 0.5);
}
.island-zoom-btn:active {
transform: scale(0.95);
} }
/* Posts container - overlay for posts on island */ /* Posts container - overlay for posts on island */

View File

@ -34,6 +34,12 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
const [pendingPostPosition, setPendingPostPosition] = useState(null); const [pendingPostPosition, setPendingPostPosition] = useState(null);
const terrainRef = useRef(null); const terrainRef = useRef(null);
// Island terrain pan/zoom state (map-like behavior)
const [terrainTransform, setTerrainTransform] = useState({ x: 0, y: 0, scale: 1 });
const isDraggingTerrain = useRef(false);
const lastPointerPos = useRef({ x: 0, y: 0 });
const lastPinchDist = useRef(0);
// Set global island context when zoomed (for FAB to use) // Set global island context when zoomed (for FAB to use)
useEffect(() => { useEffect(() => {
if (zoomedIsland) { if (zoomedIsland) {
@ -105,8 +111,108 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
setZoomedIsland(null); setZoomedIsland(null);
setIslandPosts([]); setIslandPosts([]);
setIsZooming(false); setIsZooming(false);
setTerrainTransform({ x: 0, y: 0, scale: 1 }); // Reset transform
}, [isZooming]); }, [isZooming]);
// Terrain pan handlers (map-like navigation)
const handleTerrainPointerDown = useCallback((e) => {
// Don't start drag if clicking on a post or button
if (e.target.closest('.island-post-marker, .island-pending-post-marker, button')) return;
isDraggingTerrain.current = true;
lastPointerPos.current = { x: e.clientX, y: e.clientY };
e.currentTarget.style.cursor = 'grabbing';
}, []);
const handleTerrainPointerMove = useCallback((e) => {
if (!isDraggingTerrain.current) return;
const dx = e.clientX - lastPointerPos.current.x;
const dy = e.clientY - lastPointerPos.current.y;
lastPointerPos.current = { x: e.clientX, y: e.clientY };
setTerrainTransform(prev => ({
...prev,
x: prev.x + dx,
y: prev.y + dy
}));
}, []);
const handleTerrainPointerUp = useCallback((e) => {
isDraggingTerrain.current = false;
if (e.currentTarget) e.currentTarget.style.cursor = 'grab';
}, []);
// Terrain wheel zoom (map-like)
const handleTerrainWheel = useCallback((e) => {
e.preventDefault();
const rect = terrainRef.current?.getBoundingClientRect();
if (!rect) return;
// Mouse position relative to terrain center
const mouseX = e.clientX - rect.left - rect.width / 2;
const mouseY = e.clientY - rect.top - rect.height / 2;
const delta = e.deltaY > 0 ? 0.9 : 1.1;
setTerrainTransform(prev => {
const newScale = Math.max(0.5, Math.min(4, prev.scale * delta));
const scaleDiff = newScale / prev.scale;
return {
scale: newScale,
x: prev.x * scaleDiff + mouseX * (1 - scaleDiff),
y: prev.y * scaleDiff + mouseY * (1 - scaleDiff)
};
});
}, []);
// Touch pinch zoom support
const handleTerrainTouchStart = useCallback((e) => {
if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastPinchDist.current = Math.sqrt(dx * dx + dy * dy);
} else if (e.touches.length === 1) {
isDraggingTerrain.current = true;
lastPointerPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
}, []);
const handleTerrainTouchMove = useCallback((e) => {
if (e.touches.length === 2) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (lastPinchDist.current > 0) {
const scaleDelta = dist / lastPinchDist.current;
setTerrainTransform(prev => ({
...prev,
scale: Math.max(0.5, Math.min(4, prev.scale * scaleDelta))
}));
}
lastPinchDist.current = dist;
} else if (e.touches.length === 1 && isDraggingTerrain.current) {
const dx = e.touches[0].clientX - lastPointerPos.current.x;
const dy = e.touches[0].clientY - lastPointerPos.current.y;
lastPointerPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
setTerrainTransform(prev => ({
...prev,
x: prev.x + dx,
y: prev.y + dy
}));
}
}, []);
const handleTerrainTouchEnd = useCallback(() => {
isDraggingTerrain.current = false;
lastPinchDist.current = 0;
}, []);
// Initialize MapLibre // Initialize MapLibre
useEffect(() => { useEffect(() => {
if (!containerRef.current || loading) return; if (!containerRef.current || loading) return;
@ -316,98 +422,147 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
</div> </div>
</div> </div>
{/* Interactive terrain area */} {/* Interactive terrain area - map-like pan/zoom */}
<div <div
ref={terrainRef} ref={terrainRef}
className="island-terrain-interactive" className="island-terrain-interactive"
onPointerDown={handleTerrainPointerDown}
onPointerMove={handleTerrainPointerMove}
onPointerUp={handleTerrainPointerUp}
onPointerLeave={handleTerrainPointerUp}
onWheel={handleTerrainWheel}
onTouchStart={handleTerrainTouchStart}
onTouchMove={handleTerrainTouchMove}
onTouchEnd={handleTerrainTouchEnd}
onClick={(e) => { onClick={(e) => {
// Don't place post if we just finished dragging
if (isDraggingTerrain.current) return;
// Only owner can place posts // Only owner can place posts
if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return; if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return;
const rect = terrainRef.current?.getBoundingClientRect(); const rect = terrainRef.current?.getBoundingClientRect();
if (!rect) return; if (!rect) return;
// Calculate position as percentage (0-100) // Calculate position as percentage (0-100), accounting for transform
const x = ((e.clientX - rect.left) / rect.width) * 100; const rawX = e.clientX - rect.left - rect.width / 2 - terrainTransform.x;
const y = ((e.clientY - rect.top) / rect.height) * 100; const rawY = e.clientY - rect.top - rect.height / 2 - terrainTransform.y;
const x = (rawX / terrainTransform.scale / rect.width * 100) + 50;
const y = (rawY / terrainTransform.scale / rect.height * 100) + 50;
// Keep within bounds
if (x < 0 || x > 100 || y < 0 || y > 100) return;
setPendingPostPosition({ x, y }); setPendingPostPosition({ x, y });
}} }}
> >
{/* Full-screen island terrain background */} {/* Transformable content container (pan/zoom like a map) */}
<IslandTerrainBackground island={zoomedIsland} /> <div
className="island-terrain-transform"
style={{
transform: `translate(${terrainTransform.x}px, ${terrainTransform.y}px) scale(${terrainTransform.scale})`,
}}
>
{/* Full-screen island terrain background */}
<IslandTerrainBackground island={zoomedIsland} />
{/* Loading */} {/* Loading */}
{loadingPosts && ( {loadingPosts && (
<div className="island-terrain-loading"> <div className="island-terrain-loading">
<div className="loading-spinner" /> <div className="loading-spinner" />
</div> </div>
)} )}
{/* Empty state hint */} {/* Empty state hint */}
{!loadingPosts && islandPosts.length === 0 && !pendingPostPosition && ( {!loadingPosts && islandPosts.length === 0 && !pendingPostPosition && (
<div className="island-terrain-empty"> <div className="island-terrain-empty">
<i className="fa-solid fa-umbrella-beach" /> <i className="fa-solid fa-umbrella-beach" />
<p>{t('worlds.emptyIsland')}</p> <p>{t('worlds.emptyIsland')}</p>
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && ( {username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
<p className="island-terrain-hint">{t('worlds.clickToPlacePost')}</p> <p className="island-terrain-hint">{t('worlds.clickToPlacePost')}</p>
)} )}
</div> </div>
)} )}
{/* Posts displayed at their coordinates */} {/* Posts displayed at their coordinates */}
{!loadingPosts && islandPosts.map((post, idx) => ( {!loadingPosts && islandPosts.map((post, idx) => (
<IslandPost <IslandPost
key={post.id || idx} key={post.id || idx}
post={post} post={post}
index={idx} index={idx}
islandSeed={zoomedIsland.seed || 0} islandSeed={zoomedIsland.seed || 0}
/> />
))} ))}
{/* Pending post marker (where user clicked) */} {/* Pending post marker (where user clicked) */}
{pendingPostPosition && ( {pendingPostPosition && (
<div <div
className="island-pending-post-marker" className="island-pending-post-marker"
style={{ style={{
left: `${pendingPostPosition.x}%`, left: `${pendingPostPosition.x}%`,
top: `${pendingPostPosition.y}%` top: `${pendingPostPosition.y}%`
}} }}
>
<div className="pending-marker-icon">
<i className="fa-solid fa-plus" />
</div>
<div className="pending-marker-actions">
<button
className="pending-marker-btn pending-marker-confirm"
onClick={(e) => {
e.stopPropagation();
// Open post composer with position
window.dispatchEvent(new CustomEvent('sw:openPostCreator', {
detail: {
island_username: zoomedIsland.username,
island_x: pendingPostPosition.x,
island_y: pendingPostPosition.y
}
}));
setPendingPostPosition(null);
}}
>
<i className="fa-solid fa-check" />
<span>{t('worlds.postHere')}</span>
</button>
<button
className="pending-marker-btn pending-marker-cancel"
onClick={(e) => {
e.stopPropagation();
setPendingPostPosition(null);
}}
>
<i className="fa-solid fa-times" />
</button>
</div>
</div>
)}
</div>{/* End transform container */}
{/* Zoom controls */}
<div className="island-terrain-zoom-controls">
<button
className="island-zoom-btn"
onClick={() => setTerrainTransform(prev => ({ ...prev, scale: Math.min(4, prev.scale * 1.3) }))}
title="Zoom in"
> >
<div className="pending-marker-icon"> <i className="fa-solid fa-plus" />
<i className="fa-solid fa-plus" /> </button>
</div> <button
<div className="pending-marker-actions"> className="island-zoom-btn"
<button onClick={() => setTerrainTransform(prev => ({ ...prev, scale: Math.max(0.5, prev.scale / 1.3) }))}
className="pending-marker-btn pending-marker-confirm" title="Zoom out"
onClick={(e) => { >
e.stopPropagation(); <i className="fa-solid fa-minus" />
// Open post composer with position </button>
window.dispatchEvent(new CustomEvent('sw:openPostCreator', { <button
detail: { className="island-zoom-btn"
island_username: zoomedIsland.username, onClick={() => setTerrainTransform({ x: 0, y: 0, scale: 1 })}
island_x: pendingPostPosition.x, title="Reset view"
island_y: pendingPostPosition.y >
} <i className="fa-solid fa-expand" />
})); </button>
setPendingPostPosition(null); </div>
}}
>
<i className="fa-solid fa-check" />
<span>{t('worlds.postHere')}</span>
</button>
<button
className="pending-marker-btn pending-marker-cancel"
onClick={(e) => {
e.stopPropagation();
setPendingPostPosition(null);
}}
>
<i className="fa-solid fa-times" />
</button>
</div>
</div>
)}
</div> </div>
</div> </div>
)} )}
@ -478,7 +633,7 @@ function createIslandMarker(island, idx, t, currentUsername) {
*/ */
function generateIslandSVG(seed, color) { function generateIslandSVG(seed, color) {
const rng = seededRandom(seed); const rng = seededRandom(seed);
const size = 140; const size = 200; // Larger islands for better visibility
const cx = size / 2; const cx = size / 2;
const cy = size / 2; const cy = size / 2;
const numPoints = 10; const numPoints = 10;
@ -633,7 +788,7 @@ function IslandTerrainBackground({ island }) {
*/ */
function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) { function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) {
const rng = seededRandom(seed); const rng = seededRandom(seed);
const baseRadius = 30; const baseRadius = 38; // Larger island to fill more space
const numPoints = 12; const numPoints = 12;
const points = []; const points = [];

View File

@ -1,7 +1,35 @@
* { box-sizing: border-box; } * { box-sizing: border-box; }
html {
/* Prevent zoom/scaling on mobile inputs */
touch-action: manipulation;
}
body { body {
margin: 0; margin: 0;
background: #050816; background: #050816;
color: #ffffff; color: #ffffff;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* Prevent horizontal scroll */
overflow-x: hidden;
}
/* Global modal container fixes */
[class*="modal"],
[class*="Modal"],
[class*="overlay"],
[class*="Overlay"] {
/* Prevent modals from exceeding viewport */
max-height: 100vh;
max-height: 100dvh;
max-width: 100vw;
}
/* Prevent zoom on input focus (iOS) */
input, select, textarea {
font-size: 16px !important;
}
@media (min-width: 640px) {
input, select, textarea {
font-size: inherit !important;
}
} }

View File

@ -75,6 +75,7 @@ body[data-theme="light"] {
background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62)); background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62));
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10)); border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
overflow: visible;
} }
.sw-search__bar{ .sw-search__bar{
@ -92,7 +93,8 @@ body[data-theme="light"] {
} }
.sw-search__input { .sw-search__input {
flex: 1; flex: 1 1 auto;
min-width: 0;
border: 0; border: 0;
outline: 0; outline: 0;
background: transparent; background: transparent;
@ -102,6 +104,13 @@ body[data-theme="light"] {
padding: 2px 4px; padding: 2px 4px;
} }
/* Hide native search clear button */
.sw-search__input::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
display: none;
}
.sw-search__input::placeholder { .sw-search__input::placeholder {
color: var(--sw-muted, rgba(255,255,255,0.55)); color: var(--sw-muted, rgba(255,255,255,0.55));
} }
@ -117,6 +126,8 @@ body[data-theme="light"] {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: color 0.15s ease; transition: color 0.15s ease;
flex-shrink: 0;
margin-right: 4px;
} }
.sw-search__clearBtn:hover { .sw-search__clearBtn:hover {
@ -132,11 +143,7 @@ body[data-theme="light"] {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
flex-shrink: 0; flex-shrink: 0;
margin-left: -6px; margin-left: 4px;
}
.sw-search__icons .sw-search__iconBtn:first-child{
margin-left: -2px;
} }
.sw-search__iconBtn { .sw-search__iconBtn {
@ -144,9 +151,9 @@ body[data-theme="light"] {
outline: 0; outline: 0;
cursor: pointer; cursor: pointer;
width: 34px; width: 32px;
height: 34px; height: 32px;
border-radius: 12px; border-radius: 8px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -154,6 +161,17 @@ body[data-theme="light"] {
background: var(--sw-chip-bg, rgba(255,255,255,0.08)); background: var(--sw-chip-bg, rgba(255,255,255,0.08));
color: var(--sw-text, rgba(255,255,255,0.92)); color: var(--sw-text, rgba(255,255,255,0.92));
transition: all 0.2s ease;
}
.sw-search__iconBtn:hover {
background: var(--sw-hover, rgba(255,255,255,0.15));
color: var(--sw-text, #fff);
}
/* My Position button inside search bar */
.sw-search__mySpotBtn {
font-size: 16px;
} }
.sw-search__iconBtn.is-active { .sw-search__iconBtn.is-active {