import { type CSSObject, type Theme, styled } from '@mui/material/styles'; import React, { memo, useEffect, useState } from 'react'; const SHADOW_SIZE = 10; const EditorAreaDecoration = styled('div')({ position: 'absolute', pointerEvents: 'none', }); function shadowTheme( origin: string, scaleX: boolean, scaleY: boolean, ): CSSObject { function radialGradient(opacity: number, scale: string): string { return `radial-gradient( farthest-side at ${origin}, rgba(0, 0, 0, ${opacity}), rgba(0, 0, 0, 0) ) ${origin} / ${scaleX ? scale : '100%'} ${scaleY ? scale : '100%'} no-repeat`; } return { background: ` ${radialGradient(0.2, '40%')}, ${radialGradient(0.14, '50%')}, ${radialGradient(0.12, '100%')} `, }; } function animateSize( theme: Theme, direction: 'height' | 'width', opacity: number, ): CSSObject { return { [direction]: opacity * SHADOW_SIZE, transition: theme.transitions.create(direction, { duration: theme.transitions.duration.shortest, easing: theme.transitions.easing.sharp, }), }; } const TopDecoration = memo( styled(EditorAreaDecoration, { shouldForwardProp: (prop) => prop !== 'visible' && prop !== 'opacity', })<{ visible: boolean; opacity: number; }>(({ theme, visible, opacity }) => ({ display: visible ? 'block' : 'none', top: 0, left: 0, right: 0, ...shadowTheme('50% 0', false, true), ...animateSize(theme, 'height', opacity), })), ); const GutterDecoration = memo( styled(EditorAreaDecoration, { shouldForwardProp: (prop) => prop !== 'top' && prop !== 'bottom' && prop !== 'guttersWidth' && prop !== 'opacity', })<{ top: number; bottom: number; guttersWidth: number; opacity: number; }>(({ theme, top, bottom, guttersWidth, opacity }) => ({ top, left: guttersWidth, bottom, ...shadowTheme('0 50%', true, false), ...animateSize(theme, 'width', opacity), })), ); function convertToOpacity(scroll: number): number { return Math.max(0, Math.min(1, scroll / SHADOW_SIZE)); } export default function EditorAreaDecorations({ parent, scroller, }: { parent: HTMLElement | undefined; scroller: HTMLElement | undefined; }): JSX.Element { const [top, setTop] = useState(0); const [bottom, setBottom] = useState(0); const [guttersWidth, setGuttersWidth] = useState(0); const [topOpacity, setTopOpacity] = useState(0); const [gutterOpacity, setGutterOpacity] = useState(0); useEffect(() => { if (parent === undefined || scroller === undefined) { return () => {}; } const gutters = scroller.querySelector('.cm-gutters'); const updateBounds = () => { const parentRect = parent.getBoundingClientRect(); const rect = scroller.getBoundingClientRect(); setTop(rect.top - parentRect.top); setBottom(parentRect.bottom - rect.bottom); setGuttersWidth(gutters?.clientWidth ?? 0); }; updateBounds(); const resizeObserver = new ResizeObserver(updateBounds); resizeObserver.observe(scroller); if (gutters !== null) { resizeObserver.observe(gutters); } const updateScroll = () => { setTopOpacity(convertToOpacity(scroller.scrollTop)); setGutterOpacity(convertToOpacity(scroller.scrollLeft)); }; updateScroll(); scroller.addEventListener('scroll', updateScroll); return () => { resizeObserver.disconnect(); scroller.removeEventListener('scroll', updateScroll); }; }, [parent, scroller, setTop]); return ( <> ); }