155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ChatDock — floating right-docked wrapper around <ChatPanel> for the Site
|
||
* Finder analysis report (#958 follow-up).
|
||
*
|
||
* The chat used to be mounted inline at the bottom of the report; that meant the
|
||
* user had to scroll all the way down to ask anything. ChatDock keeps it
|
||
* always reachable: a fixed launcher (bottom-right) opens a fixed popover panel
|
||
* that stays put while the report scrolls.
|
||
*
|
||
* Open/close:
|
||
* - launcher click → toggles the panel open
|
||
* - close button (X) → closes
|
||
* - Esc → closes (keydown listener active only while open, cleaned up)
|
||
*
|
||
* Double-card avoidance: <ChatPanel> already renders its own bordered <section>
|
||
* card with the «Чат по участку» header. So the dock's own chrome is just a thin
|
||
* header bar carrying the X close button (NO duplicate title) — ChatPanel's own
|
||
* header provides the title inside the scrollable body. No nested boxes.
|
||
*
|
||
* box-shadow is allowed here (floating popover / launcher), unlike in-page cards.
|
||
*/
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { MessageSquare, X } from "lucide-react";
|
||
|
||
import { ChatPanel } from "@/components/site-finder/ChatPanel";
|
||
|
||
// ── Props ─────────────────────────────────────────────────────────────────────
|
||
|
||
interface Props {
|
||
cadNum: string;
|
||
}
|
||
|
||
// ── ChatDock ───────────────────────────────────────────────────────────────────
|
||
|
||
export function ChatDock({ cadNum }: Props) {
|
||
const [open, setOpen] = useState(false);
|
||
|
||
// Esc closes the panel (listener active only while open).
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
function onKeyDown(e: KeyboardEvent) {
|
||
if (e.key === "Escape") setOpen(false);
|
||
}
|
||
window.addEventListener("keydown", onKeyDown);
|
||
return () => window.removeEventListener("keydown", onKeyDown);
|
||
}, [open]);
|
||
|
||
// ── Collapsed: floating launcher ─────────────────────────────────────────────
|
||
|
||
if (!open) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpen(true)}
|
||
aria-label="Открыть чат по участку"
|
||
aria-expanded={false}
|
||
style={{
|
||
position: "fixed",
|
||
right: 24,
|
||
bottom: 24,
|
||
zIndex: 1000,
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
padding: "12px 16px",
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
color: "var(--fg-on-dark, #E2E8F0)",
|
||
background: "var(--accent, #1D4ED8)",
|
||
border: "1px solid transparent",
|
||
borderRadius: 12,
|
||
cursor: "pointer",
|
||
boxShadow: "0 8px 24px rgba(15, 23, 42, 0.18)",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
<MessageSquare size={20} strokeWidth={1.5} aria-hidden />
|
||
Спросить по участку
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// ── Expanded: floating popover panel ─────────────────────────────────────────
|
||
|
||
return (
|
||
<div
|
||
role="dialog"
|
||
aria-label="Чат по участку"
|
||
style={{
|
||
position: "fixed",
|
||
right: 24,
|
||
bottom: 24,
|
||
zIndex: 1000,
|
||
width: "min(420px, calc(100vw - 32px))",
|
||
height: "min(640px, calc(100vh - 120px))",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
overflow: "hidden",
|
||
background: "var(--bg-card, #FFFFFF)",
|
||
border: "1px solid var(--border-card, #E6E8EC)",
|
||
borderRadius: 12,
|
||
boxShadow: "0 16px 40px rgba(15, 23, 42, 0.24)",
|
||
}}
|
||
>
|
||
{/* Thin header bar — close affordance only. The title lives in ChatPanel's
|
||
own header below, so there's no duplicate heading / nested card. */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "flex-end",
|
||
alignItems: "center",
|
||
padding: "8px 12px",
|
||
borderBottom: "1px solid var(--border-soft, #EEF0F3)",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpen(false)}
|
||
aria-label="Закрыть чат"
|
||
style={{
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
width: 32,
|
||
height: 32,
|
||
padding: 0,
|
||
background: "none",
|
||
border: "none",
|
||
borderRadius: 8,
|
||
cursor: "pointer",
|
||
color: "var(--fg-secondary, #5B6066)",
|
||
transition: "background 0.1s",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.background = "var(--bg-card-alt, #FAFBFC)";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.background = "none";
|
||
}}
|
||
>
|
||
<X size={20} strokeWidth={1.5} aria-hidden />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Scrollable body — ChatPanel renders normally (its own card + header). */}
|
||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||
<ChatPanel cadNum={cadNum} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|