RSS Git Download  Clone
Raw Blame History 7kB 167 lines
import { useEffect, useRef, useState, useCallback } from 'react'
import { Box, useTheme } from '@mui/material'
import { useRedisStateStore } from '../stores/redis-state.store'
import ConsoleComponent from '../pages/console/ConsoleComponent'

const HEIGHT_KEY = 'p3xr-console-drawer-height'
const MIN_VH = 15
const MAX_VH = 66
const FOOTER_HEIGHT = 48

/**
 * Global bottom console drawer — always renders the full ConsoleComponent.
 * The welcome banner inside the console adapts to connectionState (connected
 * vs limited-AI). This keeps the toolbar, input, Clear / Commands always
 * usable — disconnected users can still type `ai: what is ZADD?` or eventually
 * `connect <name>` without losing the chrome.
 *
 * Top 5px grab strip resizes the drawer between MIN_VH and MAX_VH, persisted
 * to localStorage. A ResizeObserver dispatches window.resize on every frame
 * the drawer's height changes so pages that read --p3xr-console-drawer-height-active
 * re-layout live during drag and during open/close transition.
 */
export default function ConsoleDrawer() {
    const isOpen = useRedisStateStore(s => s.consoleDrawerOpen)
    const setConsoleDrawerOpen = useRedisStateStore(s => s.setConsoleDrawerOpen)
    const muiTheme = useTheme()
    const isDark = muiTheme.palette.mode === 'dark'

    const [resizeClicked, setResizeClicked] = useState(false)
    const [sizerHover, setSizerHover] = useState(false)
    const drawerRef = useRef<HTMLDivElement>(null)
    const sizerRef = useRef<HTMLDivElement>(null)
    const dragStyleElRef = useRef<HTMLStyleElement | null>(null)

    const applyDragCursor = useCallback((cursor: 'ns-resize' | 'not-allowed') => {
        let el = dragStyleElRef.current
        if (!el) {
            el = document.createElement('style')
            el.setAttribute('data-p3xr-console-drawer-drag', '')
            document.head.appendChild(el)
            dragStyleElRef.current = el
        }
        el.textContent = `*, *::before, *::after { cursor: ${cursor} !important; }`
    }, [])

    const clearDragCursor = useCallback(() => {
        dragStyleElRef.current?.remove()
        dragStyleElRef.current = null
    }, [])

    // Saved height is applied at bootstrap (src/core/console-drawer-height.ts)
    // so it's in place before this component mounts.

    // Observe the drawer element — fires on every size change frame
    // (open/close height transition + live drag). Listeners on window.resize
    // (profiler/pubsub page height calc) pick it up.
    useEffect(() => {
        if (!drawerRef.current || typeof ResizeObserver === 'undefined') return
        const obs = new ResizeObserver(() => {
            window.dispatchEvent(new Event('resize'))
        })
        obs.observe(drawerRef.current)
        return () => obs.disconnect()
    }, [])

    // Drag handlers — document-level so the drag continues outside the sizer
    useEffect(() => {
        if (!resizeClicked) return

        const handleMouseMove = (e: MouseEvent) => {
            const minPx = (MIN_VH / 100) * window.innerHeight
            const maxPx = (MAX_VH / 100) * window.innerHeight
            let newHeight = window.innerHeight - e.clientY - FOOTER_HEIGHT
            const outOfBounds = newHeight < minPx || newHeight > maxPx
            if (newHeight < minPx) newHeight = minPx
            if (newHeight > maxPx) newHeight = maxPx
            applyDragCursor(outOfBounds ? 'not-allowed' : 'ns-resize')
            document.documentElement.style.setProperty('--p3xr-console-drawer-height', `${Math.round(newHeight)}px`)
        }

        const handleMouseUp = () => {
            setResizeClicked(false)
            clearDragCursor()
            document.body.classList.remove('p3xr-not-selectable')
            document.documentElement.classList.remove('p3xr-console-drawer-resizing')
            const current = document.documentElement.style.getPropertyValue('--p3xr-console-drawer-height')
            if (current && current.endsWith('px')) {
                localStorage.setItem(HEIGHT_KEY, current)
            }
        }

        document.addEventListener('mousemove', handleMouseMove)
        document.addEventListener('mouseup', handleMouseUp)
        return () => {
            document.removeEventListener('mousemove', handleMouseMove)
            document.removeEventListener('mouseup', handleMouseUp)
        }
    }, [resizeClicked, applyDragCursor, clearDragCursor])

    const handleSizerMouseDown = useCallback((e: React.MouseEvent) => {
        e.preventDefault()
        e.stopPropagation()
        setResizeClicked(true)
        applyDragCursor('ns-resize')
        document.body.classList.add('p3xr-not-selectable')
        document.documentElement.classList.add('p3xr-console-drawer-resizing')
    }, [applyDragCursor])

    const sizerFilter = resizeClicked
        ? (isDark ? 'brightness(2)' : 'brightness(0.5)')
        : sizerHover
            ? (isDark ? 'brightness(1.5)' : 'brightness(0.75)')
            : 'none'

    return (
        <Box
            ref={drawerRef}
            id="p3xr-console-drawer"
            className={isOpen ? 'p3xr-drawer-open' : ''}
            sx={{
                position: 'fixed',
                left: '5px',
                right: 'calc(5px + var(--p3xr-scroll-gutter, 0px))',
                bottom: '48px',
                height: isOpen ? 'var(--p3xr-console-drawer-height, 30vh)' : 0,
                overflow: 'hidden',
                bgcolor: 'background.paper',
                color: 'text.primary',
                border: isOpen ? '1px solid' : '0 solid',
                borderColor: (muiTheme as any).p3xr?.accordionBg,
                borderRadius: '4px 4px 0 0',
                zIndex: 8,
                transition: resizeClicked ? 'none' : 'height 150ms ease-out',
                display: 'flex',
                flexDirection: 'column',
            }}
        >
            <Box
                ref={sizerRef}
                id="p3xr-console-drawer-sizer"
                role="separator"
                aria-orientation="horizontal"
                aria-label="Resize console drawer"
                onMouseDown={handleSizerMouseDown}
                onMouseEnter={() => setSizerHover(true)}
                onMouseLeave={() => setSizerHover(false)}
                sx={{
                    position: 'absolute',
                    top: 0, left: 0, right: 0,
                    height: '5px',
                    cursor: 'ns-resize',
                    zIndex: 3,
                    bgcolor: (sizerHover || resizeClicked) ? (muiTheme as any).p3xr?.accordionBg : 'transparent',
                    filter: sizerFilter,
                    transition: 'background-color 0.15s ease, filter 0.15s ease',
                }}
            />
            <Box id="p3xr-console-drawer-body" sx={{
                flex: '1 1 auto', minHeight: 0, overflow: 'hidden', position: 'relative',
            }}>
                <ConsoleComponent embedded showCloseButton
                                  onCloseRequest={() => setConsoleDrawerOpen(false)} />
            </Box>
        </Box>
    )
}