Spaces:
Sleeping
Sleeping
| import React, { useState, useRef } from 'react'; | |
| import { Box, Fab, Slide, useMediaQuery, useTheme } from '@mui/material'; | |
| import ChatIcon from '@mui/icons-material/Chat'; | |
| import CloseIcon from '@mui/icons-material/Close'; | |
| import ChatInterface from './ChatInterface'; | |
| type FloatingChatProps = { | |
| onClose: () => void; | |
| }; | |
| const FloatingChat: React.FC<FloatingChatProps> = ({ onClose }) => { | |
| const [isOpen, setIsOpen] = useState(true); | |
| const handleClose = () => { | |
| setIsOpen(false); | |
| onClose(); | |
| }; | |
| const theme = useTheme(); | |
| const isMobile = useMediaQuery(theme.breakpoints.down('sm')); | |
| const chatRef = useRef<HTMLDivElement>(null); | |
| const toggleChat = () => setIsOpen(prev => !prev); | |
| const ANIM_DURATION = 300; | |
| const FAB_SIZE = 60; | |
| const FAB_GAP = isMobile ? 16 : 24; | |
| return ( | |
| <Box | |
| sx={{ | |
| position: 'fixed', | |
| bottom: 0, | |
| right: 0, | |
| zIndex: 1400, | |
| pointerEvents: 'none', // children control pointerEvents individually | |
| }} | |
| > | |
| {/* Slide wrapper: NOTE -> overflow must be visible so internal header isn't clipped */} | |
| <Slide | |
| direction="up" | |
| in={isOpen} | |
| mountOnEnter | |
| unmountOnExit | |
| timeout={ANIM_DURATION} | |
| > | |
| <Box | |
| ref={chatRef} | |
| // sx={{ | |
| // position: 'fixed', | |
| // right: isMobile ? FAB_GAP : FAB_GAP, | |
| // bottom: FAB_GAP, | |
| // width: isMobile ? '0vw' : 1410, | |
| // height: isMobile ? '0vh' : 1410, | |
| // maxHeight: 'calc(100vh - 48px)', | |
| // borderRadius: 2, | |
| // // Crucial: allow overflow visible so header (and shadows) aren't clipped during transform | |
| // overflow: 'visible', | |
| // display: 'flex', | |
| // flexDirection: 'column', | |
| // boxShadow: 12, | |
| // backgroundColor: theme.palette.background.paper, | |
| // zIndex: 1410, | |
| // pointerEvents: 'auto', | |
| // // Help browser optimize the transform during animation | |
| // willChange: 'transform, opacity', | |
| // transformOrigin: 'bottom right', | |
| // }} | |
| sx={{ | |
| position: 'fixed', | |
| top: 0, | |
| left: 0, | |
| width: '100vw', | |
| height: '100vh', | |
| borderRadius: 0, // remove rounded corners since it's full screen | |
| overflow: 'hidden', // keeps ChatInterface properly contained | |
| display: 'flex', | |
| flexDirection: 'column', | |
| boxShadow: 'none', // optional, remove shadow since it's full viewport | |
| backgroundColor: theme.palette.background.paper, | |
| zIndex: 1410, | |
| pointerEvents: 'auto', | |
| willChange: 'transform, opacity', | |
| transformOrigin: 'bottom right', | |
| }} | |
| > | |
| <ChatInterface onClose={handleClose} /> | |
| </Box> | |
| </Slide> | |
| {/* FAB wrapper (keeps FAB mounted during transition) */} | |
| <Box | |
| sx={{ | |
| position: 'fixed', | |
| right: isMobile ? FAB_GAP : FAB_GAP, | |
| bottom: FAB_GAP, | |
| zIndex: 1405, | |
| pointerEvents: 'none', | |
| }} | |
| > | |
| <Fab | |
| color="primary" | |
| aria-label="chat" | |
| onClick={toggleChat} | |
| sx={{ | |
| width: FAB_SIZE, | |
| height: FAB_SIZE, | |
| backgroundColor: theme.palette.primary.main, | |
| color: theme.palette.primary.contrastText, | |
| '&:hover': { backgroundColor: theme.palette.primary.dark }, | |
| transition: `transform ${ANIM_DURATION}ms ease, opacity ${ANIM_DURATION}ms ease`, | |
| pointerEvents: isOpen ? 'none' : 'auto', // make non-clickable while chat is open | |
| opacity: isOpen ? 0 : 1, | |
| transform: isOpen ? 'translateY(12px) scale(0.98)' : 'translateY(0) scale(1)', | |
| boxShadow: 6, | |
| }} | |
| > | |
| {isOpen ? <CloseIcon /> : <ChatIcon fontSize="large" />} | |
| </Fab> | |
| </Box> | |
| </Box> | |
| ); | |
| }; | |
| export default FloatingChat; | |