-
+
-
- ${
- img
- ? `
})
`
- : `
${escapeHtml(normCatLabel(postData))}
`
- }
+
+
+ `;
+ } else {
+ modalWrap.innerHTML = `
+
-
${escapeHtml(data?.headline || postData?.title || "Untitled")}
-
- ${escapeHtml(data?.summary || postData?.snippet || "")}
-
-
-
-
👁 ${formatCount(viewCount)}
-
❤️ ${formatCount(likeCount)}
-
🔁 ${formatCount(shareCount)}
-
💬 ${formatCount(commentCount)}
-
📍 ${escapeHtml(postData?.city || postData?.location_name || "Nearby")}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
${truth}
+
+
+
+ ${
+ img
+ ? `
})
`
+ : `
${escapeHtml(normCatLabel(postData))}
`
+ }
+
-
+
${escapeHtml(data?.headline || postData?.title || "Untitled")}
+ ${tagPills}
-
- `;
+
+
👁 ${formatCount(viewCount)}
+
❤️ ${formatCount(likeCount)}
+
🔁 ${formatCount(shareCount)}
+
💬 ${formatCount(commentCount)}
+ ${heatmapPill}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ bindHeroLightbox(modalWrap);
backdrop.appendChild(modalWrap);
document.body.appendChild(backdrop);
hydrateAvatars(modalWrap);
+ const relatedLinks = modalWrap.querySelectorAll("[data-related-url]");
+ relatedLinks.forEach((el) => {
+ el.addEventListener("click", () => {
+ const target = el.getAttribute("data-related-url");
+ if (!target) return;
+ try {
+ window.open(target, "_blank", "noopener");
+ } catch {
+ window.location.href = target;
+ }
+ });
+ });
requestAnimationFrame(() => {
try { backdrop.classList.add("sw-enter-active"); } catch {}
try { modalWrap.classList.add("sw-enter-active"); } catch {}
@@ -864,6 +1155,14 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
});
}
+ const heatBtn = modalWrap.querySelector('[data-action="cluster-heatmap"]');
+ if (HEATMAP_ENABLED && heatBtn && isCluster) {
+ heatBtn.addEventListener("click", () => {
+ closeCenteredOverlay(map, { force: true });
+ dispatchClusterHeatmap(postData, { mode: "click" });
+ });
+ }
+
if (postID > 0) {
fetchPostById(postID).then((fresh) => {
if (!fresh) return;
@@ -1077,26 +1376,28 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
}
function renderCompact() {
- unmountIfAny();
- root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
- root.style.zIndex = "1";
+ if (!root.__swCompactReady) {
+ root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
+ root.style.zIndex = "1";
- // Add connecting line if offset is significant
- // DISABLED: No longer using pixel offsets
- const hasOffset = false;
- const lineHTML = '';
+ // Add connecting line if offset is significant
+ // DISABLED: No longer using pixel offsets
+ const hasOffset = false;
+ const lineHTML = '';
- root.innerHTML = `
-
-
- ${lineHTML}
- `;
+ root.innerHTML = `
+
+
+ ${lineHTML}
+ `;
+ root.__swCompactReady = true;
+ }
const wrap = root.querySelector(".sw-template-mini-wrap");
if (wrap) {
const activePost = root.__post || post;
const spec = getTemplateSpecForPost(activePost, "mini");
- const data = adaptPostToTemplateData(activePost);
+ const data = adaptPostToTemplateData(activePost, { size: "mini" });
root.__lastTemplateKey = getTemplateKeyForPost(activePost);
root.__lastImage = data?.image || "";
root.__lastTitle = data?.headline || "";
@@ -1109,6 +1410,18 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
renderCompact();
root.__renderCompact = renderCompact;
+ if (HEATMAP_ENABLED && isClusterMainPost(post)) {
+ const handleEnter = () => {
+ dispatchClusterHeatmap(root.__post || post, { mode: "hover", skipPan: true });
+ };
+ const handleLeave = () => {
+ dispatchClusterHeatmapClear();
+ };
+ root.addEventListener("mouseenter", handleEnter);
+ root.addEventListener("mouseleave", handleLeave);
+ root.__swClusterHover = { handleEnter, handleLeave };
+ }
+
const marker = new maplibregl.Marker({
element: root,
anchor: "bottom",
@@ -1176,9 +1489,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
function renderSimple() {
const activePost = root.__post || post;
const color = getMarkerColorForCategory(activePost?.category);
- const img = normalizeImageUrl(
- activePost?.image_small || activePost?.image || activePost?.image_large || activePost?.media_url || "",
- );
+ const img = normalizeImageUrl(activePost?.image_small || "");
root.__lastTemplateKey = getTemplateKeyForPost(activePost);
root.__lastImage = img || "";
root.__lastTitle = (activePost?.title || "").toString();
@@ -1254,11 +1565,15 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
renderSimple();
root.__renderSimple = renderSimple;
+ const isCluster = HEATMAP_ENABLED && isClusterMainPost(post);
const markerEl = root.querySelector(".sw-simple-marker");
const hoverCard = root.querySelector(".sw-simple-hover-card");
// Hover effects
root.addEventListener("mouseenter", () => {
+ if (isCluster) {
+ dispatchClusterHeatmap(root.__post || post, { mode: "hover", skipPan: true });
+ }
if (markerEl) {
markerEl.style.transform = "scale(1.2)";
markerEl.style.boxShadow = "0 4px 16px rgba(0,0,0,0.5)";
@@ -1270,6 +1585,9 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
});
root.addEventListener("mouseleave", () => {
+ if (isCluster) {
+ dispatchClusterHeatmapClear();
+ }
if (markerEl) {
markerEl.style.transform = "scale(1)";
markerEl.style.boxShadow = "0 2px 8px rgba(0,0,0,0.3)";
diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js
index bca4d04..0ac4eb9 100644
--- a/src/components/Map/templateSpecs.js
+++ b/src/components/Map/templateSpecs.js
@@ -9,6 +9,30 @@
* - events
*/
export const TEMPLATE_SPECS = {
+ cluster: {
+ mini_spec: {
+ size: { w: 270, h: 108 },
+ background: { type: "gradient", value: "newsBlue" },
+ radius: 20,
+ layers: [
+ { id: "icon", type: "icon", x: 12, y: 12, w: 72, h: 72, text: "\uf1ea", style: "text.icon", bind: "data.categoryIcon" },
+ { id: "imageOverlay", type: "image", x: 12, y: 12, w: 72, h: 72, bind: "data.image", radius: 12 },
+ { id: "badge", type: "chip", x: 96, y: 10, text: "MAIN NEWS", style: "chip.news" },
+ { id: "title", type: "text", x: 96, y: 34, w: 156, h: 46, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 96, y: 82, w: 156, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 380, h: 280 },
+ background: { type: "solid", value: "surface" },
+ radius: 24,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "MAIN NEWS", style: "chip.news" },
+ { id: "title", type: "text", x: 16, y: 52, w: 348, h: 76, bind: "data.headline", style: "text.h1", maxLines: 3 },
+ { id: "summary", type: "text", x: 16, y: 136, w: 348, h: 90, bind: "data.summary", style: "text.body", maxLines: 4 },
+ ],
+ },
+ },
news: {
mini_spec: {
size: { w: 240, h: 96 },
@@ -185,6 +209,7 @@ function isBreaking(post) {
}
function normCat(post) {
+ if (isClusterPost(post)) return "cluster";
const c = (post?.category || post?.Category || "").toString().toUpperCase();
const sub = (post?.sub_category || post?.subCategory || "").toString().toLowerCase();
@@ -260,14 +285,19 @@ function getCategoryIcon(post) {
return categoryIcons[subCategory] || categoryIcons.default;
}
-export function adaptPostToTemplateData(post) {
- const headline = post?.title || "Untitled";
+export function adaptPostToTemplateData(post, opts = {}) {
+ const headline = post?.cluster_title || post?.title || "Untitled";
const author = post?.author ? `by ${post.author}` : "";
const sub = post?.sub_category ? String(post.sub_category) : "";
- const source = author || sub || "";
+ const clusterCount = Array.isArray(post?.cluster_members) ? post.cluster_members.length : 0;
+ const source = clusterCount > 1 ? `${clusterCount} related reports` : (author || sub || "");
const summary = post?.snippet || post?.body || "";
const url = post?.url || "";
- const rawImage = post?.image_large || post?.image_small || post?.image || post?.media_url || "";
+ const size = (opts.size || "full").toString();
+ const rawImage =
+ size === "mini"
+ ? (post?.image_small || post?.image_medium || post?.image || post?.media_url || "")
+ : (post?.image_medium || post?.image_large || post?.image_small || post?.image || post?.media_url || "");
const image = normalizeImageUrl(rawImage) || `/og/category?cat=${encodeURIComponent((post?.category || "NEWS").toString().toUpperCase())}&variant=mini`;
const categoryIcon = getCategoryIcon(post);
return { headline, source, summary, url, image, categoryIcon };
@@ -280,3 +310,13 @@ function normalizeImageUrl(raw) {
if (value.startsWith("http://")) return `https://${value.slice(7)}`;
return value;
}
+
+function isClusterPost(post) {
+ const clusterPostId = Number(post?.cluster_post_id);
+ const postId = Number(post?.post_id);
+ if (Number.isFinite(clusterPostId) && Number.isFinite(postId) && clusterPostId > 0 && postId > 0) {
+ return clusterPostId === postId;
+ }
+ const url = (post?.url || "").toString();
+ return url.includes("/cluster/");
+}
diff --git a/src/components/Map/useMapCore.js b/src/components/Map/useMapCore.js
index e872ab7..2d9b4a6 100644
--- a/src/components/Map/useMapCore.js
+++ b/src/components/Map/useMapCore.js
@@ -115,6 +115,16 @@ export function useMapCore(theme) {
map.on("load", () => {
setViewParams(getViewFromMap(map));
// Don't force pitch on load - let it stay as configured
+ try {
+ map.__swForceFetchAt = Date.now();
+ map.fire("moveend");
+ setTimeout(() => {
+ try {
+ map.__swForceFetchAt = Date.now();
+ map.fire("moveend");
+ } catch {}
+ }, 650);
+ } catch {}
});
map.on("moveend", () => {
diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js
index 32bebd9..918fba1 100644
--- a/src/components/Map/usePostsEngine.js
+++ b/src/components/Map/usePostsEngine.js
@@ -11,6 +11,15 @@ function getId(p) {
return p?.id ?? p?._id ?? null;
}
+function normalizePostId(post) {
+ if (!post || typeof post !== "object") return post;
+ const pid = Number(post?.post_id ?? post?.postId ?? post?.postID ?? 0);
+ if (Number.isFinite(pid) && pid > 0 && Number(post?.id) !== pid) {
+ return { ...post, reco_id: post?.id ?? post?.reco_id, id: pid };
+ }
+ return post;
+}
+
function getPostKey(p) {
const id = getId(p);
if (id != null && id !== 0) return `id:${id}`;
@@ -46,8 +55,9 @@ function getTemplateKey(post) {
function resolvePostImage(post) {
return (
post?.image_small ||
- post?.image ||
+ post?.image_medium ||
post?.image_large ||
+ post?.image ||
post?.media_url ||
post?.image_url ||
""
@@ -131,6 +141,142 @@ function getPostTimeMs(p) {
return d.getTime();
}
+function isClusterMainPost(p) {
+ const clusterPostId = Number(p?.cluster_post_id);
+ const postId = Number(p?.post_id || p?.id);
+ if (Number.isFinite(clusterPostId) && Number.isFinite(postId) && clusterPostId > 0 && postId > 0) {
+ return clusterPostId === postId;
+ }
+ const key = (p?.cluster_key || p?.clusterKey || "").toString().trim();
+ const members = Array.isArray(p?.cluster_members) ? p.cluster_members : [];
+ if (key && members.length > 0) return true;
+ const url = (p?.url || "").toString();
+ return url.includes("/cluster/");
+}
+
+function clusterColor(post) {
+ const c = (post?.category || "NEWS").toString().toUpperCase();
+ if (c === "EVENT" || c === "EVENTS") return "#a855f7";
+ if (c === "FRIENDS") return "#22c55e";
+ if (c === "MARKET") return "#facc15";
+ return "#38bdf8";
+}
+
+function clusterLineColor(post) {
+ const c = (post?.category || "NEWS").toString().toUpperCase();
+ if (c === "EVENT" || c === "EVENTS") return "rgba(168,85,247,0.7)";
+ if (c === "FRIENDS") return "rgba(34,197,94,0.7)";
+ if (c === "MARKET") return "rgba(250,204,21,0.8)";
+ return "rgba(56,189,248,0.75)";
+}
+
+function convexHull(points) {
+ if (!Array.isArray(points) || points.length < 3) return points || [];
+ const pts = [...points].sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]));
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
+ const lower = [];
+ for (const p of pts) {
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
+ lower.pop();
+ }
+ lower.push(p);
+ }
+ const upper = [];
+ for (let i = pts.length - 1; i >= 0; i--) {
+ const p = pts[i];
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
+ upper.pop();
+ }
+ upper.push(p);
+ }
+ upper.pop();
+ lower.pop();
+ return lower.concat(upper);
+}
+
+function chaikinSmooth(points, iterations = 2) {
+ let pts = points.slice();
+ if (pts.length < 4) return pts;
+ for (let i = 0; i < iterations; i++) {
+ const next = [];
+ for (let j = 0; j < pts.length - 1; j++) {
+ const p0 = pts[j];
+ const p1 = pts[j + 1];
+ const q = [0.75 * p0[0] + 0.25 * p1[0], 0.75 * p0[1] + 0.25 * p1[1]];
+ const r = [0.25 * p0[0] + 0.75 * p1[0], 0.25 * p0[1] + 0.75 * p1[1]];
+ next.push(q, r);
+ }
+ next.push(next[0]);
+ pts = next;
+ }
+ return pts;
+}
+
+function circlePolygon(center, radiusKm, steps = 24) {
+ const [lon, lat] = center;
+ const latDeg = radiusKm / 111;
+ const lonDeg = radiusKm / (111 * Math.cos((lat * Math.PI) / 180) || 1);
+ const coords = [];
+ for (let i = 0; i <= steps; i++) {
+ const angle = (Math.PI * 2 * i) / steps;
+ coords.push([lon + Math.cos(angle) * lonDeg, lat + Math.sin(angle) * latDeg]);
+ }
+ return coords;
+}
+
+function buildClusterAreaFeatures(posts) {
+ const features = [];
+ for (const p of posts || []) {
+ if (!isClusterMainPost(p)) continue;
+ const items = Array.isArray(p?.cluster_items) ? p.cluster_items : [];
+ const pts = items
+ .map((it) => [Number(it?.lon), Number(it?.lat)])
+ .filter((v) => Number.isFinite(v[0]) && Number.isFinite(v[1]));
+ if (pts.length === 0) continue;
+
+ let ring = [];
+ if (pts.length >= 3) {
+ ring = convexHull(pts);
+ if (ring.length < 3) {
+ ring = pts.slice(0, 3);
+ }
+ ring = [...ring, ring[0]];
+ ring = chaikinSmooth(ring, 2);
+ // Slight outward nudge so the bubble touches all points
+ const center = ring.slice(0, -1).reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1]], [0, 0]);
+ const count = Math.max(1, ring.length - 1);
+ const centerPt = [center[0] / count, center[1] / count];
+ ring = ring.map((p, idx) => {
+ if (idx === ring.length - 1) return p;
+ const dx = p[0] - centerPt[0];
+ const dy = p[1] - centerPt[1];
+ return [centerPt[0] + dx * 1.32, centerPt[1] + dy * 1.32];
+ });
+ ring[ring.length - 1] = ring[0];
+ } else {
+ const centerLon = pts.reduce((s, v) => s + v[0], 0) / pts.length;
+ const centerLat = pts.reduce((s, v) => s + v[1], 0) / pts.length;
+ let maxKm = 0.0;
+ for (const [lon, lat] of pts) {
+ maxKm = Math.max(maxKm, haversineKm(centerLat, centerLon, lat, lon));
+ }
+ const radius = Math.min(80, Math.max(0.4, maxKm * 1.55 || 0.6));
+ ring = circlePolygon([centerLon, centerLat], radius, 28);
+ }
+
+ features.push({
+ type: "Feature",
+ geometry: { type: "Polygon", coordinates: [ring] },
+ properties: {
+ id: getId(p) || 0,
+ color: clusterColor(p),
+ line: clusterLineColor(p),
+ },
+ });
+ }
+ return features;
+}
+
const MAX_POOL_POSTS = 75;
const MAX_VISIBLE_POSTS = 45;
@@ -159,6 +305,11 @@ export function usePostsEngine({
subFilter,
timeFilter,
searchQuery,
+ clusterFocus,
+ mainNewsOnly,
+ clustersEnabled = true,
+ heatmapEnabled = true,
+ forceFullPostId = null,
markersRef,
expandedElRef,
onAutoWidenTimeFilter,
@@ -174,15 +325,20 @@ export function usePostsEngine({
const initialLoadRef = useRef(true);
const initialFetchKickRef = useRef(false);
const autoWidenRef = useRef(false);
+ const searchResultsRef = useRef(false);
+ const retryRef = useRef({ key: "", count: 0, ts: 0 });
const [mapReady, setMapReady] = useState(false);
const clusterModeRef = useRef(false);
const CLUSTER_THRESHOLD = 140;
- const SIMPLE_MARKER_LIMIT = 40;
+ const SIMPLE_MARKER_LIMIT = 50;
const CLUSTER_SOURCE_ID = "sw-posts";
const CLUSTER_LAYER_ID = "sw-clusters";
const CLUSTER_COUNT_ID = "sw-cluster-count";
const UNCLUSTERED_ID = "sw-unclustered";
+ const CLUSTER_AREA_SOURCE_ID = "sw-cluster-areas";
+ const CLUSTER_AREA_FILL_ID = "sw-cluster-area-fill";
+ const CLUSTER_AREA_LINE_ID = "sw-cluster-area-line";
useEffect(() => {
if (mapReady) return;
@@ -215,7 +371,7 @@ export function usePostsEngine({
}, [mapReady, mapRef]);
const priorityOpts = {
- maxFullCards: 12,
+ maxFullCards: 15,
clusterRadius: 0.002,
minScoreForCard: 50,
};
@@ -333,6 +489,38 @@ export function usePostsEngine({
}
}, [onSelectPost]);
+ const ensureClusterAreaLayers = useCallback((map) => {
+ if (!map) return;
+ if (typeof map.isStyleLoaded === "function" && !map.isStyleLoaded()) return;
+ if (map.getSource(CLUSTER_AREA_SOURCE_ID)) return;
+
+ map.addSource(CLUSTER_AREA_SOURCE_ID, {
+ type: "geojson",
+ data: { type: "FeatureCollection", features: [] },
+ });
+
+ map.addLayer({
+ id: CLUSTER_AREA_FILL_ID,
+ type: "fill",
+ source: CLUSTER_AREA_SOURCE_ID,
+ paint: {
+ "fill-color": ["get", "color"],
+ "fill-opacity": 0.42,
+ },
+ });
+
+ map.addLayer({
+ id: CLUSTER_AREA_LINE_ID,
+ type: "line",
+ source: CLUSTER_AREA_SOURCE_ID,
+ paint: {
+ "line-color": ["get", "line"],
+ "line-width": 2,
+ "line-opacity": 0.7,
+ },
+ });
+ }, [CLUSTER_AREA_SOURCE_ID, CLUSTER_AREA_FILL_ID, CLUSTER_AREA_LINE_ID]);
+
const setClusterVisibility = useCallback((map, visible) => {
if (!map) return;
const v = visible ? "visible" : "none";
@@ -341,6 +529,14 @@ export function usePostsEngine({
if (map.getLayer(UNCLUSTERED_ID)) map.setLayoutProperty(UNCLUSTERED_ID, "visibility", v);
}, []);
+ const updateClusterAreas = useCallback((map, posts) => {
+ if (!map) return;
+ const src = map.getSource(CLUSTER_AREA_SOURCE_ID);
+ if (!src || !src.setData) return;
+ const features = buildClusterAreaFeatures(posts || []);
+ src.setData({ type: "FeatureCollection", features });
+ }, [CLUSTER_AREA_SOURCE_ID]);
+
const updateClusterData = useCallback((map, posts) => {
if (!map) return;
const src = map.getSource(CLUSTER_SOURCE_ID);
@@ -380,6 +576,10 @@ export function usePostsEngine({
(visible) => {
const map = mapRef.current;
if (!map) return { fullCardPosts: [], clusterPosts: visible || [] };
+ const searchActive = (searchQuery || "").trim().length > 0;
+ if (searchActive) {
+ return { fullCardPosts: [], clusterPosts: visible || [] };
+ }
const center = map.getCenter();
const viewCenter = center ? { lng: center.lng, lat: center.lat } : null;
const { fullCardPosts, simpleMarkerPosts } = prioritizePosts(
@@ -389,7 +589,7 @@ export function usePostsEngine({
);
return { fullCardPosts, clusterPosts: simpleMarkerPosts };
},
- [mapRef, priorityOpts]
+ [mapRef, priorityOpts, searchQuery]
);
const syncPriorityMarkers = useCallback(
@@ -458,17 +658,27 @@ export function usePostsEngine({
map.__swMarkersRef = markersRef;
// If user is reading an expanded overlay, don't touch markers
- if (expandedElRef?.current) return;
+ if (expandedElRef?.current || map.__swCenteredOverlay) return;
// Use priority system to decide which posts show full cards vs simple markers
const center = map.getCenter();
const viewCenter = center ? { lng: center.lng, lat: center.lat } : null;
- const { fullCardPosts, simpleMarkerPosts } = prioritizePosts(visible, viewCenter, {
- maxFullCards: 12, // Max 12 cartes complètes (réduit de 15)
- clusterRadius: 0.002, // ~200m clustering radius (augmenté pour plus de groupement)
- minScoreForCard: 50, // Score minimum pour carte complète (augmenté de 35 à 50)
- });
+ const searchActive = (searchQuery || "").trim().length > 0;
+ let fullCardPosts = [];
+ let simpleMarkerPosts = [];
+ if (searchActive) {
+ fullCardPosts = visible || [];
+ simpleMarkerPosts = [];
+ } else {
+ const prioritized = prioritizePosts(visible, viewCenter, {
+ maxFullCards: 15, // 10-15 cartes complètes selon le zoom
+ clusterRadius: 0.002, // ~200m clustering radius (augmenté pour plus de groupement)
+ minScoreForCard: 50, // Score minimum pour carte complète (augmenté de 35 à 50)
+ });
+ fullCardPosts = prioritized.fullCardPosts;
+ simpleMarkerPosts = prioritized.simpleMarkerPosts;
+ }
console.log('[usePostsEngine] Prioritized:', {
total: visible.length,
@@ -482,7 +692,9 @@ export function usePostsEngine({
const id = getId(post);
if (id != null) {
// Tag avec le type de marqueur souhaité
- const isFullCard = fullCardPosts.some(p => getId(p) === id);
+ const isFullCard =
+ id === forceFullPostId ||
+ fullCardPosts.some(p => getId(p) === id);
wanted.set(id, { post, type: isFullCard ? 'full' : 'simple' });
}
}
@@ -548,36 +760,58 @@ export function usePostsEngine({
}
}
},
- [mapRef, markersRef, expandedElRef, theme]
+ [mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId]
);
const computeVisible = useCallback(
(tf) => {
- const posts = allPostsRef.current || [];
+ const basePosts = allPostsRef.current || [];
+ const extraPosts = Array.isArray(clusterFocus?.posts) ? clusterFocus.posts : [];
+ const posts = extraPosts.length
+ ? Array.from(new Map([...extraPosts, ...basePosts].map((p) => {
+ const normalized = normalizePostId(p);
+ return [getPostKey(normalized), normalized];
+ })).values())
+ : basePosts;
const catCode = categoryCode(mainFilter);
+ const clusterIDs = clustersEnabled && Array.isArray(clusterFocus?.ids) ? clusterFocus.ids : null;
+ const isClusterFocus = !!(clustersEnabled && clusterIDs && clusterIDs.length > 0);
return posts.filter((p) => {
+ if (isClusterFocus) {
+ const id = Number(getId(p));
+ const clusterPostId = Number(p?.cluster_post_id);
+ if (clusterIDs && !clusterIDs.includes(id) && !clusterIDs.includes(clusterPostId)) {
+ return false;
+ }
+ return true;
+ }
+ if (clustersEnabled && mainNewsOnly && !isClusterMainPost(p)) return false;
if (catCode) {
const pc = (p.category || p.Category || "").toString().toUpperCase();
if (pc !== catCode) return false;
}
if (!matchesSubFilter(p, subFilter)) return false;
if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
- if (!matchesSearch(p, searchQuery)) return false;
+ if (!searchResultsRef.current && !matchesSearch(p, searchQuery)) return false;
return true;
});
},
- [mainFilter, subFilter, searchQuery]
+ [mainFilter, subFilter, searchQuery, clusterFocus, mainNewsOnly, clustersEnabled]
);
const refreshVisibleAndMarkers = useCallback(
(tf, force = false) => {
const vAll = computeVisible(tf);
const map = mapRef.current;
- let v = (searchQuery || "").trim() ? vAll : filterByBounds(vAll, map);
- if (v.length > MAX_VISIBLE_POSTS) {
- const center = map?.getCenter?.();
- v = prunePosts(v, [center?.lng, center?.lat], MAX_VISIBLE_POSTS);
+ const isClusterFocus = !!(clustersEnabled && Array.isArray(clusterFocus?.ids) && clusterFocus.ids.length > 0);
+ let v = vAll;
+ if (!isClusterFocus) {
+ v = filterByBounds(vAll, map);
+ if (v.length > MAX_VISIBLE_POSTS) {
+ const center = map?.getCenter?.();
+ v = prunePosts(v, [center?.lng, center?.lat], MAX_VISIBLE_POSTS);
+ }
}
// reduce wall blink: don't update if same ids
@@ -589,7 +823,12 @@ export function usePostsEngine({
// keep status silent (no annoying bubbles)
setStatus("");
- if (map && v.length >= CLUSTER_THRESHOLD) {
+ if (map && clustersEnabled && heatmapEnabled) {
+ ensureClusterAreaLayers(map);
+ updateClusterAreas(map, v);
+ }
+
+ if (clustersEnabled && map && v.length >= CLUSTER_THRESHOLD) {
const { fullCardPosts, clusterPosts } = splitForClusters(v);
const simpleVisible = clusterPosts.slice(0, SIMPLE_MARKER_LIMIT);
const clusterRemainder = clusterPosts.slice(SIMPLE_MARKER_LIMIT);
@@ -609,13 +848,22 @@ export function usePostsEngine({
syncMarkers(v);
},
- [computeVisible, syncMarkers, mapRef, ensureClusterLayers, updateClusterData, setClusterVisibility, splitForClusters, syncPriorityMarkers, viewParams]
+ [computeVisible, syncMarkers, mapRef, ensureClusterLayers, ensureClusterAreaLayers, updateClusterAreas, updateClusterData, setClusterVisibility, splitForClusters, syncPriorityMarkers, viewParams, clustersEnabled, heatmapEnabled, clusterFocus, searchQuery]
);
// On filter/search changes: recompute + sync (NO clear-all rebuild)
useEffect(() => {
refreshVisibleAndMarkers(timeFilter);
- }, [timeFilter, mainFilter, subFilter, searchQuery, refreshVisibleAndMarkers]);
+ }, [timeFilter, mainFilter, subFilter, searchQuery, clusterFocus, refreshVisibleAndMarkers]);
+
+ useEffect(() => {
+ if (!clustersEnabled) return;
+ if (!clusterFocus) return;
+ if (Array.isArray(clusterFocus.posts) && clusterFocus.posts.length > 0) {
+ allPostsRef.current = clusterFocus.posts;
+ }
+ refreshVisibleAndMarkers(timeFilter, true);
+ }, [clusterFocus, timeFilter, refreshVisibleAndMarkers, clustersEnabled]);
// On view change: re-filter markers/cards to current bounds even without fetch
useEffect(() => {
@@ -637,7 +885,7 @@ export function usePostsEngine({
if ((searchQuery || "").trim()) return;
// If expanded overlay is open: delay fetch (do NOT disturb UI)
- if (expandedElRef?.current) {
+ if (expandedElRef?.current || mapObj?.__swCenteredOverlay) {
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
delayedFetchRef.current = setTimeout(load, 1200);
return;
@@ -716,7 +964,7 @@ export function usePostsEngine({
lat,
lon: lng,
radius_km: radiusKm,
- limit: 45,
+ limit: 50,
zoom,
bounds,
username: username || undefined,
@@ -727,18 +975,20 @@ export function usePostsEngine({
const existing = allPostsRef.current || [];
const byId = new Map();
for (const p of existing) {
- const key = getPostKey(p);
+ const normalized = normalizePostId(p);
+ const key = getPostKey(normalized);
if (key) byId.set(key, p);
}
if (Array.isArray(newPosts)) {
for (const p of newPosts) {
- const key = getPostKey(p);
+ const normalized = normalizePostId(p);
+ const key = getPostKey(normalized);
if (!key) continue;
const prior = byId.get(key);
if (prior) {
- byId.set(key, { ...prior, ...p });
+ byId.set(key, { ...prior, ...normalized });
} else {
- byId.set(key, p);
+ byId.set(key, normalized);
}
}
}
@@ -776,7 +1026,19 @@ export function usePostsEngine({
setLoadingPosts(false);
} catch (err) {
-
+ setLoadingPosts(false);
+ setLoadError("fetch failed");
+ const key = `${filterKey}|${Math.round(lat * 100)}|${Math.round(lng * 100)}|${Math.round(radiusKm)}`;
+ const now = Date.now();
+ if (retryRef.current.key !== key || now - retryRef.current.ts > 5000) {
+ retryRef.current = { key, count: 0, ts: now };
+ }
+ if (retryRef.current.count < 1) {
+ retryRef.current.count += 1;
+ retryRef.current.ts = now;
+ if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
+ delayedFetchRef.current = setTimeout(load, 650);
+ }
}
}
@@ -784,8 +1046,12 @@ export function usePostsEngine({
clearTimeout(moveDebounceRef.current);
moveDebounceRef.current = null;
}
- initialLoadRef.current = false;
- load();
+ if (initialLoadRef.current) {
+ initialLoadRef.current = false;
+ load();
+ } else {
+ moveDebounceRef.current = setTimeout(load, 300);
+ }
return () => {
cancelled = true;
if (moveDebounceRef.current) {
@@ -822,7 +1088,9 @@ export function usePostsEngine({
});
if (cancelled) return;
const posts = Array.isArray(data?.posts) ? data.posts : [];
- allPostsRef.current = prunePosts(posts, [lon, lat], MAX_POOL_POSTS);
+ const normalized = posts.map((p) => normalizePostId(p));
+ allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS);
+ searchResultsRef.current = true;
refreshVisibleAndMarkers(timeFilter);
trackEvent("search_filter_apply", {
query_len: q.length,
@@ -838,10 +1106,16 @@ export function usePostsEngine({
};
}, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeFilter]);
+ useEffect(() => {
+ if ((searchQuery || "").trim()) return;
+ searchResultsRef.current = false;
+ }, [searchQuery]);
+
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const handleStyle = () => {
+ if (!clustersEnabled) return;
if (!clusterModeRef.current) return;
ensureClusterLayers(map);
setClusterVisibility(map, true);
@@ -851,7 +1125,7 @@ export function usePostsEngine({
};
map.on("styledata", handleStyle);
return () => map.off("styledata", handleStyle);
- }, [mapRef, ensureClusterLayers, setClusterVisibility, updateClusterData, computeVisible, timeFilter, splitForClusters]);
+ }, [mapRef, ensureClusterLayers, setClusterVisibility, updateClusterData, computeVisible, timeFilter, splitForClusters, clustersEnabled]);
const handleIncomingPost = useCallback(
(p) => {
@@ -862,7 +1136,8 @@ export function usePostsEngine({
}
const map = mapRef.current;
const center = map?.getCenter?.();
- allPostsRef.current = prunePosts([p, ...allPostsRef.current], [center?.lng, center?.lat], MAX_POOL_POSTS);
+ const normalized = normalizePostId(p);
+ allPostsRef.current = prunePosts([normalized, ...allPostsRef.current], [center?.lng, center?.lat], MAX_POOL_POSTS);
const catCode = categoryCode(mainFilter);
if (catCode) {
@@ -879,13 +1154,13 @@ export function usePostsEngine({
}
// add marker if missing
- const id = getId(p);
+ const id = getId(normalized);
const have = new Set((markersRef.current || []).map((x) => x.id));
if (id != null && !have.has(id)) {
- createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
+ createMarkerForPost(normalized, mapRef, markersRef, expandedElRef, theme);
}
- setVisiblePosts((current) => [p, ...current]);
+ setVisiblePosts((current) => [normalized, ...current]);
},
[mainFilter, subFilter, timeFilter, searchQuery, mapRef, markersRef, expandedElRef, theme, refreshVisibleAndMarkers]
);
@@ -917,7 +1192,7 @@ export function usePostsEngine({
updated.push(p);
continue;
}
- const next = { ...p, ...(postPatch || {}) };
+ const next = normalizePostId({ ...p, ...(postPatch || {}) });
if (counts) {
if (Number.isFinite(counts.view_count)) next.view_count = counts.view_count;
if (Number.isFinite(counts.like_count)) next.like_count = counts.like_count;
diff --git a/src/components/Posts/PostCard.jsx b/src/components/Posts/PostCard.jsx
index f9e7c1c..d91172c 100644
--- a/src/components/Posts/PostCard.jsx
+++ b/src/components/Posts/PostCard.jsx
@@ -187,7 +187,8 @@ export default function PostCard({ post, selected, onSelect }) {
setPostGroupId((post?.group_id || "").toString());
}, [post?.visibility, post?.group_id]);
- const rawImg = post?.image_large || post?.image_small || post?.image || post?.media_url || "";
+ const rawImg =
+ post?.image_medium || post?.image_large || post?.image_small || post?.image || post?.media_url || "";
const img = normalizeImageUrl(rawImg);
const title = post?.title || "Untitled";
const snippet = post?.snippet || "";
diff --git a/src/components/Posts/PostCardTemplate.jsx b/src/components/Posts/PostCardTemplate.jsx
index 2d5298c..da6771d 100644
--- a/src/components/Posts/PostCardTemplate.jsx
+++ b/src/components/Posts/PostCardTemplate.jsx
@@ -22,7 +22,7 @@ export default function PostCardTemplate({ post, theme = "blue", onOpen }) {
source: post.author ? `by ${post.author}` : (post.sub_category || ""),
summary: post.snippet || post.body || "",
url: post.url || "",
- image: post.image || post.media_url || "",
+ image: post.image_medium || post.image_large || post.image_small || post.image || post.media_url || "",
};
}, [post]);
diff --git a/src/components/Search/SmartSearchBar.jsx b/src/components/Search/SmartSearchBar.jsx
index 6eb9f77..39d2195 100644
--- a/src/components/Search/SmartSearchBar.jsx
+++ b/src/components/Search/SmartSearchBar.jsx
@@ -79,6 +79,7 @@ export default function SmartSearchBar({
const abortRef = useRef(null);
const skipSearchRef = useRef(false);
const boxRef = useRef(null);
+ const inputRef = useRef(null);
useEffect(() => {
function onDocDown(e) {
@@ -137,13 +138,14 @@ export default function SmartSearchBar({
}, [q]);
const showList = open && (items.length > 0 || loading || err);
+ const clearOnFocusRef = useRef(false);
const pick = (it, reason = "click") => {
if (!it) return;
const zoom = defaultZoomForKind(it.kind);
skipSearchRef.current = true;
setOpen(false);
- setQ(it.title || q);
+ clearOnFocusRef.current = true;
trackEvent("search_pick", {
kind: it.kind || "",
@@ -152,6 +154,9 @@ export default function SmartSearchBar({
title_len: (it.title || "").length,
});
onPick && onPick(it, { zoom, reason });
+ if (inputRef.current) {
+ try { inputRef.current.blur(); } catch {}
+ }
};
const onKeyDown = (e) => {
@@ -171,6 +176,9 @@ export default function SmartSearchBar({
setOpen(false);
trackEvent("search_submit", { query_len: q.trim().length });
onSearch && onSearch(q.trim());
+ if (inputRef.current) {
+ try { inputRef.current.blur(); } catch {}
+ }
}
} else if (e.key === "Escape") {
setOpen(false);
@@ -194,6 +202,7 @@ export default function SmartSearchBar({
{
@@ -205,10 +214,13 @@ export default function SmartSearchBar({
}
}}
onFocus={() => {
- if (q.trim()) {
+ if (clearOnFocusRef.current) {
+ clearOnFocusRef.current = false;
setQ("");
setItems([]);
+ setOpen(true);
onSearch && onSearch("");
+ return;
}
setOpen(true);
// Si pas encore cherché et query vide, charger suggestions populaires
@@ -261,12 +273,25 @@ export default function SmartSearchBar({
key={it.id}
className={"sw-search__row sw-search__item" + (idx === active ? " is-active" : "")}
onMouseEnter={() => setActive(idx)}
- onClick={() => pick(it, "click")}
+ onMouseDown={(e) => {
+ e.preventDefault();
+ pick(it, "click");
+ }}
+ onClick={(e) => {
+ e.preventDefault();
+ }}
>
{truncateText(it.title || "Untitled", 140)}
{truncateText((it.kind ? it.kind : "result") + (it.subtitle ? " • " + it.subtitle : ""), 120)}
+ {Array.isArray(it.tags) && it.tags.length > 0 && (
+
+ {it.tags.slice(0, 4).map((tag) => (
+ #{tag}
+ ))}
+
+ )}
))}
diff --git a/src/services/recoSearch.js b/src/services/recoSearch.js
index a478813..0515718 100644
--- a/src/services/recoSearch.js
+++ b/src/services/recoSearch.js
@@ -42,6 +42,12 @@ function normalizeOne(r) {
const type = (r.type ?? r.kind ?? r.entity ?? "").toString().toLowerCase().trim();
const kind = type || (coords ? "place" : "item");
+ const tags =
+ Array.isArray(r.event_tags)
+ ? r.event_tags
+ : Array.isArray(r.tags)
+ ? r.tags
+ : [];
return {
id: (r.id ?? r._id ?? r.key ?? title).toString(),
@@ -52,6 +58,7 @@ function normalizeOne(r) {
coords,
lat,
lon,
+ tags,
raw: r,
};
}
diff --git a/src/styles/filters.css b/src/styles/filters.css
index 82a02cb..75ef0d4 100644
--- a/src/styles/filters.css
+++ b/src/styles/filters.css
@@ -16,6 +16,11 @@
.sw-icon-active .sw-icon-circle { background: rgba(37, 99, 235, 0.82); border-color:#60a5fa; box-shadow:0 0 14px rgba(56,189,248,.95); }
.sw-icon-active .sw-icon-label { color:#bfdbfe; font-weight:600; }
+/* Right-side filter stack alignment */
+.map-overlay-right .sw-icon-column { align-items:flex-end; }
+.map-overlay-right .sw-icon-btn { align-items:flex-end; }
+.map-overlay-right .sw-icon-label { text-align:right; }
+
.sw-bottom-row {
display:flex;
gap:.35rem;
diff --git a/src/styles/island.css b/src/styles/island.css
index 24225da..1fbfa06 100644
--- a/src/styles/island.css
+++ b/src/styles/island.css
@@ -53,6 +53,8 @@
position: relative;
width: 92%;
max-width: 1300px;
+ max-height: 92vh;
+ overflow-y: auto;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
border-radius: 24px;
padding: 2.5rem;
@@ -64,6 +66,19 @@
border: 1px solid rgba(56, 189, 248, 0.2);
}
+.island-viewer-container::-webkit-scrollbar {
+ width: 10px;
+}
+.island-viewer-container::-webkit-scrollbar-thumb {
+ background: rgba(56, 189, 248, 0.35);
+ border-radius: 999px;
+ border: 2px solid rgba(15, 23, 42, 0.9);
+}
+.island-viewer-container::-webkit-scrollbar-track {
+ background: rgba(15, 23, 42, 0.5);
+ border-radius: 999px;
+}
+
body[data-theme="blue"] .island-viewer-modal{
background: linear-gradient(135deg, rgba(3, 12, 40, 0.95) 0%, rgba(2, 8, 28, 0.98) 100%);
}
@@ -204,7 +219,10 @@ body[data-theme="blue"] .island-viewer-container{
padding: 0.75rem 1rem;
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.2);
- background: rgba(15, 23, 42, 0.6);
+ background: linear-gradient(135deg, rgba(30, 58, 138, 0.35), rgba(15, 23, 42, 0.7));
+ box-shadow:
+ 0 10px 30px rgba(0, 0, 0, 0.35),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
margin-bottom: 1.5rem;
}
@@ -284,10 +302,18 @@ body[data-theme="blue"] .island-viewer-container{
min-width: 0;
}
.island-profile-name {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
font-size: 0.95rem;
font-weight: 800;
color: #e2e8f0;
margin-bottom: 0.35rem;
+ padding: 0.2rem 0.65rem;
+ border-radius: 999px;
+ background: rgba(2, 6, 23, 0.5);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.island-profile-actions {
display: flex;
@@ -311,6 +337,15 @@ body[data-theme="blue"] .island-viewer-container{
font-size: 0.75rem;
color: #cbd5f5;
}
+.island-profile-inline {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+.island-profile-inline .island-profile-input {
+ flex: 1;
+}
.island-profile-row select {
flex: 1;
background: rgba(15, 23, 42, 0.6);
diff --git a/src/styles/map.css b/src/styles/map.css
index 2c5bd63..5bd12a7 100644
--- a/src/styles/map.css
+++ b/src/styles/map.css
@@ -83,7 +83,7 @@
/* Left / right: filtres */
.map-overlay-left{
left:.7rem;
- top:44%;
+ top:50%;
transform:translateY(-50%);
display:flex;
flex-direction:column;
@@ -91,13 +91,14 @@
}
.map-overlay-right{
right:.7rem;
- top:44%;
+ top:50%;
transform:translateY(-50%);
display:flex;
flex-direction:column;
gap:.5rem;
}
+
/* BOTTOM: sub-cats au-dessus du Sociowall */
.map-overlay-bottom{
bottom: calc(3.2rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom) + var(--sw-subcats-nudge, 0px));
@@ -230,6 +231,42 @@
gap:.5rem;
margin-bottom:.4rem;
}
+.create-link-row{
+ display:flex;
+ align-items:center;
+ gap:.5rem;
+ margin-bottom:.35rem;
+}
+.create-link-input{
+ flex:1;
+ margin-bottom:0;
+}
+.create-link-btn{
+ border-radius:999px;
+ border:1px solid rgba(148, 163, 184, 0.4);
+ background:#0f172a;
+ color:#e2e8f0;
+ padding:.35rem .7rem;
+ font-size:.7rem;
+ cursor:pointer;
+ white-space:nowrap;
+}
+.create-link-btn:disabled{
+ opacity:.6;
+ cursor:not-allowed;
+}
+.create-link-meta{
+ display:flex;
+ gap:.6rem;
+ font-size:.68rem;
+ color:#94a3b8;
+ margin-bottom:.35rem;
+}
+.create-link-error{
+ font-size:.68rem;
+ color:#f87171;
+ margin-bottom:.35rem;
+}
.crosshair-label{
font-size:.75rem;
color:#cbd5f5;
@@ -286,14 +323,69 @@
gap:.4rem;
margin-top:.35rem;
}
+.create-image-choice{
+ display:flex;
+ gap:.5rem;
+ align-items:center;
+ margin:.25rem 0 .35rem;
+}
+.create-image-choice-btn{
+ flex:1;
+ padding:.4rem .6rem;
+ border-radius:999px;
+ border:1px solid rgba(148, 163, 184, 0.35);
+ background:rgba(15, 23, 42, 0.6);
+ color:#e2e8f0;
+ font-size:.72rem;
+ font-weight:700;
+ cursor:pointer;
+}
+.create-image-choice-btn.is-active{
+ border-color: rgba(56, 189, 248, 0.7);
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
+}
+.create-image-placeholder{
+ display:flex;
+ align-items:center;
+ gap:.6rem;
+ padding:.6rem;
+ border-radius:12px;
+ border:1px dashed rgba(148, 163, 184, 0.4);
+ background:rgba(15, 23, 42, 0.45);
+ margin:.35rem 0 .65rem;
+}
+.create-image-placeholder-title{
+ font-size:.8rem;
+ font-weight:800;
+ color:#e2e8f0;
+}
+.create-image-placeholder-sub{
+ font-size:.7rem;
+ color:#94a3b8;
+}
.create-image-row{
display:flex;
gap:.5rem;
align-items:center;
}
-.create-image-row input[type="file"]{
- flex:1;
+.create-file-input{
+ display:none;
+}
+.create-file-btn{
+ display:inline-flex;
+ align-items:center;
+ gap:.4rem;
+ padding:.35rem .7rem;
+ border-radius:999px;
+ border:1px solid rgba(148, 163, 184, 0.4);
color:#e2e8f0;
+ font-size:.7rem;
+ background:rgba(15, 23, 42, 0.8);
+ cursor:pointer;
+}
+.create-file-btn--camera{
+ border-color: rgba(56, 189, 248, 0.65);
+ color:#e0f2fe;
}
.create-uploading{
font-size:.7rem;
@@ -319,6 +411,30 @@
border-radius:999px;
font-size:.68rem;
}
+.create-link-preview{
+ display:flex;
+ align-items:center;
+ gap:.65rem;
+ padding:.45rem .6rem;
+ border-radius:12px;
+ border:1px solid rgba(148, 163, 184, 0.35);
+ background:rgba(15, 23, 42, 0.55);
+ margin-bottom:.6rem;
+}
+.create-link-preview img{
+ width:58px;
+ height:58px;
+ border-radius:10px;
+ object-fit:cover;
+ border:1px solid rgba(148, 163, 184, 0.5);
+}
+.create-link-preview-meta{
+ display:flex;
+ flex-direction:column;
+ gap:.2rem;
+ font-size:.7rem;
+ color:#cbd5f5;
+}
.create-muted{
font-size:.7rem;
color:#94a3b8;
@@ -442,7 +558,8 @@ body[data-theme="blue"] .create-post-panel{
body[data-theme="blue"] .create-row select,
body[data-theme="blue"] .create-input,
body[data-theme="blue"] .create-textarea,
-body[data-theme="blue"] .create-privacy-row select{
+body[data-theme="blue"] .create-privacy-row select,
+body[data-theme="blue"] .create-link-btn{
background: #0b1b3b;
border-color: rgba(96, 165, 250, 0.5);
color:#e2e8f0;
@@ -476,7 +593,8 @@ body[data-theme="light"] .create-post-panel{
body[data-theme="light"] .create-row select,
body[data-theme="light"] .create-input,
body[data-theme="light"] .create-textarea,
-body[data-theme="light"] .create-privacy-row select{
+body[data-theme="light"] .create-privacy-row select,
+body[data-theme="light"] .create-link-btn{
background:#ffffff;
border-color: rgba(148, 163, 184, 0.7);
color:#0f172a;
diff --git a/src/styles/mapMarkers.css b/src/styles/mapMarkers.css
index 3a097bd..79f31db 100644
--- a/src/styles/mapMarkers.css
+++ b/src/styles/mapMarkers.css
@@ -607,7 +607,7 @@ body[data-theme="light"] .sw-chat-bubble::before{
}
.sw-modal-hero{
- width:100%;
+ width:auto;
height: 180px;
min-height: 180px;
flex: 1 1 auto;
@@ -991,8 +991,15 @@ body[data-theme="light"] .sw-modal-hero-fallback{
align-items: stretch;
}
.sw-truth-rail{
- width: 12px;
- min-width: 12px;
+ width: 22px;
+ min-width: 22px;
+ height: 140px;
+ min-height: 140px;
+ margin: 0 8px 0 0;
+ align-self: flex-start;
+ }
+ .sw-cluster-hero{
+ grid-template-columns: 28px 1fr;
}
}
@@ -1080,15 +1087,145 @@ body[data-theme="light"] .sw-modal-x{
.post-pin.sw-appear{
opacity: 0;
- transform: translate3d(0,0,0) scale(0.96);
- transition: opacity 840ms ease, transform 840ms ease;
+ transition: opacity 640ms ease;
}
.post-pin.sw-appear.sw-appear-in{
opacity: 1;
- transform: translate3d(0,0,0) scale(1);
}
.post-pin.sw-disappear{
opacity: 0;
- transform: translate3d(0,0,0) scale(0.96);
- transition: opacity 840ms ease, transform 840ms ease;
+ transition: opacity 640ms ease;
+}
+.post-pin--simple.sw-appear{
+ opacity: 0;
+ transition: opacity 520ms ease;
+}
+.post-pin--simple.sw-appear.sw-appear-in{
+ opacity: 1;
+}
+.post-pin--simple.sw-disappear{
+ opacity: 0;
+ transition: opacity 520ms ease;
+}
+
+/* Cluster main news layout */
+.sw-cluster-modal .sw-modal-toprow{
+ margin-bottom: 6px;
+}
+.sw-cluster-title{
+ font-size: 22px;
+ font-weight: 800;
+ margin: 4px 0 14px;
+ color: var(--sw-modal-text, #e5e7eb);
+}
+.sw-modal-tags{
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: -6px 0 14px;
+}
+.sw-modal-tag{
+ font-size: 11px;
+ text-transform: lowercase;
+ padding: 3px 8px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.12);
+ color: var(--sw-modal-text, #e5e7eb);
+ letter-spacing: 0.2px;
+}
+.sw-cluster-body{
+ display: grid;
+ grid-template-columns: minmax(220px, 44%) minmax(240px, 56%);
+ gap: 16px;
+ align-items: start;
+}
+.sw-cluster-hero{
+ display: grid;
+ grid-template-columns: 22px minmax(0,1fr);
+ grid-auto-flow: column;
+ gap: 10px;
+ align-items: stretch;
+}
+.sw-cluster-hero .sw-truth-rail{
+ grid-row: 1;
+ align-self: flex-start;
+}
+.sw-cluster-hero-media{
+ border-radius: 18px;
+ overflow: hidden;
+ background: radial-gradient(circle at 30% 30%, var(--sw-modal-accent-soft, rgba(56,189,248,0.18)), rgba(3,14,32,0.98));
+ box-shadow: 0 10px 24px rgba(0,0,0,0.35), 0 0 18px var(--sw-modal-accent-glow, rgba(56,189,248,0.45));
+ min-height: 200px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.sw-cluster-hero-media img{
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.sw-cluster-actions{
+ margin-top: 10px;
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+.sw-cluster-related-title{
+ font-size: 13px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--sw-modal-meta, #9eb7ff);
+ margin-bottom: 8px;
+}
+.sw-cluster-related-list{
+ display: grid;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+.sw-cluster-source-item{
+ padding: 8px 10px;
+ border-radius: 12px;
+ background: rgba(15,23,42,0.5);
+ border: 1px solid rgba(148,163,184,0.2);
+ cursor: pointer;
+}
+.sw-cluster-source-item:hover{
+ border-color: var(--sw-modal-accent, rgba(56,189,248,0.6));
+ box-shadow: 0 0 0 1px var(--sw-modal-accent-soft, rgba(56,189,248,0.2));
+}
+.sw-cluster-source-title{
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--sw-modal-text, #e5e7eb);
+}
+.sw-cluster-source{
+ font-size: 12px;
+ color: var(--sw-modal-meta, #9eb7ff);
+}
+.sw-cluster-source-empty{
+ font-size: 13px;
+ color: var(--sw-modal-meta, #9eb7ff);
+}
+.sw-cluster-generated{
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--sw-modal-meta, #9eb7ff);
+ margin: 6px 0 6px;
+}
+.sw-cluster-summary{
+ font-size: 15px;
+ line-height: 1.5;
+ color: var(--sw-modal-text, #e5e7eb);
+}
+
+@media (max-width: 860px){
+ .sw-cluster-body{
+ grid-template-columns: 1fr;
+ }
+ .sw-cluster-hero{
+ grid-template-columns: 28px 1fr;
+ }
}
diff --git a/src/styles/smartSearchBar.css b/src/styles/smartSearchBar.css
index 0216e3a..e7b1220 100644
--- a/src/styles/smartSearchBar.css
+++ b/src/styles/smartSearchBar.css
@@ -96,6 +96,11 @@
color: var(--sw-text, rgba(255,255,255,0.92));
}
+.sw-search__iconBtn.is-active {
+ background: linear-gradient(135deg, rgba(56,189,248,0.9), rgba(34,211,238,0.9));
+ color: #0b1220;
+}
+
.sw-search__iconBtn:active {
transform: translateY(1px);
}
@@ -144,6 +149,20 @@
color: var(--sw-muted, rgba(255,255,255,0.60));
font-size: 12px;
}
+.sw-search__tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 6px;
+}
+.sw-search__tag {
+ font-size: 11px;
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.12);
+ color: rgba(255,255,255,0.88);
+ letter-spacing: 0.2px;
+}
.sw-search__muted {
color: var(--sw-muted, rgba(255,255,255,0.60));