gendesign/frontend/src/components/site-finder/ChatDock.tsx
bot-backend 02f49e795c
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m20s
Deploy / deploy (push) Successful in 1m16s
feat(report): nav scrollspy + offset-jump, чат в плавающее окно справа (#958) (#2104)
2026-06-30 10:29:02 +00:00

155 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}