nleh
This commit is contained in:
parent
4753cacdcc
commit
d7f12db601
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
import "../../styles/mapMarkers.css";
|
import "../../styles/mapMarkers.css";
|
||||||
|
|
@ -34,6 +34,9 @@ export default function MapView({
|
||||||
const [timeFilter, setTimeFilter] = useState("RECENT");
|
const [timeFilter, setTimeFilter] = useState("RECENT");
|
||||||
const [subFilter, setSubFilter] = useState("All");
|
const [subFilter, setSubFilter] = useState("All");
|
||||||
|
|
||||||
|
const stageRef = useRef(null);
|
||||||
|
const [showSubcats, setShowSubcats] = useState(true);
|
||||||
|
|
||||||
const userPosition = useUserPosition(mapRef, hasLastView);
|
const userPosition = useUserPosition(mapRef, hasLastView);
|
||||||
|
|
||||||
const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
|
const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
|
||||||
|
|
@ -89,6 +92,23 @@ export default function MapView({
|
||||||
}, [selectedPost, mapRef]);
|
}, [selectedPost, mapRef]);
|
||||||
|
|
||||||
// Websocket for new posts
|
// Websocket for new posts
|
||||||
|
useEffect(() => {
|
||||||
|
const el = stageRef.current;
|
||||||
|
if (!el || typeof IntersectionObserver === "undefined") return;
|
||||||
|
|
||||||
|
const obs = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const e = entries && entries[0];
|
||||||
|
const ok = !!(e && e.isIntersecting && e.intersectionRatio > 0.15);
|
||||||
|
setShowSubcats(ok);
|
||||||
|
},
|
||||||
|
{ threshold: [0, 0.15, 0.3, 0.6] }
|
||||||
|
);
|
||||||
|
|
||||||
|
obs.observe(el);
|
||||||
|
return () => obs.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
const host =
|
const host =
|
||||||
|
|
@ -224,7 +244,7 @@ export default function MapView({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-page">
|
<div className="map-page">
|
||||||
<div className="map-stage">
|
<div className="map-stage" ref={stageRef}>
|
||||||
<div ref={containerRef} className="map-container" />
|
<div ref={containerRef} className="map-container" />
|
||||||
{status && <div className="map-status">{status}</div>}
|
{status && <div className="map-status">{status}</div>}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,40 @@ function mountCard(container, spec, data, theme) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the marker anchor to be displayed at:
|
||||||
|
* - X: centered (50% width)
|
||||||
|
* - Y: 6/7 of map height (so the popup sits "1/7 from bottom")
|
||||||
|
*/
|
||||||
|
function panMarkerToPreferredAnchor(map, lngLat) {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const el = map.getContainer();
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const targetX = rect.width * 0.5;
|
||||||
|
const targetY = rect.height * (6 / 7); // 1/7 from bottom => anchor at 6/7 height
|
||||||
|
|
||||||
|
const p = map.project(lngLat);
|
||||||
|
const dx = targetX - p.x;
|
||||||
|
const dy = targetY - p.y;
|
||||||
|
|
||||||
|
// tiny deltas -> skip
|
||||||
|
if (Math.abs(dx) < 2 && Math.abs(dy) < 2) return;
|
||||||
|
|
||||||
|
// lock close/fetch/rebuild while panning
|
||||||
|
try {
|
||||||
|
const until = Date.now() + 2000;
|
||||||
|
map.__swIgnoreMapClickUntil = until;
|
||||||
|
map.__swIgnoreCloseUntil = until;
|
||||||
|
map.__swIgnoreFetchUntil = until;
|
||||||
|
map.__swIgnoreRebuildUntil = until;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
map.panBy([dx, dy], { duration: 650 });
|
||||||
|
}
|
||||||
|
|
||||||
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
|
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
@ -118,6 +152,8 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lngLat = [lon, lat];
|
||||||
|
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
root.className = "post-pin post-pin--compact";
|
root.className = "post-pin post-pin--compact";
|
||||||
root.style.zIndex = "1";
|
root.style.zIndex = "1";
|
||||||
|
|
@ -134,7 +170,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
||||||
root.className = "post-pin post-pin--compact";
|
root.className = "post-pin post-pin--compact";
|
||||||
root.style.zIndex = "1";
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
// pointer stays: .post-pin-pointer-small (UNCHANGED)
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="sw-template-mini-wrap"></div>
|
<div class="sw-template-mini-wrap"></div>
|
||||||
<div class="post-pin-pointer-small"></div>
|
<div class="post-pin-pointer-small"></div>
|
||||||
|
|
@ -155,7 +190,18 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
||||||
root.className = "post-pin post-pin--expanded";
|
root.className = "post-pin post-pin--expanded";
|
||||||
root.style.zIndex = "999999";
|
root.style.zIndex = "999999";
|
||||||
|
|
||||||
// pointer stays: .post-card-pointer (UNCHANGED)
|
// 2000ms locks so recenter/fetch/rebuild does not close this expanded popup
|
||||||
|
try {
|
||||||
|
const m = mapRef.current;
|
||||||
|
if (m) {
|
||||||
|
const until = Date.now() + 2000;
|
||||||
|
m.__swIgnoreMapClickUntil = until;
|
||||||
|
m.__swIgnoreCloseUntil = until;
|
||||||
|
m.__swIgnoreFetchUntil = until;
|
||||||
|
m.__swIgnoreRebuildUntil = until;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const headline = escapeHtml(post?.title || "Untitled");
|
const headline = escapeHtml(post?.title || "Untitled");
|
||||||
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
|
|
@ -212,8 +258,23 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
||||||
root.__swUnmount = mountCard(wrap, spec, data, theme);
|
root.__swUnmount = mountCard(wrap, spec, data, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After render, pan so marker sits at the preferred anchor (1/7 from bottom)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
const m = mapRef.current;
|
||||||
|
if (!m) return;
|
||||||
|
|
||||||
|
// 1) put the marker anchor exactly where we want
|
||||||
|
panMarkerToPreferredAnchor(m, lngLat);
|
||||||
|
|
||||||
|
// 2) re-occlude after the pan finishes
|
||||||
|
m.once("moveend", () => {
|
||||||
|
try { applyOcclusionForExpanded(markersRef, root); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// also occlude right away
|
||||||
applyOcclusionForExpanded(markersRef, root);
|
applyOcclusionForExpanded(markersRef, root);
|
||||||
|
} catch {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +282,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
||||||
root.__renderCompact = renderCompact;
|
root.__renderCompact = renderCompact;
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
|
const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
|
||||||
.setLngLat([lon, lat])
|
.setLngLat(lngLat)
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
markersRef.current.push({ id: post.id, marker, el: root, post });
|
markersRef.current.push({ id: post.id, marker, el: root, post });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import CardRenderer from "../Cards/CardRenderer";
|
||||||
|
import { cardTokens } from "../../theme/cardTokens";
|
||||||
|
import { getTemplateSpecForPost, adaptPostToTemplateData } from "./templateSpecs";
|
||||||
|
|
||||||
|
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||||
|
if (markersRef.current) {
|
||||||
|
for (const m of markersRef.current) {
|
||||||
|
try {
|
||||||
|
if (m?.el && m.el.__swUnmount) m.el.__swUnmount();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
m.marker.remove();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markersRef.current = [];
|
||||||
|
if (expandedElRef) expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectsOverlap(a, b) {
|
||||||
|
return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOcclusionForExpanded(markersRef, expandedEl) {
|
||||||
|
if (!expandedEl) return;
|
||||||
|
|
||||||
|
const expandedCard = expandedEl.querySelector(".post-card");
|
||||||
|
if (!expandedCard) return;
|
||||||
|
|
||||||
|
const PAD_X = 26;
|
||||||
|
const PAD_TOP = 20;
|
||||||
|
const PAD_BOTTOM = 140;
|
||||||
|
|
||||||
|
const raw = expandedCard.getBoundingClientRect();
|
||||||
|
const expandedRect = {
|
||||||
|
left: raw.left - PAD_X,
|
||||||
|
right: raw.right + PAD_X,
|
||||||
|
top: raw.top - PAD_TOP,
|
||||||
|
bottom: raw.bottom + PAD_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
const list = markersRef.current || [];
|
||||||
|
for (const item of list) {
|
||||||
|
const el = item?.el;
|
||||||
|
if (!el || el === expandedEl) continue;
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const isBelow = r.top >= raw.top;
|
||||||
|
if (!isBelow) {
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
el.style.pointerEvents = "auto";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hide = rectsOverlap(expandedRect, r);
|
||||||
|
el.style.visibility = hide ? "hidden" : "visible";
|
||||||
|
el.style.pointerEvents = hide ? "none" : "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearOcclusion(markersRef) {
|
||||||
|
const list = markersRef.current || [];
|
||||||
|
for (const item of list) {
|
||||||
|
const el = item?.el;
|
||||||
|
if (!el) continue;
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
el.style.pointerEvents = "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountCard(container, spec, data, theme) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
const tokens = cardTokens?.[theme] || cardTokens.blue;
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
React.createElement(CardRenderer, {
|
||||||
|
spec,
|
||||||
|
data,
|
||||||
|
themeTokens: tokens,
|
||||||
|
onAction: (action, value) => {
|
||||||
|
if (action?.type === "open_url" && value) {
|
||||||
|
try { window.open(value, "_blank"); } catch {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
className: "",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
try { root.unmount(); } catch {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put clicked coordinate at ~6/7 of map height (=> 1/7 from bottom)
|
||||||
|
function panMapSoPointHitsTarget(map, lng, lat) {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// ✅ ignore map click-close while we animate
|
||||||
|
map.__swIgnoreMapClickUntil = Date.now() + 2200;
|
||||||
|
|
||||||
|
const c = map.getContainer();
|
||||||
|
const w = c.clientWidth || 0;
|
||||||
|
const h = c.clientHeight || 0;
|
||||||
|
if (!w || !h) return;
|
||||||
|
|
||||||
|
const mobile = (typeof window !== "undefined" && window.innerWidth <= 640);
|
||||||
|
|
||||||
|
const targetX = w * 0.50;
|
||||||
|
const targetY = mobile ? h * (6 / 7) : h * 0.80;
|
||||||
|
|
||||||
|
const p = map.project([lng, lat]);
|
||||||
|
const dx = p.x - targetX;
|
||||||
|
const dy = p.y - targetY;
|
||||||
|
|
||||||
|
const centerPx = { x: w / 2, y: h / 2 };
|
||||||
|
const newCenter = map.unproject([centerPx.x + dx, centerPx.y + dy]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
map.easeTo({
|
||||||
|
center: newCenter,
|
||||||
|
duration: 420,
|
||||||
|
easing: (t) => t * (2 - t),
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const lat =
|
||||||
|
typeof post.lat === "number"
|
||||||
|
? post.lat
|
||||||
|
: typeof post.latitude === "number"
|
||||||
|
? post.latitude
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const lon =
|
||||||
|
typeof post.lon === "number"
|
||||||
|
? post.lon
|
||||||
|
: typeof post.lng === "number"
|
||||||
|
? post.lng
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (lat == null || lon == null) {
|
||||||
|
console.warn("post sans coordonnée:", post);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
|
function unmountIfAny() {
|
||||||
|
if (root.__swUnmount) {
|
||||||
|
try { root.__swUnmount(); } catch {}
|
||||||
|
root.__swUnmount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCompact() {
|
||||||
|
unmountIfAny();
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="sw-template-mini-wrap"></div>
|
||||||
|
<div class="post-pin-pointer-small"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const wrap = root.querySelector(".sw-template-mini-wrap");
|
||||||
|
if (wrap) {
|
||||||
|
const spec = getTemplateSpecForPost(post, "mini");
|
||||||
|
const data = adaptPostToTemplateData(post);
|
||||||
|
root.__swUnmount = mountCard(wrap, spec, data, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearOcclusion(markersRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExpanded() {
|
||||||
|
unmountIfAny();
|
||||||
|
root.className = "post-pin post-pin--expanded";
|
||||||
|
root.style.zIndex = "999999";
|
||||||
|
|
||||||
|
const headline = escapeHtml(post?.title || "Untitled");
|
||||||
|
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="post-card sw-expanded-shell">
|
||||||
|
<div class="sw-expanded-top">
|
||||||
|
<div class="sw-expanded-left">
|
||||||
|
<div class="sw-template-full-wrap"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sw-expanded-right">
|
||||||
|
<div class="sw-watch-title">Watching</div>
|
||||||
|
<div class="sw-watch-list">
|
||||||
|
<div class="sw-watch-item">☑ Julia ★<div class="sw-watch-msg">— I see it…</div></div>
|
||||||
|
<div class="sw-watch-item">☑ Kim / CNN<div class="sw-watch-msg">— Blah blah blah</div></div>
|
||||||
|
<div class="sw-watch-item">☑ Yan ★<div class="sw-watch-msg">— Watch this!!</div></div>
|
||||||
|
</div>
|
||||||
|
<button class="sw-add-feed-btn" type="button">ADD TO FEED</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sw-news-generated">
|
||||||
|
<div class="sw-news-title">${headline}</div>
|
||||||
|
<div class="sw-news-sub">The news here (generated)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card-footer sw-actions-row">
|
||||||
|
<button class="post-card-btn">Contact</button>
|
||||||
|
<button class="post-card-btn">Like</button>
|
||||||
|
<button class="post-card-btn">Share</button>
|
||||||
|
<button class="post-card-btn">Fix</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card-comments sw-livechat">
|
||||||
|
<div class="sw-livechat-title">Live chat / comments</div>
|
||||||
|
<div class="sw-livechat-body">
|
||||||
|
<div class="sw-chat-line">— …</div>
|
||||||
|
<div class="sw-chat-line">— what the f***?</div>
|
||||||
|
<div class="sw-chat-line">— nice…</div>
|
||||||
|
<div class="sw-chat-line">— my eyes!!</div>
|
||||||
|
</div>
|
||||||
|
<div class="sw-livechat-inputrow">
|
||||||
|
<input class="sw-livechat-input" placeholder="Write a comment…" />
|
||||||
|
<button class="sw-livechat-post" type="button">POST</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-card-pointer"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const wrap = root.querySelector(".sw-template-full-wrap");
|
||||||
|
if (wrap) {
|
||||||
|
const spec = getTemplateSpecForPost(post, "full");
|
||||||
|
const data = adaptPostToTemplateData(post);
|
||||||
|
root.__swUnmount = mountCard(wrap, spec, data, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
panMapSoPointHitsTarget(mapRef.current, lon, lat);
|
||||||
|
applyOcclusionForExpanded(markersRef, root);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompact();
|
||||||
|
root.__renderCompact = renderCompact;
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
|
||||||
|
.setLngLat([lon, lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
markersRef.current.push({ id: post.id, marker, el: root, post });
|
||||||
|
|
||||||
|
root.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
// ✅ ignore the map click-close that often fires after marker click on mobile
|
||||||
|
const m = mapRef.current;
|
||||||
|
if (m) m.__swIgnoreMapClickUntil = Date.now() + 2000;
|
||||||
|
|
||||||
|
if (expandedElRef.current && expandedElRef.current !== root) {
|
||||||
|
if (expandedElRef.current.__renderCompact) expandedElRef.current.__renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = root.classList.contains("post-pin--expanded");
|
||||||
|
if (isExpanded) {
|
||||||
|
renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
eval { my = q{}; };
|
||||||
|
} while(0);
|
||||||
|
do {
|
||||||
|
try { const map = mapRef.current; if (map) { map.__swIgnoreCloseUntil = Date.now() + 2000; map.__swIgnoreMapClickUntil = Date.now() + 2000; } } catch {}
|
||||||
|
} while(0);
|
||||||
|
renderExpanded();
|
||||||
|
expandedElRef.current = root;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str ?? "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
@ -1,58 +1,34 @@
|
||||||
/**
|
/**
|
||||||
* Frontend-only template specs (no DB changes).
|
* Frontend-only template specs.
|
||||||
* Keep pointers as-is:
|
* Pointers stay as-is:
|
||||||
* - compact triangle: .post-pin-pointer-small
|
* - compact triangle: .post-pin-pointer-small
|
||||||
* - expanded triangle: .post-card-pointer
|
* - expanded triangle: .post-card-pointer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TEMPLATE_SPECS = {
|
export const TEMPLATE_SPECS = {
|
||||||
news: {
|
news: {
|
||||||
mini_spec: {
|
mini_spec: {
|
||||||
size: { w: 280, h: 110 },
|
size: { w: 240, h: 96 }, /* ✅ smaller to avoid layout push */
|
||||||
background: { type: "gradient", value: "newsBlue" },
|
background: { type: "gradient", value: "newsBlue" },
|
||||||
radius: 18,
|
radius: 18,
|
||||||
layers: [
|
layers: [
|
||||||
{ id: "badge", type: "chip", x: 14, y: 12, text: "NEWS", style: "chip.news" },
|
{ id: "badge", type: "chip", x: 12, y: 10, text: "NEWS", style: "chip.news" },
|
||||||
{ id: "title", type: "text", x: 14, y: 40, w: 252, h: 44, bind: "data.headline", style: "text.title", maxLines: 2 },
|
{ id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
|
||||||
{ id: "meta", type: "text", x: 14, y: 88, w: 252, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
|
{ id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Desktop/tablet full */
|
|
||||||
full_spec: {
|
full_spec: {
|
||||||
size: { w: 360, h: 480 },
|
size: { w: 360, h: 520 },
|
||||||
background: { type: "solid", value: "surface" },
|
background: { type: "solid", value: "surface" },
|
||||||
radius: 22,
|
radius: 22,
|
||||||
layers: [
|
layers: [
|
||||||
{ id: "hero", type: "image", x: 0, y: 0, w: 360, h: 180, bind: "data.image" },
|
{ id: "hero", type: "image", x: 0, y: 0, w: 360, h: 220, bind: "data.image" },
|
||||||
{ id: "badge", type: "chip", x: 16, y: 16, text: "NEWS", style: "chip.news" },
|
{ id: "badge", type: "chip", x: 16, y: 16, text: "NEWS", style: "chip.news" },
|
||||||
{ id: "title", type: "text", x: 16, y: 200, w: 328, h: 70, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
{ id: "title", type: "text", x: 16, y: 240, w: 328, h: 78, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
||||||
{ id: "summary", type: "text", x: 16, y: 272, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 4 },
|
{ id: "summary", type: "text", x: 16, y: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 },
|
||||||
{
|
{
|
||||||
id: "cta",
|
id: "cta",
|
||||||
type: "button",
|
type: "button",
|
||||||
x: 16, y: 402, w: 328, h: 48,
|
x: 16, y: 450, w: 328, h: 48,
|
||||||
text: "Open source",
|
|
||||||
style: "button.primary",
|
|
||||||
action: { type: "open_url", bind: "data.url" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ✅ Mobile full (shorter) */
|
|
||||||
full_spec_mobile: {
|
|
||||||
size: { w: 320, h: 380 },
|
|
||||||
background: { type: "solid", value: "surface" },
|
|
||||||
radius: 20,
|
|
||||||
layers: [
|
|
||||||
{ id: "hero", type: "image", x: 0, y: 0, w: 320, h: 120, bind: "data.image" },
|
|
||||||
{ id: "badge", type: "chip", x: 14, y: 12, text: "NEWS", style: "chip.news" },
|
|
||||||
{ id: "title", type: "text", x: 14, y: 140, w: 292, h: 58, bind: "data.headline", style: "text.h1", maxLines: 2 },
|
|
||||||
{ id: "summary", type: "text", x: 14, y: 202, w: 292, h: 80, bind: "data.summary", style: "text.body", maxLines: 3 },
|
|
||||||
{
|
|
||||||
id: "cta",
|
|
||||||
type: "button",
|
|
||||||
x: 14, y: 300, w: 292, h: 46,
|
|
||||||
text: "Open source",
|
text: "Open source",
|
||||||
style: "button.primary",
|
style: "button.primary",
|
||||||
action: { type: "open_url", bind: "data.url" }
|
action: { type: "open_url", bind: "data.url" }
|
||||||
|
|
@ -78,17 +54,7 @@ export function getTemplateKeyForPost(post) {
|
||||||
export function getTemplateSpecForPost(post, variant /* "mini"|"full" */) {
|
export function getTemplateSpecForPost(post, variant /* "mini"|"full" */) {
|
||||||
const key = getTemplateKeyForPost(post);
|
const key = getTemplateKeyForPost(post);
|
||||||
const t = TEMPLATE_SPECS[key] || TEMPLATE_SPECS.news;
|
const t = TEMPLATE_SPECS[key] || TEMPLATE_SPECS.news;
|
||||||
|
return variant === "full" ? t.full_spec : t.mini_spec;
|
||||||
if (variant === "full") {
|
|
||||||
try {
|
|
||||||
if (typeof window !== "undefined" && window.innerWidth <= 640 && t.full_spec_mobile) {
|
|
||||||
return t.full_spec_mobile;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return t.full_spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.mini_spec;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adaptPostToTemplateData(post) {
|
export function adaptPostToTemplateData(post) {
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,22 @@ export function useMapCore(theme) {
|
||||||
map.on("move", reOcclude);
|
map.on("move", reOcclude);
|
||||||
map.on("zoom", reOcclude);
|
map.on("zoom", reOcclude);
|
||||||
|
|
||||||
map.on("click", () => closeExpandedIfAny());
|
// IMPORTANT: ignore close while we are auto-panning to keep popup visible
|
||||||
map.on("dragstart", () => closeExpandedIfAny());
|
map.on("click", () => {
|
||||||
|
try {
|
||||||
|
const until = map.__swIgnoreMapClickUntil || 0;
|
||||||
|
if (until && Date.now() < until) return;
|
||||||
|
} catch {}
|
||||||
|
closeExpandedIfAny();
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on("dragstart", () => {
|
||||||
|
try {
|
||||||
|
const until = map.__swIgnoreCloseUntil || 0;
|
||||||
|
if (until && Date.now() < until) return;
|
||||||
|
} catch {}
|
||||||
|
closeExpandedIfAny();
|
||||||
|
});
|
||||||
|
|
||||||
map.on("load", () => setViewParams(getViewFromMap(map)));
|
map.on("load", () => setViewParams(getViewFromMap(map)));
|
||||||
|
|
||||||
|
|
@ -98,7 +112,10 @@ export function useMapCore(theme) {
|
||||||
setViewParams(vp);
|
setViewParams(vp);
|
||||||
try {
|
try {
|
||||||
const center = vp.center;
|
const center = vp.center;
|
||||||
localStorage.setItem(LAST_VIEW_KEY, JSON.stringify({ lat: center[1], lon: center[0], zoom: map.getZoom() }));
|
localStorage.setItem(
|
||||||
|
LAST_VIEW_KEY,
|
||||||
|
JSON.stringify({ lat: center[1], lon: center[0], zoom: map.getZoom() })
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export function usePostsEngine({
|
||||||
}) {
|
}) {
|
||||||
const allPostsRef = useRef([]);
|
const allPostsRef = useRef([]);
|
||||||
const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
|
const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
|
||||||
|
const delayedFetchRef = useRef(null);
|
||||||
|
|
||||||
const [status, setStatus] = useState("Loading posts...");
|
const [status, setStatus] = useState("Loading posts...");
|
||||||
const [visiblePosts, setVisiblePosts] = useState([]);
|
const [visiblePosts, setVisiblePosts] = useState([]);
|
||||||
|
|
@ -26,6 +27,12 @@ export function usePostsEngine({
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
|
// If expanded is open and we are inside ignore window, do not rebuild (it closes the popup)
|
||||||
|
try {
|
||||||
|
const until = map.__swIgnoreRebuildUntil || 0;
|
||||||
|
if (expandedElRef?.current && until && Date.now() < until) return;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
clearAllMarkers(markersRef, expandedElRef);
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
|
||||||
const posts = allPostsRef.current || [];
|
const posts = allPostsRef.current || [];
|
||||||
|
|
@ -61,6 +68,19 @@ export function usePostsEngine({
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
const mapObj = mapRef.current;
|
||||||
|
|
||||||
|
// Delay fetch while popup is open (prevents marker clear/rebuild)
|
||||||
|
try {
|
||||||
|
const until = mapObj?.__swIgnoreFetchUntil || 0;
|
||||||
|
if (until && Date.now() < until) {
|
||||||
|
const wait = Math.min(4000, until - Date.now());
|
||||||
|
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
|
||||||
|
delayedFetchRef.current = setTimeout(load, wait + 25);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const [lng, lat] = viewParams.center;
|
const [lng, lat] = viewParams.center;
|
||||||
const radiusKm = viewParams.radiusKm || 750;
|
const radiusKm = viewParams.radiusKm || 750;
|
||||||
const filterKey = `${mainFilter}|${subFilter}`;
|
const filterKey = `${mainFilter}|${subFilter}`;
|
||||||
|
|
@ -101,11 +121,9 @@ export function usePostsEngine({
|
||||||
|
|
||||||
const existing = allPostsRef.current || [];
|
const existing = allPostsRef.current || [];
|
||||||
const byId = new Map();
|
const byId = new Map();
|
||||||
|
|
||||||
for (const p of existing) {
|
for (const p of existing) {
|
||||||
if (p && typeof p.id !== "undefined") byId.set(p.id, p);
|
if (p && typeof p.id !== "undefined") byId.set(p.id, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(newPosts)) {
|
if (Array.isArray(newPosts)) {
|
||||||
for (const p of newPosts) {
|
for (const p of newPosts) {
|
||||||
if (p && typeof p.id !== "undefined" && !byId.has(p.id)) byId.set(p.id, p);
|
if (p && typeof p.id !== "undefined" && !byId.has(p.id)) byId.set(p.id, p);
|
||||||
|
|
@ -113,7 +131,6 @@ export function usePostsEngine({
|
||||||
}
|
}
|
||||||
|
|
||||||
allPostsRef.current = Array.from(byId.values());
|
allPostsRef.current = Array.from(byId.values());
|
||||||
|
|
||||||
lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
|
lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
|
||||||
|
|
||||||
rebuildMarkersForFilters(timeFilter);
|
rebuildMarkersForFilters(timeFilter);
|
||||||
|
|
@ -131,6 +148,10 @@ export function usePostsEngine({
|
||||||
load();
|
load();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
if (delayedFetchRef.current) {
|
||||||
|
try { clearTimeout(delayedFetchRef.current); } catch {}
|
||||||
|
delayedFetchRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]);
|
}, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,22 @@
|
||||||
.sw-bottom-label { font-size:.65rem; color:#e5e7eb; text-shadow:0 1px 2px rgba(0,0,0,.9); }
|
.sw-bottom-label { font-size:.65rem; color:#e5e7eb; text-shadow:0 1px 2px rgba(0,0,0,.9); }
|
||||||
.sw-bottom-active .sw-bottom-circle { background: rgba(37, 99, 235, 0.82); border-color:#60a5fa; }
|
.sw-bottom-active .sw-bottom-circle { background: rgba(37, 99, 235, 0.82); border-color:#60a5fa; }
|
||||||
.sw-bottom-active .sw-bottom-label { color:#bfdbfe; font-weight:600; }
|
.sw-bottom-active .sw-bottom-label { color:#bfdbfe; font-weight:600; }
|
||||||
|
|
||||||
|
/* Floating subcategory dock (always above Sociowall, not coverable) */
|
||||||
|
.sw-subcats-float{
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: calc(14px + var(--sw-wall-overlap, 0px));
|
||||||
|
z-index: 300;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-subcats-float .sw-bottom-row{
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: min(980px, 96vw);
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-subcats-float .sw-bottom-row::-webkit-scrollbar{ height: 0; }
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
.app-root{ min-height:100vh; display:flex; flex-direction:column; }
|
.app-root{ min-height:100vh; display:flex; flex-direction:column; }
|
||||||
|
|
||||||
/* allow scrolling under the map */
|
/* allow full page scroll */
|
||||||
.main-shell{ flex: 0 0 auto; display:block; padding:0; }
|
.main-shell{ flex: 1 0 auto; display:block; padding:0; }
|
||||||
|
|
||||||
/* MAP HEIGHT (make map taller so Sociowall/Chat sit lower) */
|
/* DO NOT clip MapView (it contains map + below-stage) */
|
||||||
.map-shell{
|
.map-shell{
|
||||||
position:relative;
|
position:relative;
|
||||||
width:100%;
|
width:100%;
|
||||||
height:68vh;
|
height:auto;
|
||||||
border-radius:0;
|
overflow:visible;
|
||||||
overflow:hidden;
|
|
||||||
border:none;
|
border:none;
|
||||||
background:#020617;
|
background:transparent;
|
||||||
}
|
|
||||||
|
|
||||||
/* Phones: taller map => wall+chat lower */
|
|
||||||
@media (max-width: 640px){
|
|
||||||
.map-shell{ height:76vh; }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
.map-page {
|
:root{
|
||||||
display: flex;
|
--sw-below-overlap: 40px; /* wall overlaps map a bit */
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Map stage should fill the map-shell height (no competing vh/clamp here) */
|
.map-page{ display:flex; flex-direction:column; width:100%; }
|
||||||
.map-stage {
|
|
||||||
position: relative;
|
/* Map stage first */
|
||||||
width: 100%;
|
.map-stage{
|
||||||
height: 100%;
|
position:relative;
|
||||||
|
width:100%;
|
||||||
|
height: clamp(520px, 72vh, 860px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container,
|
/* Landscape: vh is small, reduce min height */
|
||||||
.maplibre-container {
|
@media (orientation: landscape){
|
||||||
position: absolute;
|
:root{ --sw-below-overlap: 22px; }
|
||||||
inset: 0;
|
.map-stage{ height: clamp(360px, 62vh, 720px); }
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-status {
|
.map-container, .maplibre-container{
|
||||||
position: absolute;
|
position:absolute; inset:0; width:100%; height:100%;
|
||||||
left: 0.5rem;
|
|
||||||
bottom: 0.5rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
padding: 0.2rem 0.4rem;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
z-index: 30;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Below map area */
|
.map-status{
|
||||||
.below-stage {
|
position:absolute; left:.5rem; bottom:.5rem;
|
||||||
padding: 0.25rem 0.7rem 1rem;
|
font-size:.7rem; padding:.2rem .4rem;
|
||||||
|
background:rgba(0,0,0,.6); border-radius:.5rem;
|
||||||
|
z-index:30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.below-row {
|
/* BELOW MAP (scroll area) – pulled up slightly over map */
|
||||||
display: flex;
|
.below-stage{
|
||||||
gap: 0.6rem;
|
margin-top: calc(var(--sw-below-overlap) * -1);
|
||||||
align-items: stretch;
|
position:relative;
|
||||||
|
z-index:25;
|
||||||
|
padding: .25rem .7rem 1.0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile: stack */
|
.below-row{
|
||||||
@media (max-width: 640px) {
|
display:flex;
|
||||||
.below-row {
|
gap:.6rem;
|
||||||
flex-direction: column;
|
align-items:stretch;
|
||||||
}
|
min-width:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keep side-by-side on most phones; stack only ultra-small */
|
||||||
|
@media (max-width: 420px){
|
||||||
|
.below-row{ flex-direction:column; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
.map-overlay { position:absolute; pointer-events:none; z-index:20; }
|
.map-overlay { position:absolute; pointer-events:none; z-index:40; }
|
||||||
.map-overlay * { pointer-events:auto; }
|
.map-overlay * { pointer-events:auto; }
|
||||||
|
|
||||||
.map-overlay-top {
|
.map-overlay-top {
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
display:flex; flex-direction:column; gap:.5rem;
|
display:flex; flex-direction:column; gap:.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-overlay-bottom { bottom: 3.2rem; left:50%; transform:translateX(-50%); display:flex; gap:.4rem; }
|
/* ✅ Sub-categories row MUST stay above the Sociowall overlap */
|
||||||
|
.map-overlay-bottom { bottom: calc(3.2rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom)); left:50%; transform:translateX(-50%); display:flex; gap:.4rem; }
|
||||||
|
|
||||||
.map-overlay-myloc { right:.7rem; bottom:6rem; }
|
.map-overlay-myloc { right:.7rem; bottom:6rem; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
display:flex;
|
display:flex;
|
||||||
gap:.6rem;
|
gap:.6rem;
|
||||||
align-items:stretch;
|
align-items:stretch;
|
||||||
|
min-width:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panels */
|
.sociowall-panel{
|
||||||
.sociowall-panel {
|
flex: 1 1 auto;
|
||||||
flex: 1.8;
|
min-width:0; /* IMPORTANT: prevents pushing chat */
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
background: rgba(15, 23, 42, 0.96);
|
background: rgba(15, 23, 42, 0.96);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
flex-direction:column;
|
flex-direction:column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sociowall-header {
|
.sociowall-header{
|
||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: .06em;
|
letter-spacing: .06em;
|
||||||
|
|
@ -27,39 +28,45 @@
|
||||||
margin-bottom:.25rem;
|
margin-bottom:.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-list { flex: 1; overflow-y: auto; padding-right: .15rem; max-height: 42vh; }
|
/* list scroll inside wall */
|
||||||
.post-list-header { display:flex; align-items:center; gap:.5rem; margin-bottom:.25rem; font-size:.7rem; color:#cbd5f5; }
|
.post-list{
|
||||||
.km-filter input[type="range"] { width:160px; }
|
flex:1;
|
||||||
|
overflow-y:auto;
|
||||||
|
padding-right:.15rem;
|
||||||
|
max-height: 44vh;
|
||||||
|
}
|
||||||
|
|
||||||
.status-text { font-size:.72rem; opacity:.85; margin:.15rem 0; }
|
/* prevent long content from widening layout */
|
||||||
.status-error { color:#fecaca; }
|
.post-card{
|
||||||
|
overflow:hidden;
|
||||||
/* list items compact */
|
min-width:0;
|
||||||
.post-card {
|
|
||||||
border-radius:10px;
|
border-radius:10px;
|
||||||
border:1px solid rgba(55, 65, 81, 0.9);
|
border:1px solid rgba(55, 65, 81, 0.9);
|
||||||
background: rgba(15, 23, 42, 0.92);
|
background: rgba(15, 23, 42, 0.92);
|
||||||
padding:.3rem .45rem;
|
padding:.3rem .45rem;
|
||||||
margin-bottom:.22rem;
|
margin-bottom:.22rem;
|
||||||
font-size:.76rem;
|
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
transition: background .15s ease, border-color .15s ease, box-shadow .15s ease, transform .1s ease;
|
|
||||||
}
|
|
||||||
.post-card:hover {
|
|
||||||
background: rgba(17, 24, 39, 0.95);
|
|
||||||
border-color: rgba(96, 165, 250, 0.9);
|
|
||||||
box-shadow: 0 0 12px rgba(56,189,248,.4);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.post-card.selected {
|
|
||||||
background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
|
|
||||||
border-color: #bfdbfe;
|
|
||||||
box-shadow: 0 0 16px rgba(56,189,248,.6), 0 10px 22px rgba(15,23,42,.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-panel {
|
/* SCALE the fixed 280px template so it never forces width */
|
||||||
width: 170px;
|
.post-card .sw-wall-mini{
|
||||||
min-width: 170px;
|
--sw-wall-scale: 0.86;
|
||||||
|
transform: scale(var(--sw-wall-scale));
|
||||||
|
transform-origin: left top;
|
||||||
|
width: calc(100% / var(--sw-wall-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list-header{ display:flex; align-items:center; gap:.5rem; margin-bottom:.25rem; font-size:.7rem; color:#cbd5f5; }
|
||||||
|
.km-filter input[type="range"]{ width:160px; }
|
||||||
|
|
||||||
|
.status-text{ font-size:.72rem; opacity:.85; margin:.15rem 0; }
|
||||||
|
.status-error{ color:#fecaca; }
|
||||||
|
|
||||||
|
.chat-panel{
|
||||||
|
width: 190px;
|
||||||
|
min-width: 190px;
|
||||||
|
max-width: 220px;
|
||||||
|
min-height: 160px;
|
||||||
background: rgba(15, 23, 42, 0.96);
|
background: rgba(15, 23, 42, 0.96);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
|
@ -67,13 +74,10 @@
|
||||||
padding:.4rem .45rem;
|
padding:.4rem .45rem;
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction:column;
|
flex-direction:column;
|
||||||
|
overflow:hidden;
|
||||||
position: sticky;
|
|
||||||
top: 10px;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header{
|
||||||
font-size:.78rem;
|
font-size:.78rem;
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
letter-spacing:.06em;
|
letter-spacing:.06em;
|
||||||
|
|
@ -81,23 +85,22 @@
|
||||||
color:#bfdbfe;
|
color:#bfdbfe;
|
||||||
margin-bottom:.2rem;
|
margin-bottom:.2rem;
|
||||||
}
|
}
|
||||||
.chat-users { list-style:none; padding:0; margin:0; font-size:.76rem; }
|
|
||||||
.chat-users li { display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; }
|
|
||||||
.chat-users li::before { content:"👤"; font-size:.72rem; opacity:.9; }
|
|
||||||
|
|
||||||
/* ✅ MOBILE: keep chat BESIDE sociowall (no stacking) */
|
.chat-users{
|
||||||
@media (max-width: 640px){
|
list-style:none;
|
||||||
.below-row{ flex-direction: row; }
|
padding:0; margin:0;
|
||||||
.sociowall-panel{ flex: 2.2; }
|
font-size:.76rem;
|
||||||
.chat-panel{
|
overflow:auto;
|
||||||
width: 34vw;
|
-webkit-overflow-scrolling: touch;
|
||||||
min-width: 140px;
|
max-height: 44vh;
|
||||||
max-width: 180px;
|
|
||||||
position: static;
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
.post-list{ max-height: 36vh; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Guard: never render Sociowall overlay on top of the map */
|
.chat-users li{ display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; }
|
||||||
.map-overlay-wall{ display:none !important; }
|
.chat-users li::before{ content:"👤"; font-size:.72rem; opacity:.9; }
|
||||||
|
|
||||||
|
/* stack only very small */
|
||||||
|
@media (max-width: 420px){
|
||||||
|
.below-row{ flex-direction: column; }
|
||||||
|
.chat-panel{ width:100%; min-width:0; max-width:none; }
|
||||||
|
.post-list{ max-height: 46vh; }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue