From d7f12db601fc4cc2928180796a83be44a9ec845e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Dec 2025 22:19:15 -0500 Subject: [PATCH] nleh --- src/components/Map/MapView.jsx | 24 +- src/components/Map/markerManager.js | 69 +++++- src/components/Map/markerManager.js.bak | 296 ++++++++++++++++++++++++ src/components/Map/templateSpecs.js | 58 +---- src/components/Map/useMapCore.js | 23 +- src/components/Map/usePostsEngine.js | 27 ++- src/styles/filters.css | 19 ++ src/styles/layout.css | 18 +- src/styles/map.css | 72 +++--- src/styles/overlays.css | 5 +- src/styles/posts.css | 103 +++++---- 11 files changed, 556 insertions(+), 158 deletions(-) create mode 100644 src/components/Map/markerManager.js.bak diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index c4cf18b..268a452 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -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 "../../styles/mapMarkers.css"; @@ -34,6 +34,9 @@ export default function MapView({ const [timeFilter, setTimeFilter] = useState("RECENT"); const [subFilter, setSubFilter] = useState("All"); + const stageRef = useRef(null); + const [showSubcats, setShowSubcats] = useState(true); + const userPosition = useUserPosition(mapRef, hasLastView); const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({ @@ -89,6 +92,23 @@ export default function MapView({ }, [selectedPost, mapRef]); // 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(() => { const proto = window.location.protocol === "https:" ? "wss" : "ws"; const host = @@ -224,7 +244,7 @@ export default function MapView({ return (
-
+
{status &&
{status}
} diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index 0eba557..8b8c1a1 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -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") { const map = mapRef.current; if (!map) return; @@ -118,6 +152,8 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the return; } + const lngLat = [lon, lat]; + const root = document.createElement("div"); root.className = "post-pin post-pin--compact"; root.style.zIndex = "1"; @@ -134,7 +170,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the root.className = "post-pin post-pin--compact"; root.style.zIndex = "1"; - // pointer stays: .post-pin-pointer-small (UNCHANGED) root.innerHTML = `
@@ -155,7 +190,18 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the root.className = "post-pin post-pin--expanded"; 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"); root.innerHTML = ` @@ -212,8 +258,23 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the root.__swUnmount = mountCard(wrap, spec, data, theme); } + // After render, pan so marker sits at the preferred anchor (1/7 from bottom) requestAnimationFrame(() => { - applyOcclusionForExpanded(markersRef, root); + 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); + } catch {} }); } @@ -221,7 +282,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the root.__renderCompact = renderCompact; const marker = new maplibregl.Marker({ element: root, anchor: "bottom" }) - .setLngLat([lon, lat]) + .setLngLat(lngLat) .addTo(map); markersRef.current.push({ id: post.id, marker, el: root, post }); diff --git a/src/components/Map/markerManager.js.bak b/src/components/Map/markerManager.js.bak new file mode 100644 index 0000000..10c6af8 --- /dev/null +++ b/src/components/Map/markerManager.js.bak @@ -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 = ` +
+
+ `; + + 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 = ` +
+
+
+
+
+ +
+
Watching
+
+
☑ Julia ★
— I see it…
+
☑ Kim / CNN
— Blah blah blah
+
☑ Yan ★
— Watch this!!
+
+ +
+
+ +
+
${headline}
+
The news here (generated)
+
+ +
+ + + + +
+ +
+
Live chat / comments
+
+
— …
+
— what the f***?
+
— nice…
+
— my eyes!!
+
+
+ + +
+
+
+
+ `; + + 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, """); +} diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js index 0d35c4e..6cad01f 100644 --- a/src/components/Map/templateSpecs.js +++ b/src/components/Map/templateSpecs.js @@ -1,58 +1,34 @@ /** - * Frontend-only template specs (no DB changes). - * Keep pointers as-is: + * Frontend-only template specs. + * Pointers stay as-is: * - compact triangle: .post-pin-pointer-small * - expanded triangle: .post-card-pointer */ - export const TEMPLATE_SPECS = { news: { mini_spec: { - size: { w: 280, h: 110 }, + size: { w: 240, h: 96 }, /* ✅ smaller to avoid layout push */ background: { type: "gradient", value: "newsBlue" }, radius: 18, layers: [ - { id: "badge", type: "chip", x: 14, y: 12, 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: "meta", type: "text", x: 14, y: 88, w: 252, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 } + { id: "badge", type: "chip", x: 12, y: 10, text: "NEWS", style: "chip.news" }, + { 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: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 } ], }, - - /* Desktop/tablet full */ full_spec: { - size: { w: 360, h: 480 }, + size: { w: 360, h: 520 }, background: { type: "solid", value: "surface" }, radius: 22, 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: "title", type: "text", x: 16, y: 200, w: 328, h: 70, 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: "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: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 }, { id: "cta", type: "button", - x: 16, y: 402, 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, + x: 16, y: 450, w: 328, h: 48, text: "Open source", style: "button.primary", action: { type: "open_url", bind: "data.url" } @@ -78,17 +54,7 @@ export function getTemplateKeyForPost(post) { export function getTemplateSpecForPost(post, variant /* "mini"|"full" */) { const key = getTemplateKeyForPost(post); const t = TEMPLATE_SPECS[key] || TEMPLATE_SPECS.news; - - 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; + return variant === "full" ? t.full_spec : t.mini_spec; } export function adaptPostToTemplateData(post) { diff --git a/src/components/Map/useMapCore.js b/src/components/Map/useMapCore.js index 85f09a2..14445d4 100644 --- a/src/components/Map/useMapCore.js +++ b/src/components/Map/useMapCore.js @@ -88,8 +88,22 @@ export function useMapCore(theme) { map.on("move", reOcclude); map.on("zoom", reOcclude); - map.on("click", () => closeExpandedIfAny()); - map.on("dragstart", () => closeExpandedIfAny()); + // IMPORTANT: ignore close while we are auto-panning to keep popup visible + 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))); @@ -98,7 +112,10 @@ export function useMapCore(theme) { setViewParams(vp); try { 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 {} }); diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js index d45be49..bbdc7a2 100644 --- a/src/components/Map/usePostsEngine.js +++ b/src/components/Map/usePostsEngine.js @@ -15,6 +15,7 @@ export function usePostsEngine({ }) { const allPostsRef = useRef([]); const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" }); + const delayedFetchRef = useRef(null); const [status, setStatus] = useState("Loading posts..."); const [visiblePosts, setVisiblePosts] = useState([]); @@ -26,6 +27,12 @@ export function usePostsEngine({ const map = mapRef.current; 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); const posts = allPostsRef.current || []; @@ -61,6 +68,19 @@ export function usePostsEngine({ let cancelled = false; 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 radiusKm = viewParams.radiusKm || 750; const filterKey = `${mainFilter}|${subFilter}`; @@ -101,11 +121,9 @@ export function usePostsEngine({ const existing = allPostsRef.current || []; const byId = new Map(); - for (const p of existing) { if (p && typeof p.id !== "undefined") byId.set(p.id, p); } - if (Array.isArray(newPosts)) { for (const p of newPosts) { 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()); - lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey }; rebuildMarkersForFilters(timeFilter); @@ -131,6 +148,10 @@ export function usePostsEngine({ load(); return () => { cancelled = true; + if (delayedFetchRef.current) { + try { clearTimeout(delayedFetchRef.current); } catch {} + delayedFetchRef.current = null; + } }; }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]); diff --git a/src/styles/filters.css b/src/styles/filters.css index 760ac24..fa12dcd 100644 --- a/src/styles/filters.css +++ b/src/styles/filters.css @@ -33,3 +33,22 @@ .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-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; } diff --git a/src/styles/layout.css b/src/styles/layout.css index a6f94d4..4c262bf 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1,20 +1,14 @@ .app-root{ min-height:100vh; display:flex; flex-direction:column; } -/* allow scrolling under the map */ -.main-shell{ flex: 0 0 auto; display:block; padding:0; } +/* allow full page scroll */ +.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{ position:relative; width:100%; - height:68vh; - border-radius:0; - overflow:hidden; + height:auto; + overflow:visible; border:none; - background:#020617; -} - -/* Phones: taller map => wall+chat lower */ -@media (max-width: 640px){ - .map-shell{ height:76vh; } + background:transparent; } diff --git a/src/styles/map.css b/src/styles/map.css index 1e33848..9b4b999 100644 --- a/src/styles/map.css +++ b/src/styles/map.css @@ -1,49 +1,49 @@ -.map-page { - display: flex; - flex-direction: column; - height: 100%; +:root{ + --sw-below-overlap: 40px; /* wall overlaps map a bit */ } -/* Map stage should fill the map-shell height (no competing vh/clamp here) */ -.map-stage { - position: relative; - width: 100%; - height: 100%; +.map-page{ display:flex; flex-direction:column; width:100%; } + +/* Map stage first */ +.map-stage{ + position:relative; + width:100%; + height: clamp(520px, 72vh, 860px); } -.map-container, -.maplibre-container { - position: absolute; - inset: 0; - width: 100%; - height: 100%; +/* Landscape: vh is small, reduce min height */ +@media (orientation: landscape){ + :root{ --sw-below-overlap: 22px; } + .map-stage{ height: clamp(360px, 62vh, 720px); } } -.map-status { - position: absolute; - 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; +.map-container, .maplibre-container{ + position:absolute; inset:0; width:100%; height:100%; } -/* Below map area */ -.below-stage { - padding: 0.25rem 0.7rem 1rem; +.map-status{ + position:absolute; left:.5rem; bottom:.5rem; + font-size:.7rem; padding:.2rem .4rem; + background:rgba(0,0,0,.6); border-radius:.5rem; + z-index:30; } -.below-row { - display: flex; - gap: 0.6rem; - align-items: stretch; +/* BELOW MAP (scroll area) – pulled up slightly over map */ +.below-stage{ + margin-top: calc(var(--sw-below-overlap) * -1); + position:relative; + z-index:25; + padding: .25rem .7rem 1.0rem; } -/* Mobile: stack */ -@media (max-width: 640px) { - .below-row { - flex-direction: column; - } +.below-row{ + display:flex; + gap:.6rem; + 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; } } diff --git a/src/styles/overlays.css b/src/styles/overlays.css index b5a0945..92c15e0 100644 --- a/src/styles/overlays.css +++ b/src/styles/overlays.css @@ -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-top { @@ -15,7 +15,8 @@ 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; } diff --git a/src/styles/posts.css b/src/styles/posts.css index 563f856..5444240 100644 --- a/src/styles/posts.css +++ b/src/styles/posts.css @@ -3,11 +3,12 @@ display:flex; gap:.6rem; align-items:stretch; + min-width:0; } -/* Panels */ -.sociowall-panel { - flex: 1.8; +.sociowall-panel{ + flex: 1 1 auto; + min-width:0; /* IMPORTANT: prevents pushing chat */ min-height: 160px; background: rgba(15, 23, 42, 0.96); border-radius: 14px; @@ -18,7 +19,7 @@ flex-direction:column; } -.sociowall-header { +.sociowall-header{ font-size: .78rem; font-weight: 700; letter-spacing: .06em; @@ -27,39 +28,45 @@ margin-bottom:.25rem; } -.post-list { flex: 1; overflow-y: auto; padding-right: .15rem; max-height: 42vh; } -.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; } +/* list scroll inside wall */ +.post-list{ + flex:1; + overflow-y:auto; + padding-right:.15rem; + max-height: 44vh; +} -.status-text { font-size:.72rem; opacity:.85; margin:.15rem 0; } -.status-error { color:#fecaca; } - -/* list items compact */ -.post-card { +/* prevent long content from widening layout */ +.post-card{ + overflow:hidden; + min-width:0; border-radius:10px; border:1px solid rgba(55, 65, 81, 0.9); background: rgba(15, 23, 42, 0.92); padding:.3rem .45rem; margin-bottom:.22rem; - font-size:.76rem; 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 { - width: 170px; - min-width: 170px; +/* SCALE the fixed 280px template so it never forces width */ +.post-card .sw-wall-mini{ + --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); border-radius: 14px; border: 1px solid rgba(148, 163, 184, 0.9); @@ -67,13 +74,10 @@ padding:.4rem .45rem; display:flex; flex-direction:column; - - position: sticky; - top: 10px; - align-self: flex-start; + overflow:hidden; } -.chat-header { +.chat-header{ font-size:.78rem; font-weight:700; letter-spacing:.06em; @@ -81,23 +85,22 @@ color:#bfdbfe; 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) */ -@media (max-width: 640px){ - .below-row{ flex-direction: row; } - .sociowall-panel{ flex: 2.2; } - .chat-panel{ - width: 34vw; - min-width: 140px; - max-width: 180px; - position: static; - top: auto; - } - .post-list{ max-height: 36vh; } +.chat-users{ + list-style:none; + padding:0; margin:0; + font-size:.76rem; + overflow:auto; + -webkit-overflow-scrolling: touch; + max-height: 44vh; } -/* Guard: never render Sociowall overlay on top of the map */ -.map-overlay-wall{ display:none !important; } +.chat-users li{ display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; } +.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; } +}