This commit is contained in:
Your Name 2025-12-18 15:45:46 -05:00
parent 70251093a8
commit 4753cacdcc
7 changed files with 317 additions and 158 deletions

View File

@ -1,5 +1,6 @@
import React, { Suspense, useEffect, useState } from "react"; import React, { Suspense, useEffect, useState, useCallback } from "react";
import TopBar from "./components/Layout/TopBar"; import TopBar from "./components/Layout/TopBar";
import PostList from "./components/Posts/PostList";
// Lazy-load de la carte (gros chunk : maplibre, CSS, etc.) // Lazy-load de la carte (gros chunk : maplibre, CSS, etc.)
const MapView = React.lazy(() => import("./components/Map/MapView")); const MapView = React.lazy(() => import("./components/Map/MapView"));
@ -9,6 +10,19 @@ const THEME_KEY = "sociowire:theme";
export default function App() { export default function App() {
const [theme, setTheme] = useState("blue"); const [theme, setTheme] = useState("blue");
// Wall state (lifted from MapView)
const [wallPosts, setWallPosts] = useState([]);
const [wallLoading, setWallLoading] = useState(false);
const [wallError, setWallError] = useState("");
const [selectedPost, setSelectedPost] = useState(null);
const handlePostsState = useCallback((next) => {
if (!next) return;
if (Array.isArray(next.posts)) setWallPosts(next.posts);
setWallLoading(!!next.loading);
setWallError(next.error || "");
}, []);
// charge le thème au démarrage // charge le thème au démarrage
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@ -39,12 +53,45 @@ export default function App() {
return ( return (
<div className="app-root"> <div className="app-root">
<TopBar theme={theme} onChangeTheme={setTheme} /> <TopBar theme={theme} onChangeTheme={setTheme} />
<div className="main-shell"> <div className="main-shell">
<div className="map-shell"> <div className="map-shell">
<Suspense fallback={<div style={{ padding: 16 }}>Loading map</div>}> <Suspense fallback={<div style={{ padding: 16 }}>Loading map</div>}>
<MapView theme={theme} /> <MapView
theme={theme}
onPostsState={handlePostsState}
selectedPost={selectedPost}
onSelectPost={setSelectedPost}
/>
</Suspense> </Suspense>
</div> </div>
{/* BELOW THE MAP (page scroll like Facebook) */}
<div className="below-stage">
<div className="below-row">
<div className="sociowall-panel">
<div className="sociowall-header">SOCIOWALL</div>
<PostList
posts={wallPosts}
loading={wallLoading}
error={wallError}
selectedPost={selectedPost}
onSelectPost={setSelectedPost}
/>
</div>
<div className="chat-panel">
<div className="chat-header">CHAT</div>
<ul className="chat-users">
<li>Yan</li>
<li>Marc</li>
<li>Fiso</li>
</ul>
</div>
</div>
</div>
<div style={{ height: 18 }} />
</div> </div>
</div> </div>
); );

View File

@ -11,15 +11,24 @@ import { FILTER_MAIN_CATEGORIES, FILTER_CATEGORY_MAP, CREATE_CATEGORY_MAP } from
import { useMapCore } from "./useMapCore"; import { useMapCore } from "./useMapCore";
import { useUserPosition } from "./useUserPosition"; import { useUserPosition } from "./useUserPosition";
import { usePostsEngine } from "./usePostsEngine"; import { usePostsEngine } from "./usePostsEngine";
import PostList from "../Posts/PostList";
import { useAuth } from "../../auth/AuthContext"; import { useAuth } from "../../auth/AuthContext";
import FilterButtons from "../Filters/FilterButtons"; import FilterButtons from "../Filters/FilterButtons";
import TimeFilterButtons from "../Filters/TimeFilterButtons"; import TimeFilterButtons from "../Filters/TimeFilterButtons";
export default function MapView({ theme = "dark" }) { export default function MapView({
theme = "dark",
// lift posts to App (so Sociowall is NOT inside the map)
onPostsState,
// selection controlled by App
selectedPost,
onSelectPost,
}) {
const { authenticated, username, token } = useAuth(); const { authenticated, username, token } = useAuth();
const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } = useMapCore(theme); const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } =
useMapCore(theme);
const [mainFilter, setMainFilter] = useState("All"); const [mainFilter, setMainFilter] = useState("All");
const [timeFilter, setTimeFilter] = useState("RECENT"); const [timeFilter, setTimeFilter] = useState("RECENT");
@ -37,8 +46,6 @@ export default function MapView({ theme = "dark" }) {
expandedElRef, expandedElRef,
}); });
const [selectedPost, setSelectedPost] = useState(null);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [draftTitle, setDraftTitle] = useState(""); const [draftTitle, setDraftTitle] = useState("");
const [draftBody, setDraftBody] = useState(""); const [draftBody, setDraftBody] = useState("");
@ -50,11 +57,38 @@ export default function MapView({ theme = "dark" }) {
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"]; const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
// keep subFilter valid
useEffect(() => { useEffect(() => {
if (!bottomCategories.length) return; if (!bottomCategories.length) return;
if (!subFilter || !bottomCategories.includes(subFilter)) setSubFilter("All"); if (!subFilter || !bottomCategories.includes(subFilter)) setSubFilter("All");
}, [mainFilter, bottomCategories, subFilter]); }, [mainFilter, bottomCategories, subFilter]);
// push posts state up to App (for Sociowall)
useEffect(() => {
if (!onPostsState) return;
onPostsState({
status,
posts: visiblePosts,
loading: loadingPosts,
error: loadError,
});
}, [status, visiblePosts, loadingPosts, loadError, onPostsState]);
// if App selects a post (from Sociowall), fly to it on map
useEffect(() => {
if (!selectedPost) return;
const map = mapRef.current;
if (!map) return;
const lng = selectedPost.lon ?? selectedPost.lng;
const lat = selectedPost.lat ?? selectedPost.latitude;
if (typeof lng === "number" && typeof lat === "number") {
map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
}
}, [selectedPost, mapRef]);
// Websocket for new posts
useEffect(() => { useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws"; const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host = const host =
@ -108,6 +142,7 @@ export default function MapView({ theme = "dark" }) {
const handleSubmitPost = async () => { const handleSubmitPost = async () => {
if (isSaving) return; if (isSaving) return;
setSaveError(""); setSaveError("");
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
@ -130,8 +165,8 @@ export default function MapView({ theme = "dark" }) {
baseLat = center.lat; baseLat = center.lat;
} }
// keep marker visually above pointer
const pixelOffsetY = -20; const pixelOffsetY = -20;
const screenPoint = map.project([baseLng, baseLat]); const screenPoint = map.project([baseLng, baseLat]);
const correctedPoint = { x: screenPoint.x, y: screenPoint.y + pixelOffsetY }; const correctedPoint = { x: screenPoint.x, y: screenPoint.y + pixelOffsetY };
const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]); const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]);
@ -170,146 +205,133 @@ export default function MapView({ theme = "dark" }) {
} }
}; };
const handleSelectPost = (post) => {
setSelectedPost(post);
const map = mapRef.current;
if (!map) return;
const lng = post.lon ?? post.lng;
const lat = post.lat ?? post.latitude;
if (typeof lng === "number" && typeof lat === "number") map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
};
const handleMainFilterSelect = (code) => { const handleMainFilterSelect = (code) => {
if (!FILTER_MAIN_CATEGORIES.includes(code)) return; if (!FILTER_MAIN_CATEGORIES.includes(code)) return;
setMainFilter(code); setMainFilter(code);
}; };
// allow selecting from map later (optional hook)
const handleSelectPost = (post) => {
if (onSelectPost) onSelectPost(post);
const map = mapRef.current;
if (!map) return;
const lng = post.lon ?? post.lng;
const lat = post.lat ?? post.latitude;
if (typeof lng === "number" && typeof lat === "number") {
map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
}
};
return ( return (
<div className="map-page">\n <div className="map-stage"> <div className="map-page">
<div ref={containerRef} className="map-container" /> <div className="map-stage">
{status && <div className="map-status">{status}</div>} <div ref={containerRef} className="map-container" />
{status && <div className="map-status">{status}</div>}
<div className="map-overlay map-overlay-top"> <div className="map-overlay map-overlay-top">
<button className="chip-pill" onClick={handleSearchClick}>Look at</button> <button className="chip-pill" onClick={handleSearchClick}>
<button className="chip-pill" onClick={handleOpenCreate}>Place your wire</button> Look at
</div> </button>
<button className="chip-pill" onClick={handleOpenCreate}>
<div className="map-overlay map-overlay-left"> Place your wire
<TimeFilterButtons active={timeFilter} onSelect={(code) => setTimeFilter(code)} /> </button>
</div>
<div className="map-overlay map-overlay-right">
<FilterButtons active={mainFilter} onSelect={handleMainFilterSelect} />
</div>
<div className="map-overlay map-overlay-bottom">
<div className="sw-bottom-row">
{bottomCategories.map((c) => (
<button
key={c}
type="button"
className={"sw-bottom-item" + (subFilter === c ? " sw-bottom-active" : "")}
onClick={() => setSubFilter(c)}
>
<div className="sw-bottom-circle">{c === "All" ? "All" : c.slice(0, 2)}</div>
<div className="sw-bottom-label">{c}</div>
</button>
))}
</div> </div>
</div>
<div className="map-overlay map-overlay-myloc"> <div className="map-overlay map-overlay-left">
<button className="chip-pill" disabled={!userPosition} onClick={handleFlyToMe}> <TimeFilterButtons active={timeFilter} onSelect={(code) => setTimeFilter(code)} />
My spot
</button>
</div>
{isCreating && (
<div className="map-overlay map-crosshair">
<div className="crosshair-aim" />
</div> </div>
)}
{isCreating && ( <div className="map-overlay map-overlay-right">
<div className="map-overlay create-post-panel"> <FilterButtons active={mainFilter} onSelect={handleMainFilterSelect} />
<div className="create-post-header"> </div>
<span>Create wire</span>
<button className="create-close" onClick={() => setIsCreating(false)}></button> <div className="map-overlay map-overlay-bottom">
</div> <div className="sw-bottom-row">
<span className="crosshair-label"> {bottomCategories.map((c) => (
Move the map, aim with the crosshair your wire will be pinned there. <button
</span> key={c}
<div className="create-row"> type="button"
<select className={"sw-bottom-item" + (subFilter === c ? " sw-bottom-active" : "")}
value={draftCategory} onClick={() => setSubFilter(c)}
onChange={(e) => { >
const cat = e.target.value; <div className="sw-bottom-circle">{c === "All" ? "All" : c.slice(0, 2)}</div>
setDraftCategory(cat); <div className="sw-bottom-label">{c}</div>
setDraftSubCategory((CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]); </button>
}} ))}
>
{["News", "Friends", "Events", "Market"].map((cat) => (
<option key={cat}>{cat}</option>
))}
</select>
<select value={draftSubCategory} onChange={(e) => setDraftSubCategory(e.target.value)}>
{(CREATE_CATEGORY_MAP[draftCategory] || CREATE_CATEGORY_MAP.News).map((sub) => (
<option key={sub}>{sub}</option>
))}
</select>
</div>
<input
className="create-input"
placeholder="Short title..."
value={draftTitle}
onChange={(e) => setDraftTitle(e.target.value)}
/>
<textarea
className="create-textarea"
rows={2}
placeholder="Context (optional)..."
value={draftBody}
onChange={(e) => setDraftBody(e.target.value)}
/>
{saveError && <div className="create-error">{saveError}</div>}
<div className="create-actions">
<button className="chip-pill" onClick={handleSubmitPost} disabled={isSaving}>
{isSaving ? "Posting..." : "Post"}
</button>
</div> </div>
</div> </div>
)}
<div className="map-overlay map-overlay-myloc">
<button className="chip-pill" disabled={!userPosition} onClick={handleFlyToMe}>
My spot
</button>
</div>
</div> {isCreating && (
<div className="map-overlay map-crosshair">
<div className="crosshair-aim" />
</div>
)}
{/* BELOW THE MAP (page scroll) */} {isCreating && (
<div className="below-stage"> <div className="map-overlay create-post-panel">
<div className="below-row"> <div className="create-post-header">
<div className="sociowall-panel"> <span>Create wire</span>
<div className="sociowall-header">Sociowall</div> <button className="create-close" onClick={() => setIsCreating(false)}>
<PostList
posts={visiblePosts} </button>
loading={loadingPosts} </div>
error={loadError}
selectedPost={selectedPost} <span className="crosshair-label">
onSelectPost={handleSelectPost} Move the map, aim with the crosshair your wire will be pinned there.
</span>
<div className="create-row">
<select
value={draftCategory}
onChange={(e) => {
const cat = e.target.value;
setDraftCategory(cat);
setDraftSubCategory((CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]);
}}
>
{["News", "Friends", "Events", "Market"].map((cat) => (
<option key={cat}>{cat}</option>
))}
</select>
<select value={draftSubCategory} onChange={(e) => setDraftSubCategory(e.target.value)}>
{(CREATE_CATEGORY_MAP[draftCategory] || CREATE_CATEGORY_MAP.News).map((sub) => (
<option key={sub}>{sub}</option>
))}
</select>
</div>
<input
className="create-input"
placeholder="Short title..."
value={draftTitle}
onChange={(e) => setDraftTitle(e.target.value)}
/> />
</div>
<div className="chat-panel"> <textarea
<div className="chat-header">Chat</div> className="create-textarea"
<ul className="chat-users"> rows={2}
<li>Yan</li> placeholder="Context (optional)..."
<li>Marc</li> value={draftBody}
<li>Fiso</li> onChange={(e) => setDraftBody(e.target.value)}
</ul> />
{saveError && <div className="create-error">{saveError}</div>}
<div className="create-actions">
<button className="chip-pill" onClick={handleSubmitPost} disabled={isSaving}>
{isSaving ? "Posting..." : "Post"}
</button>
</div>
</div> </div>
</div> )}
</div> </div>
<div style={{ height: 24 }} />
</div> </div>
); );
} }

View File

@ -17,19 +17,42 @@ export const TEMPLATE_SPECS = {
{ 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: 14, y: 88, w: 252, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
], ],
}, },
/* Desktop/tablet full */
full_spec: { full_spec: {
size: { w: 360, h: 520 }, size: { w: 360, h: 480 },
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: 220, bind: "data.image" }, { id: "hero", type: "image", x: 0, y: 0, w: 360, h: 180, 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: 240, w: 328, h: 78, bind: "data.headline", style: "text.h1", maxLines: 3 }, { 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: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 }, { id: "summary", type: "text", x: 16, y: 272, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 4 },
{ {
id: "cta", id: "cta",
type: "button", type: "button",
x: 16, y: 450, w: 328, h: 48, 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,
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" }
@ -49,14 +72,23 @@ function normCat(post) {
} }
export function getTemplateKeyForPost(post) { export function getTemplateKeyForPost(post) {
// Future mapping: if (post.template_id === 101) return "news";
return normCat(post); return normCat(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) {

View File

@ -1,5 +1,20 @@
.app-root{ min-height:100vh; display:flex; flex-direction:column; } .app-root{ min-height:100vh; display:flex; flex-direction:column; }
/* IMPORTANT: allow scrolling under the map */ /* allow scrolling under the map */
.main-shell{ flex: 0 0 auto; display:block; padding:0; } .main-shell{ flex: 0 0 auto; display:block; padding:0; }
.map-shell{ position:relative; width:100%; height:62vh; border-radius:0; overflow:hidden; border:none; background:#020617; }
/* MAP HEIGHT (make map taller so Sociowall/Chat sit lower) */
.map-shell{
position:relative;
width:100%;
height:68vh;
border-radius:0;
overflow:hidden;
border:none;
background:#020617;
}
/* Phones: taller map => wall+chat lower */
@media (max-width: 640px){
.map-shell{ height:76vh; }
}

View File

@ -1,13 +1,18 @@
.map-page { display: flex; flex-direction: column; } .map-page {
display: flex;
flex-direction: column;
height: 100%;
}
/* Map stage (visible first). Then you scroll down for the wall. */ /* Map stage should fill the map-shell height (no competing vh/clamp here) */
.map-stage { .map-stage {
position: relative; position: relative;
width: 100%; width: 100%;
height: clamp(520px, 72vh, 860px); height: 100%;
} }
.map-container, .maplibre-container { .map-container,
.maplibre-container {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
@ -26,17 +31,19 @@
} }
/* Below map area */ /* Below map area */
.below-stage{ .below-stage {
padding: 0.25rem 0.7rem 1.0rem; padding: 0.25rem 0.7rem 1rem;
} }
.below-row{
display:flex; .below-row {
gap:.6rem; display: flex;
align-items:stretch; gap: 0.6rem;
align-items: stretch;
} }
/* Mobile: stack */ /* Mobile: stack */
@media (max-width: 640px){ @media (max-width: 640px) {
.map-stage{ height: clamp(520px, 78vh, 900px); } .below-row {
.below-row{ flex-direction: column; } flex-direction: column;
}
} }

View File

@ -633,3 +633,34 @@
.post-pin--expanded .post-card-footer{ gap: 5px !important; } .post-pin--expanded .post-card-footer{ gap: 5px !important; }
.post-pin--expanded .post-card-btn{ font-size: 10px !important; padding: 3px 7px !important; } .post-pin--expanded .post-card-btn{ font-size: 10px !important; padding: 3px 7px !important; }
} }
/* === FINAL OVERRIDES (safe) === */
/* Keep pointers unchanged; only scale the mini template card */
.sw-template-mini-wrap{
transform: translate(-50%, calc(-100% - 12px)) scale(0.62) !important;
transform-origin: bottom center;
}
@media (max-width: 640px){
.sw-template-mini-wrap{
transform: translate(-50%, calc(-100% - 12px)) scale(0.52) !important;
}
}
/* === SW MOBILE: make expanded popup shorter (no pointer change) === */
@media (max-width: 640px){
.post-pin--expanded .post-card{
max-height: 58vh !important;
overflow: auto !important;
-webkit-overflow-scrolling: touch;
}
/* keep right panel from making it tall */
.sw-expanded-right{
display:none !important;
}
.sw-news-generated{
margin-top: 8px;
}
}

View File

@ -5,6 +5,7 @@
align-items:stretch; align-items:stretch;
} }
/* Panels */
.sociowall-panel { .sociowall-panel {
flex: 1.8; flex: 1.8;
min-height: 160px; min-height: 160px;
@ -26,7 +27,7 @@
margin-bottom:.25rem; margin-bottom:.25rem;
} }
.post-list { flex: 1; overflow-y: auto; padding-right: .15rem; max-height: 44vh; } .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; } .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; } .km-filter input[type="range"] { width:160px; }
@ -55,8 +56,6 @@
border-color: #bfdbfe; border-color: #bfdbfe;
box-shadow: 0 0 16px rgba(56,189,248,.6), 0 10px 22px rgba(15,23,42,.98); box-shadow: 0 0 16px rgba(56,189,248,.6), 0 10px 22px rgba(15,23,42,.98);
} }
.post-card h3 { margin:0 0 .12rem 0; font-size:.8rem; }
.post-card p { margin:0; font-size:.72rem; opacity:.9; }
.chat-panel { .chat-panel {
width: 170px; width: 170px;
@ -69,7 +68,6 @@
display:flex; display:flex;
flex-direction:column; flex-direction:column;
/* stays visible beside wall while scrolling inside the list */
position: sticky; position: sticky;
top: 10px; top: 10px;
align-self: flex-start; align-self: flex-start;
@ -87,11 +85,18 @@
.chat-users li { display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; } .chat-users li { display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; }
.chat-users li::before { content:"👤"; font-size:.72rem; opacity:.9; } .chat-users li::before { content:"👤"; font-size:.72rem; opacity:.9; }
/* Mobile: stack */ /* ✅ MOBILE: keep chat BESIDE sociowall (no stacking) */
@media (max-width: 640px){ @media (max-width: 640px){
.below-row{ flex-direction: column; } .below-row{ flex-direction: row; }
.chat-panel{ width: 100%; min-width: 0; position: static; } .sociowall-panel{ flex: 2.2; }
.post-list{ max-height: 46vh; } .chat-panel{
width: 34vw;
min-width: 140px;
max-width: 180px;
position: static;
top: auto;
}
.post-list{ max-height: 36vh; }
} }
/* Guard: never render Sociowall overlay on top of the map */ /* Guard: never render Sociowall overlay on top of the map */