fix hover popup bug

This commit is contained in:
taDuc
2026-06-07 10:50:44 +07:00
parent 820e0b5216
commit 501d562025
9 changed files with 530 additions and 243 deletions
+1 -23
View File
@@ -20,32 +20,10 @@ const srOnlyStyle: React.CSSProperties = {
export default function Page() { export default function Page() {
return ( return (
<div style={{ position: "relative", width: "100%", height: "100svh", overflow: "hidden", backgroundColor: "#0b1220" }}> <div style={{ position: "relative", width: "100%", height: "100svh", overflow: "hidden" }}>
{/* Preload LCP image */} {/* Preload LCP image */}
<link rel="preload" as="image" href="/images/map_placeholder.webp" fetchPriority="high" /> <link rel="preload" as="image" href="/images/map_placeholder.webp" fetchPriority="high" />
{/* Permanent, static LCP image that is NEVER hidden or unmounted */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/map_placeholder.webp"
alt="Map Background"
fetchPriority="high"
loading="eager"
decoding="sync"
width={1920}
height={1080}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 0,
backgroundImage: "url('data:image/webp;base64,UklGRmgAAABXRUJQVlA4IFwAAAAQAgCdASoQAAkAAgA0JbACdAD0j9ruBiEAAP71e6Hb3PzxBzEI1XGXkdQ3Wq1ek8XLa1nPPm65FhrFIjmR0%2BxZwNUJBvg15I7CuzvhuunZ%2FUF83IaP8Evo6gAAAA%3D%3D')",
backgroundSize: "cover",
}}
/>
{/* Header (SSR & SEO) */} {/* Header (SSR & SEO) */}
<header style={srOnlyStyle}> <header style={srOnlyStyle}>
<nav> <nav>
+1 -16
View File
@@ -298,22 +298,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
}, [hasImageOverlay, isMapLoaded, mapRef]); }, [hasImageOverlay, isMapLoaded, mapRef]);
return ( return (
<div style={{ width: "100%", height, position: "relative", backgroundColor: "#0b1220" }}> <div style={{ width: "100%", height, position: "relative" }}>
{/* Opaque map placeholder image for LCP optimization */}
<img
src="/images/map_placeholder.webp"
alt="Map Loading Placeholder"
fetchPriority="high"
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 0,
}}
/>
<div <div
ref={containerRef} ref={containerRef}
style={{ style={{
+35 -15
View File
@@ -58,6 +58,7 @@ export function useMapHoverPopup({
let currentContent: MapHoverPopupContent | null = null; let currentContent: MapHoverPopupContent | null = null;
let selectedRowIndex = 0; let selectedRowIndex = 0;
let selectionVisible = false; let selectionVisible = false;
let selectionDirection: "next" | "prev" | null = null;
let lastSelectedRowClick: (() => void) | null = null; let lastSelectedRowClick: (() => void) | null = null;
let lastSelectedRowAt = 0; let lastSelectedRowAt = 0;
let hoverLayerIds = getHoverLayerIds(map); let hoverLayerIds = getHoverLayerIds(map);
@@ -98,14 +99,23 @@ export function useMapHoverPopup({
selectedRowIndex = wrapIndex(selectedRowIndex, rows.length); selectedRowIndex = wrapIndex(selectedRowIndex, rows.length);
lastSelectedRowClick = rows[selectedRowIndex]?.onClick || null; lastSelectedRowClick = rows[selectedRowIndex]?.onClick || null;
lastSelectedRowAt = Date.now(); lastSelectedRowAt = Date.now();
updatePopupRowSelection(popup, selectedRowIndex, selectionVisible); updatePopupRowSelection(popup, selectedRowIndex, selectionVisible, selectionDirection);
}; };
const cyclePopupRow = (direction: "next" | "prev") => { const cyclePopupRow = (direction: "next" | "prev") => {
const rows = getCurrentRows(); const rows = getCurrentRows();
if (!rows.length) return; if (!rows.length) return;
selectedRowIndex += direction === "prev" ? -1 : 1; const currentIndex = wrapIndex(selectedRowIndex, rows.length);
selectedRowIndex = wrapIndex(selectedRowIndex, rows.length); const nextIndex = direction === "prev"
? Math.max(0, currentIndex - 1)
: Math.min(rows.length - 1, currentIndex + 1);
selectionDirection = direction;
if (nextIndex === currentIndex) {
selectedRowIndex = currentIndex;
syncSelectedRow();
return;
}
selectedRowIndex = nextIndex;
syncSelectedRow(); syncSelectedRow();
}; };
@@ -153,7 +163,7 @@ export function useMapHoverPopup({
if (event.repeat) return; if (event.repeat) return;
if (isEditableEventTarget(event.target)) return; if (isEditableEventTarget(event.target)) return;
selectionVisible = true; selectionVisible = true;
updatePopupRowSelection(popup, selectedRowIndex, selectionVisible); updatePopupRowSelection(popup, selectedRowIndex, selectionVisible, selectionDirection);
requestPopupUpdateFromLastMouseEvent(); requestPopupUpdateFromLastMouseEvent();
return; return;
} }
@@ -488,7 +498,7 @@ function buildPopupNode(content: MapHoverPopupContent, selectedRowIndex: number,
grid.appendChild(card); grid.appendChild(card);
}); });
updatePopupNodeRowSelection(root, selectedRowIndex, selectionVisible); updatePopupNodeRowSelection(root, selectedRowIndex, selectionVisible, null);
return root; return root;
} }
@@ -598,11 +608,21 @@ function wrapIndex(index: number, length: number): number {
return ((index % length) + length) % length; return ((index % length) + length) % length;
} }
function updatePopupRowSelection(popup: maplibregl.Popup, selectedRowIndex: number, selectionVisible: boolean) { function updatePopupRowSelection(
updatePopupNodeRowSelection(popup.getElement() || null, selectedRowIndex, selectionVisible); popup: maplibregl.Popup,
selectedRowIndex: number,
selectionVisible: boolean,
selectionDirection: "next" | "prev" | null
) {
updatePopupNodeRowSelection(popup.getElement() || null, selectedRowIndex, selectionVisible, selectionDirection);
} }
function updatePopupNodeRowSelection(root: HTMLElement | null, selectedRowIndex: number, selectionVisible: boolean) { function updatePopupNodeRowSelection(
root: HTMLElement | null,
selectedRowIndex: number,
selectionVisible: boolean,
selectionDirection: "next" | "prev" | null
) {
if (!root) return; if (!root) return;
const cards = Array.from(root.querySelectorAll<HTMLElement>("[data-hover-popup-row-index]")); const cards = Array.from(root.querySelectorAll<HTMLElement>("[data-hover-popup-row-index]"));
let selectedCard: HTMLElement | null = null; let selectedCard: HTMLElement | null = null;
@@ -616,8 +636,8 @@ function updatePopupNodeRowSelection(root: HTMLElement | null, selectedRowIndex:
} }
} }
if (selectedCard) { if (selectedCard) {
ensurePopupRowVisible(selectedCard); ensurePopupRowVisible(selectedCard, selectionDirection);
window.requestAnimationFrame(() => ensurePopupRowVisible(selectedCard)); window.requestAnimationFrame(() => ensurePopupRowVisible(selectedCard, selectionDirection));
} }
} }
@@ -627,20 +647,20 @@ function applyPopupRowStyle(card: HTMLElement, selected: boolean) {
card.style.boxShadow = selected ? "inset 2px 0 0 rgba(56, 189, 248, 0.95)" : "none"; card.style.boxShadow = selected ? "inset 2px 0 0 rgba(56, 189, 248, 0.95)" : "none";
} }
function ensurePopupRowVisible(card: HTMLElement) { function ensurePopupRowVisible(card: HTMLElement, direction: "next" | "prev" | null) {
const scrollRoot = card.closest<HTMLElement>("[data-hover-popup-scroll-root='true']"); const scrollRoot = card.closest<HTMLElement>("[data-hover-popup-scroll-root='true']");
if (!scrollRoot) return; if (!scrollRoot) return;
const padding = 8; const padding = 8;
const groupHeader = findPreviousGroupHeader(card); const groupHeader = direction === "prev" ? findPreviousGroupHeader(card) : null;
const cardTop = card.offsetTop; const cardTop = card.offsetTop;
const cardBottom = cardTop + card.offsetHeight; const cardBottom = cardTop + card.offsetHeight;
const contextTop = groupHeader?.offsetTop ?? cardTop; const targetTop = groupHeader?.offsetTop ?? cardTop;
const visibleTop = scrollRoot.scrollTop + padding; const visibleTop = scrollRoot.scrollTop + padding;
const visibleBottom = scrollRoot.scrollTop + scrollRoot.clientHeight - padding; const visibleBottom = scrollRoot.scrollTop + scrollRoot.clientHeight - padding;
if (contextTop < visibleTop) { if (targetTop < visibleTop) {
scrollRoot.scrollTop = Math.max(0, contextTop - padding); scrollRoot.scrollTop = Math.max(0, targetTop - padding);
return; return;
} }
@@ -1,99 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import type { Entity } from "@/uhm/api/entities";
import type { Wiki } from "@/uhm/api/wikis";
type PopupRow = {
entity: Entity;
wiki: Wiki | null;
quote: string;
};
type Props = {
rows: PopupRow[];
featureId: string | number;
top: number;
left: number;
onClose: () => void;
onSelectRow: (entityId: string, wikiId?: string) => void;
};
export default function PinnedWikiPopup({
rows,
featureId,
top,
left,
onClose,
onSelectRow,
}: Props) {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
const handlePointerDown = (event: PointerEvent) => {
const target = event.target as Node | null;
if (target && containerRef.current?.contains(target)) {
return;
}
onClose();
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("pointerdown", handlePointerDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("pointerdown", handlePointerDown);
};
}, [onClose]);
return (
<div
ref={containerRef}
className="absolute z-30 w-[320px] max-w-[calc(100vw-2rem)]"
style={{ left, top }}
>
<div className="overflow-hidden rounded-xl border border-white/10 bg-slate-950/95 shadow-xl backdrop-blur">
<div className="max-h-[300px] overflow-y-auto p-3">
<div className="grid gap-2">
{rows.map(({ entity, wiki, quote }) => (
<button
key={`${entity.id}:${wiki?.id || "entity-only"}`}
type="button"
onClick={() => {
onSelectRow(entity.id, wiki?.id);
}}
className="w-full rounded-lg border border-white/10 bg-white/[0.03] px-3 py-3 text-left transition hover:border-sky-400/40 hover:bg-sky-500/10"
>
<div className="truncate text-sm font-semibold text-white">
{entity.name || String(entity.id)}
</div>
{quote ? (
<div
className="mt-2 pl-3 pr-1 text-sm italic leading-relaxed text-slate-300"
style={{
borderLeft: "3px solid rgba(56, 189, 248, 0.4)",
display: "-webkit-box",
WebkitLineClamp: 4,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "normal",
}}
>
{quote}
</div>
) : null}
</button>
))}
</div>
</div>
</div>
</div>
);
}
+90 -58
View File
@@ -10,7 +10,7 @@ import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerP
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar"; import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
import TimelineBar from "@/uhm/components/ui/TimelineBar"; import TimelineBar from "@/uhm/components/ui/TimelineBar";
import RelatedEntityPopup from "./RelatedEntityPopup"; import RelatedEntityPopup from "./RelatedEntityPopup";
import PinnedWikiPopup from "./PinnedWikiPopup"; import WikiSelectionPanel from "./WikiSelectionPanel";
import { fitMapToFeatureCollection } from "@/uhm/components/map/mapUtils"; import { fitMapToFeatureCollection } from "@/uhm/components/map/mapUtils";
import { fetchWikiById, type Wiki } from "@/uhm/api/wikis"; import { fetchWikiById, type Wiki } from "@/uhm/api/wikis";
@@ -104,7 +104,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
// Preview specific UI states // Preview specific UI states
const [previewWikiError, setPreviewWikiError] = useState<string | null>(null); const [previewWikiError, setPreviewWikiError] = useState<string | null>(null);
const [isPreviewWikiLoading, setIsPreviewWikiLoading] = useState(false); const [isPreviewWikiLoading, setIsPreviewWikiLoading] = useState(false);
const [previewPinnedWikiPopupAnchor, setPreviewPinnedWikiPopupAnchor] = useState<MapFeaturePayload | null>(null); const [previewWikiSelectionPanelAnchor, setPreviewWikiSelectionPanelAnchor] = useState<MapFeaturePayload | null>(null);
const [previewRightPanelMode, setPreviewRightPanelMode] = useState<"wiki" | "selection" | null>(null);
const [isPreviewEntitySidebarOpen, setIsPreviewEntitySidebarOpen] = useState(false); const [isPreviewEntitySidebarOpen, setIsPreviewEntitySidebarOpen] = useState(false);
const [previewLinkEntityPopup, setPreviewLinkEntityPopup] = useState<{ const [previewLinkEntityPopup, setPreviewLinkEntityPopup] = useState<{
slug: string; slug: string;
@@ -122,7 +123,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
setPreviewWikiCache({}); setPreviewWikiCache({});
setPreviewWikiError(null); setPreviewWikiError(null);
setIsPreviewWikiLoading(false); setIsPreviewWikiLoading(false);
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode(null);
setPreviewActiveEntityId(null); setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(false); setIsPreviewEntitySidebarOpen(false);
setPreviewLinkEntityPopup(null); setPreviewLinkEntityPopup(null);
@@ -317,7 +319,9 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
: null; : null;
const isReplayPreviewWikiSidebarOpen = mode && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen); const isPreviewWikiChooserOpen = previewRightPanelMode === "selection" && Boolean(previewWikiSelectionPanelAnchor);
const isReplayPreviewWikiSidebarOpen = mode && !isPreviewWikiChooserOpen && (replayPreviewSidebarOpen || isPreviewEntitySidebarOpen);
const isReplayPreviewRightPanelOpen = isReplayPreviewWikiSidebarOpen || isPreviewWikiChooserOpen;
// Handle replay preview entity selection // Handle replay preview entity selection
const selectReplayPreviewEntity = useCallback(( const selectReplayPreviewEntity = useCallback((
@@ -345,8 +349,9 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
setPreviewActiveEntityId(id); setPreviewActiveEntityId(id);
setIsPreviewEntitySidebarOpen(true); setIsPreviewEntitySidebarOpen(true);
setPreviewRightPanelMode("wiki");
setPreviewWikiError(null); setPreviewWikiError(null);
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewLinkEntityPopup(null); setPreviewLinkEntityPopup(null);
if (options?.focusMap === true) { if (options?.focusMap === true) {
@@ -368,6 +373,7 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
closeReplayPreviewWikiPanel(); closeReplayPreviewWikiPanel();
setPreviewActiveEntityId(null); setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(false); setIsPreviewEntitySidebarOpen(false);
setPreviewRightPanelMode(null);
setPreviewWikiError(null); setPreviewWikiError(null);
setPreviewLinkEntityPopup(null); setPreviewLinkEntityPopup(null);
}, [closeReplayPreviewWikiPanel, setPreviewActiveEntityId]); }, [closeReplayPreviewWikiPanel, setPreviewActiveEntityId]);
@@ -396,7 +402,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
setPreviewLinkEntityPopup(null); setPreviewLinkEntityPopup(null);
if (!payload) { if (!payload) {
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode(null);
return; return;
} }
@@ -406,15 +413,12 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
if (!entity) return []; if (!entity) return [];
const linkedWikis = previewRelations.entityWikisById[entity.id] || []; const linkedWikis = previewRelations.entityWikisById[entity.id] || [];
if (!linkedWikis.length) {
return [{ entity, wiki: null as Wiki | null }];
}
return linkedWikis.map((wiki) => ({ entity, wiki })); return linkedWikis.map((wiki) => ({ entity, wiki }));
}); });
if (!rows.length) { if (!rows.length) {
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode(null);
return; return;
} }
@@ -422,20 +426,28 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
const row = rows[0]; const row = rows[0];
selectReplayPreviewEntity(row.entity.id, { selectReplayPreviewEntity(row.entity.id, {
sourceFeatureId: payload.featureId, sourceFeatureId: payload.featureId,
preferredWikiId: row.wiki?.id, preferredWikiId: row.wiki.id,
focusMap: false, focusMap: false,
selectGeometry: false, selectGeometry: false,
}); });
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode("wiki");
return; return;
} }
setPreviewPinnedWikiPopupAnchor(payload); closeReplayPreviewWikiPanel();
setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(false);
setPreviewWikiError(null);
setPreviewWikiSelectionPanelAnchor(payload);
setPreviewRightPanelMode("selection");
}, [ }, [
closeReplayPreviewWikiPanel,
previewRelations.entitiesById, previewRelations.entitiesById,
previewRelations.entityWikisById, previewRelations.entityWikisById,
previewRelations.geometryEntityIds, previewRelations.geometryEntityIds,
selectReplayPreviewEntity, selectReplayPreviewEntity,
setPreviewActiveEntityId,
]); ]);
// Hover popup content provider // Hover popup content provider
@@ -617,7 +629,7 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
setPreviewActiveEntityId(null); setPreviewActiveEntityId(null);
setIsPreviewEntitySidebarOpen(true); setIsPreviewEntitySidebarOpen(true);
setPreviewWikiError(null); setPreviewWikiError(null);
setPreviewPinnedWikiPopupAnchor(null); setPreviewWikiSelectionPanelAnchor(null);
setPreviewLinkEntityPopup(null); setPreviewLinkEntityPopup(null);
openReplayPreviewWikiPanelById(wiki.id); openReplayPreviewWikiPanelById(wiki.id);
} }
@@ -628,7 +640,7 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
}, [geometryVisibility]); }, [geometryVisibility]);
const computedTimelineStyle = useMemo(() => { const computedTimelineStyle = useMemo(() => {
const rightMargin = (isReplayPreviewWikiSidebarOpen && isLargeScreen) const rightMargin = (isReplayPreviewRightPanelOpen && isLargeScreen)
? previewSidebarWidth + 32 ? previewSidebarWidth + 32
: 18; : 18;
return { return {
@@ -636,31 +648,27 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
right: `${rightMargin}px`, right: `${rightMargin}px`,
transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1)", transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}; };
}, [isReplayPreviewWikiSidebarOpen, isLargeScreen, previewSidebarWidth]); }, [isReplayPreviewRightPanelOpen, isLargeScreen, previewSidebarWidth]);
// Popup PinnedWikiPopup rows // WikiSelectionPanel rows
const previewPinnedWikiPopupRows = useMemo(() => { const previewWikiSelectionPanelRows = useMemo(() => {
if (!previewPinnedWikiPopupAnchor) return []; if (!previewWikiSelectionPanelAnchor) return [];
const entityIds = previewRelations.geometryEntityIds[String(previewPinnedWikiPopupAnchor.featureId)] || []; const entityIds = previewRelations.geometryEntityIds[String(previewWikiSelectionPanelAnchor.featureId)] || [];
return entityIds.flatMap((entityId) => { return entityIds.flatMap((entityId) => {
const entity = previewRelations.entitiesById[entityId] || null; const entity = previewRelations.entitiesById[entityId] || null;
if (!entity) return []; if (!entity) return [];
const linkedWikis = previewRelations.entityWikisById[entity.id] || []; const linkedWikis = previewRelations.entityWikisById[entity.id] || [];
if (!linkedWikis.length) {
return [{ entity, wiki: null as Wiki | null, quote: "" }];
}
return linkedWikis.map((wiki) => ({ return linkedWikis.map((wiki) => ({
entity, entity,
wiki, wiki,
quote: extractWikiBlockquoteText(wiki.content), quote: cleanWikiPreviewQuote(wiki.preview_quote) || extractWikiBlockquoteText(wiki.content),
})); }));
}); });
}, [previewPinnedWikiPopupAnchor, previewRelations]); }, [previewWikiSelectionPanelAnchor, previewRelations]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleFeatureClick: handlePreviewMapFeatureClick, handleFeatureClick: handlePreviewMapFeatureClick,
@@ -730,6 +738,39 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
</aside> </aside>
) : null} ) : null}
{isPreviewWikiChooserOpen ? (
<aside
style={{
position: "absolute",
top: 16,
right: 16,
bottom: 16,
left: isLargeScreen ? "auto" : 16,
width: isLargeScreen ? `min(${previewSidebarWidth}px, calc(100vw - 2rem))` : "auto",
maxWidth: "calc(100vw - 2rem)",
zIndex: 20,
}}
>
<WikiSelectionPanel
rows={previewWikiSelectionPanelRows}
onClose={() => {
setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode(null);
}}
onSelectRow={(entityId, wikiId) => {
setPreviewWikiSelectionPanelAnchor(null);
setPreviewRightPanelMode("wiki");
selectReplayPreviewEntity(entityId, {
sourceFeatureId: previewWikiSelectionPanelAnchor?.featureId,
preferredWikiId: wikiId,
focusMap: false,
selectGeometry: false,
});
}}
/>
</aside>
) : null}
<aside <aside
style={{ style={{
position: "absolute", position: "absolute",
@@ -772,32 +813,6 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
</div> </div>
</aside> </aside>
{previewPinnedWikiPopupAnchor && previewPinnedWikiPopupRows.length > 0 ? (
<PinnedWikiPopup
rows={previewPinnedWikiPopupRows}
featureId={previewPinnedWikiPopupAnchor.featureId}
top={clampNumber(
previewPinnedWikiPopupAnchor.point.y - 8,
16,
typeof window !== "undefined" ? window.innerHeight - 280 : previewPinnedWikiPopupAnchor.point.y - 8
)}
left={clampNumber(
previewPinnedWikiPopupAnchor.point.x + 18,
16,
typeof window !== "undefined" ? window.innerWidth - 340 : previewPinnedWikiPopupAnchor.point.x + 18
)}
onClose={() => setPreviewPinnedWikiPopupAnchor(null)}
onSelectRow={(entityId, wikiId) => {
selectReplayPreviewEntity(entityId, {
sourceFeatureId: previewPinnedWikiPopupAnchor.featureId,
preferredWikiId: wikiId,
focusMap: false,
selectGeometry: false,
});
}}
/>
) : null}
{timelineBarVisible ? ( {timelineBarVisible ? (
<TimelineBar <TimelineBar
year={activeTimelineYear} year={activeTimelineYear}
@@ -847,21 +862,38 @@ export default PreviewLayout;
function extractWikiBlockquoteText(content: string | null | undefined): string { function extractWikiBlockquoteText(content: string | null | undefined): string {
if (!content) return ""; if (!content) return "";
const blockquoteMatch = content.match(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/i); const decoded = decodeHtmlEntities(content);
const blockquoteMatch = decoded.match(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/i);
const rawText = blockquoteMatch?.[1]?.trim() || ""; const rawText = blockquoteMatch?.[1]?.trim() || "";
if (!rawText) return ""; if (!rawText) return "";
return rawText return cleanWikiPlainText(rawText);
}
function cleanWikiPreviewQuote(raw: string | null | undefined): string {
const decoded = decodeHtmlEntities(String(raw || ""));
const blockquote = extractWikiBlockquoteText(decoded);
return cleanWikiPlainText(blockquote || decoded);
}
function cleanWikiPlainText(raw: string): string {
return decodeHtmlEntities(raw)
.replace(/<[^>]*>/g, "") .replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, " ")
.replace(/\u00a0/g, " ") .replace(/\u00a0/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function decodeHtmlEntities(raw: string): string {
return raw
.replace(/&nbsp;/gi, " ")
.replace(/&#160;/gi, " ")
.replace(/&amp;/gi, "&") .replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<") .replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">") .replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"') .replace(/&quot;/gi, '"')
.replace(/&#39;/g, "'") .replace(/&#39;/g, "'")
.replace(/\s+/g, " ") .replace(/&apos;/gi, "'");
.trim();
} }
function getWikiHoverTitle(wiki: { title?: string | null } | null | undefined, fallbackTitle: string): string { function getWikiHoverTitle(wiki: { title?: string | null } | null | undefined, fallbackTitle: string): string {
@@ -9,9 +9,10 @@ const PreviewMapShell = dynamic(
import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay"; import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay";
import MapPlaceholder from "@/uhm/components/preview/MapPlaceholder"; import MapPlaceholder from "@/uhm/components/preview/MapPlaceholder";
import WikiSelectionPanel from "@/uhm/components/preview/WikiSelectionPanel";
import { usePublicPreviewData } from "@/uhm/components/preview/hooks/usePublicPreviewData"; import { usePublicPreviewData } from "@/uhm/components/preview/hooks/usePublicPreviewData";
import { useReplayPreview } from "@/uhm/lib/replay/useReplayPreview"; import { useReplayPreview } from "@/uhm/lib/replay/useReplayPreview";
import type { MapHandle } from "@/uhm/components/Map"; import type { MapFeaturePayload, MapHandle } from "@/uhm/components/Map";
import { useRef, useMemo, useCallback, useState, useEffect } from "react"; import { useRef, useMemo, useCallback, useState, useEffect } from "react";
import { usePublicPreviewInteraction } from "@/uhm/components/preview/hooks/usePublicPreviewInteraction"; import { usePublicPreviewInteraction } from "@/uhm/components/preview/hooks/usePublicPreviewInteraction";
import PresentPlaceSearch, { import PresentPlaceSearch, {
@@ -89,6 +90,8 @@ export default function PublicPreviewClientPage({
const [isLargeScreen, setIsLargeScreen] = useState(false); const [isLargeScreen, setIsLargeScreen] = useState(false);
const [loadInteractiveMap, setLoadInteractiveMap] = useState(false); const [loadInteractiveMap, setLoadInteractiveMap] = useState(false);
const [isLayerPanelVisible, setIsLayerPanelVisible] = useState(true); const [isLayerPanelVisible, setIsLayerPanelVisible] = useState(true);
const [wikiSelectionPanelAnchor, setWikiSelectionPanelAnchor] = useState<MapFeaturePayload | null>(null);
const [rightPanelMode, setRightPanelMode] = useState<"wiki" | "selection" | null>(null);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -205,6 +208,7 @@ export default function PublicPreviewClientPage({
selectWiki, selectWiki,
handleWikiLinkRequest, handleWikiLinkRequest,
closeWikiSidebar, closeWikiSidebar,
closeWikiSidebarPreserveSelection,
setLinkEntityPopup, setLinkEntityPopup,
isManualSidebarOpen, isManualSidebarOpen,
} = usePublicPreviewInteraction({ } = usePublicPreviewInteraction({
@@ -375,6 +379,8 @@ export default function PublicPreviewClientPage({
const handleFocusWiki = useCallback((wiki: Wiki) => { const handleFocusWiki = useCallback((wiki: Wiki) => {
setFocusedPresentPlace(null); setFocusedPresentPlace(null);
setWikiSelectionPanelAnchor(null);
setRightPanelMode("wiki");
selectWiki(wiki); selectWiki(wiki);
// Focus geometries if any // Focus geometries if any
@@ -390,6 +396,77 @@ export default function PublicPreviewClientPage({
} }
}, [relations.wikiEntityIdsById, relations.entityGeometriesById, selectWiki]); }, [relations.wikiEntityIdsById, relations.entityGeometriesById, selectWiki]);
const handleCloseWikiSidebar = useCallback(() => {
setRightPanelMode(null);
setWikiSelectionPanelAnchor(null);
closeWikiSidebar();
}, [closeWikiSidebar]);
const wikiSelectionPanelRows = useMemo(() => {
if (!wikiSelectionPanelAnchor) return [];
const entityIds = relations.geometryEntityIds[String(wikiSelectionPanelAnchor.featureId)] || [];
return entityIds.flatMap((entityId) => {
const entity = relations.entitiesById[entityId] || null;
if (!entity) return [];
const linkedWikis = relations.entityWikisById[entity.id] || [];
return linkedWikis.map((wiki) => ({
entity,
wiki,
quote: cleanWikiPreviewQuote(wiki.preview_quote) || extractWikiBlockquoteText(wiki.content),
}));
});
}, [wikiSelectionPanelAnchor, relations.entitiesById, relations.entityWikisById, relations.geometryEntityIds]);
const handleMapFeatureClick = useCallback((payload: MapFeaturePayload | null) => {
setLinkEntityPopup(null);
if (!payload) {
setWikiSelectionPanelAnchor(null);
setRightPanelMode(null);
return;
}
const entityIds = relations.geometryEntityIds[String(payload.featureId)] || [];
const rows = entityIds.flatMap((entityId) => {
const entity = relations.entitiesById[entityId] || null;
if (!entity) return [];
const linkedWikis = relations.entityWikisById[entity.id] || [];
return linkedWikis.map((wiki) => ({ entity, wiki }));
});
if (!rows.length) {
setWikiSelectionPanelAnchor(null);
setRightPanelMode(null);
return;
}
if (rows.length === 1) {
const row = rows[0];
selectEntity(row.entity.id, {
sourceFeatureId: payload.featureId,
preferredWikiSlug: row.wiki.slug,
selectGeometry: false,
});
setWikiSelectionPanelAnchor(null);
setRightPanelMode("wiki");
return;
}
closeWikiSidebarPreserveSelection();
setWikiSelectionPanelAnchor(payload);
setRightPanelMode("selection");
}, [
closeWikiSidebarPreserveSelection,
relations.entitiesById,
relations.entityWikisById,
relations.geometryEntityIds,
selectEntity,
setLinkEntityPopup,
]);
const filteredRenderDraft = useMemo(() => { const filteredRenderDraft = useMemo(() => {
if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) { if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) {
return renderDraft; return renderDraft;
@@ -428,24 +505,26 @@ export default function PublicPreviewClientPage({
return `Stage #${replayPreview.activeCursor.stageId} · Step ${replayPreview.activeCursor.stepIndex + 1}`; return `Stage #${replayPreview.activeCursor.stageId} · Step ${replayPreview.activeCursor.stepIndex + 1}`;
}, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]); }, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]);
const isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor);
const isSidebarOpen = replayMode === "playing" const isSidebarOpen = replayMode === "playing"
? (replayPreview.sidebarOpen || isManualSidebarOpen) ? (replayPreview.sidebarOpen || isManualSidebarOpen)
: Boolean(activeEntity || activeWiki || isManualSidebarOpen); : Boolean(activeEntity || activeWiki || isManualSidebarOpen);
const displayedActiveEntity = isSidebarOpen ? activeEntity : null; const displayedActiveEntity = rightPanelMode !== "selection" && isSidebarOpen ? activeEntity : null;
const displayedActiveWiki = isSidebarOpen ? activeWiki : null; const displayedActiveWiki = rightPanelMode !== "selection" && isSidebarOpen ? activeWiki : null;
const computedTimelineStyle = useMemo(() => { const computedTimelineStyle = useMemo(() => {
const leftMargin = isLayerPanelVisible ? 88 : 18; const leftMargin = isLayerPanelVisible ? 88 : 18;
const rightMargin = ((displayedActiveEntity || displayedActiveWiki) && isLargeScreen) ? sidebarWidth + 32 : 18; const rightPanelOpen = Boolean(displayedActiveEntity || displayedActiveWiki || isWikiChooserOpen);
const bottomOffset = ((displayedActiveEntity || displayedActiveWiki) && !isLargeScreen) ? `${sidebarHeight + 16}px` : undefined; const rightMargin = (rightPanelOpen && isLargeScreen) ? sidebarWidth + 32 : 18;
const bottomOffset = (rightPanelOpen && !isLargeScreen) ? `${sidebarHeight + 16}px` : undefined;
return { return {
left: `${leftMargin}px`, left: `${leftMargin}px`,
right: `${rightMargin}px`, right: `${rightMargin}px`,
bottom: bottomOffset, bottom: bottomOffset,
transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1), bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1)", transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1), bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}; };
}, [isLayerPanelVisible, displayedActiveEntity, displayedActiveWiki, isLargeScreen, sidebarWidth, sidebarHeight]); }, [isLayerPanelVisible, displayedActiveEntity, displayedActiveWiki, isWikiChooserOpen, isLargeScreen, sidebarWidth, sidebarHeight]);
const searchBarWidth = useMemo(() => { const searchBarWidth = useMemo(() => {
if (isLargeScreen) { if (isLargeScreen) {
@@ -513,13 +592,14 @@ export default function PublicPreviewClientPage({
isTimelineLoading={isTimelineLoading || isRelationsLoading} isTimelineLoading={isTimelineLoading || isRelationsLoading}
timelineStatusText={relationsStatus || timelineStatus} timelineStatusText={relationsStatus || timelineStatus}
timelineStyle={computedTimelineStyle} timelineStyle={computedTimelineStyle}
onFeatureClick={handleMapFeatureClick}
hoverPopupEnabled hoverPopupEnabled
getHoverPopupContent={getHoverPopupContent} getHoverPopupContent={getHoverPopupContent}
activeEntity={displayedActiveEntity} activeEntity={displayedActiveEntity}
activeWiki={displayedActiveWiki} activeWiki={displayedActiveWiki}
isWikiLoading={isActiveWikiLoading} isWikiLoading={isActiveWikiLoading}
wikiError={activeWikiError} wikiError={activeWikiError}
onCloseWikiSidebar={closeWikiSidebar} onCloseWikiSidebar={handleCloseWikiSidebar}
onWikiLinkRequest={handleWikiLinkRequest} onWikiLinkRequest={handleWikiLinkRequest}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
onSidebarWidthChange={setSidebarWidth} onSidebarWidthChange={setSidebarWidth}
@@ -604,6 +684,8 @@ export default function PublicPreviewClientPage({
key={entity.id} key={entity.id}
type="button" type="button"
onClick={() => { onClick={() => {
setWikiSelectionPanelAnchor(null);
setRightPanelMode("wiki");
selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug }); selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug });
setLinkEntityPopup(null); setLinkEntityPopup(null);
}} }}
@@ -616,6 +698,76 @@ export default function PublicPreviewClientPage({
</div> </div>
</div> </div>
) : null} ) : null}
{isWikiChooserOpen ? (
<aside
className={isLargeScreen ? "fixed bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]" : "fixed bottom-0 left-0 right-0 top-auto z-20"}
style={isLargeScreen ? {
width: `min(${sidebarWidth}px, calc(100vw - 2rem))`,
} : {
height: `${sidebarHeight || 400}px`,
maxHeight: "90vh",
width: "100%",
maxWidth: "100%",
}}
>
<WikiSelectionPanel
rows={wikiSelectionPanelRows}
onClose={() => {
setWikiSelectionPanelAnchor(null);
setRightPanelMode(null);
}}
onSelectRow={(entityId, wikiId) => {
const wiki = wikiSelectionPanelRows.find((row) => row.entity.id === entityId && row.wiki.id === wikiId)?.wiki || null;
const sourceFeatureId = wikiSelectionPanelAnchor?.featureId ?? null;
setWikiSelectionPanelAnchor(null);
setRightPanelMode("wiki");
selectEntity(entityId, {
sourceFeatureId,
preferredWikiSlug: wiki?.slug,
selectGeometry: false,
});
}}
/>
</aside>
) : null}
</> </>
); );
} }
function cleanWikiPreviewQuote(raw: string | null | undefined): string {
const decoded = decodeHtmlEntities(String(raw || ""));
const blockquote = extractWikiBlockquoteText(decoded);
return cleanWikiPlainText(blockquote || decoded);
}
function extractWikiBlockquoteText(content: string | null | undefined): string {
if (!content) return "";
const decoded = decodeHtmlEntities(content);
const blockquoteMatch = decoded.match(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/i);
const rawText = blockquoteMatch?.[1]?.trim() || "";
if (!rawText) return "";
return cleanWikiPlainText(rawText);
}
function cleanWikiPlainText(raw: string): string {
return decodeHtmlEntities(raw)
.replace(/<[^>]*>/g, "")
.replace(/\u00a0/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function decodeHtmlEntities(raw: string): string {
return raw
.replace(/&nbsp;/gi, " ")
.replace(/&#160;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/gi, "'");
}
@@ -0,0 +1,230 @@
"use client";
import { useEffect, useRef } from "react";
import type { Entity } from "@/uhm/api/entities";
import type { Wiki } from "@/uhm/api/wikis";
type WikiSelectionRow = {
entity: Entity;
wiki: Wiki;
quote: string;
};
type Props = {
rows: WikiSelectionRow[];
onClose: () => void;
onSelectRow: (entityId: string, wikiId: string) => void;
};
export default function WikiSelectionPanel({
rows,
onClose,
onSelectRow,
}: Props) {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div
ref={containerRef}
style={{
width: "100%",
maxWidth: "100%",
display: "flex",
flexDirection: "column",
height: "100%",
minHeight: 0,
overflow: "hidden",
borderRadius: 20,
border: "1px solid rgba(148, 163, 184, 0.22)",
background: "linear-gradient(145deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.85))",
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.45)",
backdropFilter: "blur(12px)",
position: "relative",
}}
>
<div
style={{
borderBottom: "1px solid rgba(148, 163, 184, 0.15)",
padding: "16px",
}}
>
<div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 12 }}>
<div style={{ minWidth: 0, flex: 1 }}>
<div
style={{
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "1.2px",
fontWeight: 900,
color: "#94a3b8",
}}
>
Wiki
</div>
<div
style={{
marginTop: 4,
fontSize: 18,
fontWeight: 700,
lineHeight: 1.3,
color: "#f8fafc",
}}
>
Chọn wiki đ mở
</div>
</div>
<button
type="button"
onClick={onClose}
style={{
display: "inline-flex",
height: 28,
width: 28,
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
border: "1px solid rgba(148, 163, 184, 0.25)",
background: "rgba(30, 41, 59, 0.4)",
color: "#94a3b8",
cursor: "pointer",
fontSize: 12,
transition: "all 0.2s",
outline: "none",
}}
className="hover:bg-slate-700/50 hover:text-slate-100"
aria-label="Close wiki chooser"
>
x
</button>
</div>
</div>
<div className="uhm-pinned-wiki-panel-scroll" style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: 16 }}>
<div style={{ display: "grid", gap: 10 }}>
{rows.map(({ entity, wiki, quote }, index) => {
const previous = rows[index - 1];
const startsEntityGroup = !previous || previous.entity.id !== entity.id;
return (
<div key={`${entity.id}:${wiki.id}`}>
{startsEntityGroup ? (
<div
style={{
paddingTop: index > 0 ? 12 : 0,
marginTop: index > 0 ? 4 : 0,
borderTop: index > 0 ? "1px solid rgba(148, 163, 184, 0.16)" : "none",
}}
>
<div style={{ fontSize: 14, fontWeight: 800, color: "#f8fafc", lineHeight: "20px" }}>
{entity.name || String(entity.id)}
</div>
{entity.description?.trim() ? (
<div
style={{
marginTop: 4,
fontSize: 12,
lineHeight: "17px",
color: "#94a3b8",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{entity.description.trim()}
</div>
) : null}
</div>
) : null}
<button
type="button"
onClick={() => onSelectRow(entity.id, wiki.id)}
style={{
width: "100%",
marginTop: 6,
padding: "9px 10px 9px 12px",
border: "1px solid transparent",
borderRadius: 10,
background: "rgba(15, 23, 42, 0.34)",
boxShadow: "inset 2px 0 0 rgba(56, 189, 248, 0.52)",
textAlign: "left",
cursor: "pointer",
transition: "background 0.15s ease, border-color 0.15s ease",
}}
className="hover:border-sky-400/30 hover:bg-sky-500/10"
>
<div
style={{
fontSize: 13,
fontWeight: 800,
lineHeight: "19px",
color: "#e2e8f0",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
}}
>
{wiki.title?.trim() || entity.name || String(wiki.id)}
</div>
{quote ? (
<div
style={{
marginTop: 6,
paddingLeft: 10,
borderLeft: "2px solid rgba(56, 189, 248, 0.48)",
fontSize: 12.5,
fontStyle: "italic",
lineHeight: "18px",
color: "#cbd5e1",
display: "-webkit-box",
WebkitLineClamp: 4,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "normal",
overflowWrap: "anywhere",
wordBreak: "break-word",
}}
>
{quote}
</div>
) : null}
</button>
</div>
);
})}
</div>
</div>
<style jsx>{`
.uhm-pinned-wiki-panel-scroll {
scrollbar-width: thin;
scrollbar-color: rgba(56, 189, 248, 0.58) rgba(15, 23, 42, 0.72);
}
.uhm-pinned-wiki-panel-scroll::-webkit-scrollbar {
width: 9px;
}
.uhm-pinned-wiki-panel-scroll::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.72);
}
.uhm-pinned-wiki-panel-scroll::-webkit-scrollbar-thumb {
border: 2px solid rgba(15, 23, 42, 0.95);
border-radius: 999px;
background: linear-gradient(180deg, rgba(56, 189, 248, 0.86), rgba(14, 165, 233, 0.58));
}
`}</style>
</div>
);
}
@@ -217,12 +217,15 @@ export function usePublicPreviewInteraction(options: {
const onlyEntityId = linkedEntityIds[0]; const onlyEntityId = linkedEntityIds[0];
if (activeEntityId === onlyEntityId) return; if (activeEntityId === onlyEntityId) return;
const linkedWikis = relations.entityWikisById[onlyEntityId] || [];
if (linkedWikis.length !== 1) return;
selectEntity(onlyEntityId, { selectEntity(onlyEntityId, {
sourceFeatureId: selectedFeatureIds[0], sourceFeatureId: selectedFeatureIds[0],
preferredWikiSlug: linkedWikis[0]?.slug,
selectGeometry: false, selectGeometry: false,
}); });
}, [activeEntityId, relations.geometryEntityIds, selectEntity, selectedFeatureIds]); }, [activeEntityId, relations.entityWikisById, relations.geometryEntityIds, selectEntity, selectedFeatureIds]);
const loadHoverWikiPreviewForEntity = useCallback(async (entityId: string) => { const loadHoverWikiPreviewForEntity = useCallback(async (entityId: string) => {
try { try {
@@ -520,6 +523,14 @@ export function usePublicPreviewInteraction(options: {
setIsManualSidebarOpen(false); setIsManualSidebarOpen(false);
}, [setSelectedFeatureIds]); }, [setSelectedFeatureIds]);
const closeWikiSidebarPreserveSelection = useCallback(() => {
setActiveEntityId(null);
setActiveWikiSlug(null);
setActiveWikiError(null);
setLinkEntityPopup(null);
setIsManualSidebarOpen(false);
}, []);
return { return {
activeEntity, activeEntity,
activeWiki, activeWiki,
@@ -532,6 +543,7 @@ export function usePublicPreviewInteraction(options: {
selectWiki, selectWiki,
handleWikiLinkRequest, handleWikiLinkRequest,
closeWikiSidebar, closeWikiSidebar,
closeWikiSidebarPreserveSelection,
setLinkEntityPopup, setLinkEntityPopup,
isManualSidebarOpen, isManualSidebarOpen,
setIsManualSidebarOpen, setIsManualSidebarOpen,
+1 -24
View File
@@ -441,31 +441,8 @@ function PublicWikiSidebar({
color: "#f8fafc", color: "#f8fafc",
}} }}
> >
{entity?.name?.trim() || wiki?.title?.trim() || "Wiki"} {wiki?.title?.trim() || entity?.name?.trim() || "Wiki"}
</div> </div>
{entity?.description?.trim() ? (
<div
style={{
marginTop: 8,
fontSize: 13,
lineHeight: 1.5,
color: "#cbd5e1",
}}
>
{entity.description.trim()}
</div>
) : null}
{!compactHeader && wiki?.title?.trim() && wiki.title.trim() !== entity?.name?.trim() ? (
<div
style={{
marginTop: 6,
fontSize: 12,
color: "#94a3b8",
}}
>
{wiki.title.trim()}
</div>
) : null}
</div> </div>
<button <button