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:
parent
90e9f79fcd
commit
3e780268ba
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "sociowire-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.116",
|
||||
"version": "0.0.117",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
|
|
|
|||
|
|
@ -2200,10 +2200,10 @@ export default function MapView({
|
|||
<button
|
||||
type="button"
|
||||
className="sw-search__iconBtn"
|
||||
title="New Post"
|
||||
onClick={() => handleOpenCreate && handleOpenCreate()}
|
||||
title="My Position"
|
||||
onClick={handleFlyToMe}
|
||||
>
|
||||
<i className="fa-solid fa-plus"></i>
|
||||
<i className="fa-solid fa-location-crosshairs"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1343,71 +1343,91 @@ export function usePostsEngine({
|
|||
// 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]);
|
||||
|
||||
// Quand searchQuery change, chercher via API dans le viewport actuel
|
||||
// Track previous search to detect when search is cleared
|
||||
const prevSearchRef = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
async function runSearch() {
|
||||
try {
|
||||
const map = mapRef.current;
|
||||
let lat;
|
||||
let lon;
|
||||
let radiusKm;
|
||||
try {
|
||||
const view = map ? getViewFromMap(map) : null;
|
||||
if (view?.center) {
|
||||
lon = view.center[0];
|
||||
lat = view.center[1];
|
||||
} else {
|
||||
const center = map?.getCenter?.();
|
||||
lat = center?.lat;
|
||||
lon = center?.lng;
|
||||
if (!map) return;
|
||||
|
||||
// Obtenir le viewport actuel
|
||||
const center = map.getCenter();
|
||||
const bounds = map.getBounds();
|
||||
let radiusKm = 50; // default
|
||||
|
||||
if (bounds) {
|
||||
const sw = bounds.getSouthWest();
|
||||
const ne = bounds.getNorthEast();
|
||||
// Calculer le rayon approximatif du viewport
|
||||
const latDiff = Math.abs(ne.lat - sw.lat);
|
||||
const lonDiff = Math.abs(ne.lng - sw.lng);
|
||||
radiusKm = Math.max(latDiff, lonDiff) * 111 / 2; // ~111km per degree
|
||||
radiusKm = Math.min(Math.max(radiusKm, 10), 2000);
|
||||
}
|
||||
if (view?.radiusKm) {
|
||||
radiusKm = Math.min(Math.max(view.radiusKm * 1.2, 25), 2500);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const data = await fetchUnifiedSearch(q, {
|
||||
type: "posts",
|
||||
lat,
|
||||
lon,
|
||||
lat: center?.lat,
|
||||
lon: center?.lng,
|
||||
radius_km: radiusKm,
|
||||
limit: 60,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const posts = Array.isArray(data?.posts) ? data.posts : [];
|
||||
const normalized = posts.map((p) => normalizePostId(p));
|
||||
let bounds = null;
|
||||
try {
|
||||
const b = map?.getBounds?.();
|
||||
if (b) {
|
||||
const sw = b.getSouthWest();
|
||||
const ne = b.getNorthEast();
|
||||
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", {
|
||||
|
||||
// Remplacer les posts par les résultats de recherche
|
||||
allPostsRef.current = normalized;
|
||||
refreshVisibleAndMarkers(timeHours, true);
|
||||
|
||||
trackEvent("search_viewport", {
|
||||
query_len: q.length,
|
||||
results: posts.length,
|
||||
context: "map",
|
||||
radius_km: Math.round(radiusKm),
|
||||
});
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('[usePostsEngine] Search error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
runSearch();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeHours]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((searchQuery || "").trim()) return;
|
||||
searchResultsRef.current = false;
|
||||
}, [searchQuery]);
|
||||
}, [searchQuery, mapRef, refreshVisibleAndMarkers, timeHours]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
|
|
|
|||
|
|
@ -250,9 +250,10 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
|
|||
onClick={onClose}
|
||||
>
|
||||
<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
|
||||
border border-white/10 shadow-2xl"
|
||||
style={{ maxHeight: 'min(85vh, 85dvh)' }}
|
||||
initial={{ y: "100%", opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: "100%", opacity: 0 }}
|
||||
|
|
@ -288,7 +289,7 @@ export default function PostComposer({ onClose, onPostCreated, islandUsername, i
|
|||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -242,11 +242,25 @@ export default function SmartSearchBar({
|
|||
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 (
|
||||
<div className="sw-search" ref={boxRef}>
|
||||
<div className="sw-search__bar">
|
||||
<form className="sw-search__bar" onSubmit={handleFormSubmit}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
className="sw-search__input"
|
||||
value={q}
|
||||
onChange={(e) => {
|
||||
|
|
@ -294,21 +308,7 @@ export default function SmartSearchBar({
|
|||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
{showList && (
|
||||
<div className="sw-search__dropdown">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
padding: 0;
|
||||
background: linear-gradient(180deg, #0a1628 0%, #0d2847 50%, #1e3a5f 100%);
|
||||
overflow: hidden;
|
||||
/* Prevent browser zoom on touch devices - only our map should zoom */
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.islands-world-canvas {
|
||||
|
|
@ -403,6 +405,10 @@
|
|||
z-index: 700;
|
||||
background: #0a1628;
|
||||
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 {
|
||||
|
|
@ -506,20 +512,72 @@
|
|||
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 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 70px;
|
||||
cursor: crosshair;
|
||||
cursor: grab;
|
||||
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;
|
||||
inset: -10%;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
inset: 0;
|
||||
transform-origin: center center;
|
||||
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 */
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
|||
const [pendingPostPosition, setPendingPostPosition] = useState(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)
|
||||
useEffect(() => {
|
||||
if (zoomedIsland) {
|
||||
|
|
@ -105,8 +111,108 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
|||
setZoomedIsland(null);
|
||||
setIslandPosts([]);
|
||||
setIsZooming(false);
|
||||
setTerrainTransform({ x: 0, y: 0, scale: 1 }); // Reset transform
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || loading) return;
|
||||
|
|
@ -316,23 +422,46 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive terrain area */}
|
||||
{/* Interactive terrain area - map-like pan/zoom */}
|
||||
<div
|
||||
ref={terrainRef}
|
||||
className="island-terrain-interactive"
|
||||
onPointerDown={handleTerrainPointerDown}
|
||||
onPointerMove={handleTerrainPointerMove}
|
||||
onPointerUp={handleTerrainPointerUp}
|
||||
onPointerLeave={handleTerrainPointerUp}
|
||||
onWheel={handleTerrainWheel}
|
||||
onTouchStart={handleTerrainTouchStart}
|
||||
onTouchMove={handleTerrainTouchMove}
|
||||
onTouchEnd={handleTerrainTouchEnd}
|
||||
onClick={(e) => {
|
||||
// Don't place post if we just finished dragging
|
||||
if (isDraggingTerrain.current) return;
|
||||
|
||||
// Only owner can place posts
|
||||
if (!username || username.toLowerCase() !== (zoomedIsland.username || '').toLowerCase()) return;
|
||||
|
||||
const rect = terrainRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
// Calculate position as percentage (0-100)
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
// Calculate position as percentage (0-100), accounting for transform
|
||||
const rawX = e.clientX - rect.left - rect.width / 2 - terrainTransform.x;
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
{/* Transformable content container (pan/zoom like a map) */}
|
||||
<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} />
|
||||
|
|
@ -408,6 +537,32 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
|||
</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"
|
||||
>
|
||||
<i className="fa-solid fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className="island-zoom-btn"
|
||||
onClick={() => setTerrainTransform(prev => ({ ...prev, scale: Math.max(0.5, prev.scale / 1.3) }))}
|
||||
title="Zoom out"
|
||||
>
|
||||
<i className="fa-solid fa-minus" />
|
||||
</button>
|
||||
<button
|
||||
className="island-zoom-btn"
|
||||
onClick={() => setTerrainTransform({ x: 0, y: 0, scale: 1 })}
|
||||
title="Reset view"
|
||||
>
|
||||
<i className="fa-solid fa-expand" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -478,7 +633,7 @@ function createIslandMarker(island, idx, t, currentUsername) {
|
|||
*/
|
||||
function generateIslandSVG(seed, color) {
|
||||
const rng = seededRandom(seed);
|
||||
const size = 140;
|
||||
const size = 200; // Larger islands for better visibility
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const numPoints = 10;
|
||||
|
|
@ -633,7 +788,7 @@ function IslandTerrainBackground({ island }) {
|
|||
*/
|
||||
function IslandShape({ seed, cx, cy, scale, fill, opacity = 1, filter }) {
|
||||
const rng = seededRandom(seed);
|
||||
const baseRadius = 30;
|
||||
const baseRadius = 38; // Larger island to fill more space
|
||||
const numPoints = 12;
|
||||
|
||||
const points = [];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,35 @@
|
|||
* { box-sizing: border-box; }
|
||||
html {
|
||||
/* Prevent zoom/scaling on mobile inputs */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: #050816;
|
||||
color: #ffffff;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ body[data-theme="light"] {
|
|||
background: var(--sw-panel-bg, rgba(15, 18, 28, 0.62));
|
||||
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sw-search__bar{
|
||||
|
|
@ -92,7 +93,8 @@ body[data-theme="light"] {
|
|||
}
|
||||
|
||||
.sw-search__input {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
|
|
@ -102,6 +104,13 @@ body[data-theme="light"] {
|
|||
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 {
|
||||
color: var(--sw-muted, rgba(255,255,255,0.55));
|
||||
}
|
||||
|
|
@ -117,6 +126,8 @@ body[data-theme="light"] {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.sw-search__clearBtn:hover {
|
||||
|
|
@ -132,11 +143,7 @@ body[data-theme="light"] {
|
|||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.sw-search__icons .sw-search__iconBtn:first-child{
|
||||
margin-left: -2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.sw-search__iconBtn {
|
||||
|
|
@ -144,9 +151,9 @@ body[data-theme="light"] {
|
|||
outline: 0;
|
||||
cursor: pointer;
|
||||
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -154,6 +161,17 @@ body[data-theme="light"] {
|
|||
|
||||
background: var(--sw-chip-bg, rgba(255,255,255,0.08));
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue