aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor/EditorAreaDecorations.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/editor/EditorAreaDecorations.tsx')
-rw-r--r--subprojects/frontend/src/editor/EditorAreaDecorations.tsx149
1 files changed, 149 insertions, 0 deletions
diff --git a/subprojects/frontend/src/editor/EditorAreaDecorations.tsx b/subprojects/frontend/src/editor/EditorAreaDecorations.tsx
new file mode 100644
index 00000000..f33bf25f
--- /dev/null
+++ b/subprojects/frontend/src/editor/EditorAreaDecorations.tsx
@@ -0,0 +1,149 @@
1import { type CSSObject, type Theme, styled } from '@mui/material/styles';
2import React, { memo, useEffect, useState } from 'react';
3
4const SHADOW_SIZE = 10;
5
6const EditorAreaDecoration = styled('div')({
7 position: 'absolute',
8 pointerEvents: 'none',
9});
10
11function shadowTheme(
12 origin: string,
13 scaleX: boolean,
14 scaleY: boolean,
15): CSSObject {
16 function radialGradient(opacity: number, scale: string): string {
17 return `radial-gradient(
18 farthest-side at ${origin},
19 rgba(0, 0, 0, ${opacity}),
20 rgba(0, 0, 0, 0)
21 )
22 ${origin} /
23 ${scaleX ? scale : '100%'}
24 ${scaleY ? scale : '100%'}
25 no-repeat`;
26 }
27
28 return {
29 background: `
30 ${radialGradient(0.2, '40%')},
31 ${radialGradient(0.14, '50%')},
32 ${radialGradient(0.12, '100%')}
33 `,
34 };
35}
36
37function animateSize(
38 theme: Theme,
39 direction: 'height' | 'width',
40 opacity: number,
41): CSSObject {
42 return {
43 [direction]: opacity * SHADOW_SIZE,
44 transition: theme.transitions.create(direction, {
45 duration: theme.transitions.duration.shortest,
46 easing: theme.transitions.easing.sharp,
47 }),
48 };
49}
50
51const TopDecoration = memo(
52 styled(EditorAreaDecoration, {
53 shouldForwardProp: (prop) => prop !== 'visible' && prop !== 'opacity',
54 })<{
55 visible: boolean;
56 opacity: number;
57 }>(({ theme, visible, opacity }) => ({
58 display: visible ? 'block' : 'none',
59 top: 0,
60 left: 0,
61 right: 0,
62 ...shadowTheme('50% 0', false, true),
63 ...animateSize(theme, 'height', opacity),
64 })),
65);
66
67const GutterDecoration = memo(
68 styled(EditorAreaDecoration, {
69 shouldForwardProp: (prop) =>
70 prop !== 'top' &&
71 prop !== 'bottom' &&
72 prop !== 'guttersWidth' &&
73 prop !== 'opacity',
74 })<{
75 top: number;
76 bottom: number;
77 guttersWidth: number;
78 opacity: number;
79 }>(({ theme, top, bottom, guttersWidth, opacity }) => ({
80 top,
81 left: guttersWidth,
82 bottom,
83 ...shadowTheme('0 50%', true, false),
84 ...animateSize(theme, 'width', opacity),
85 })),
86);
87
88function convertToOpacity(scroll: number): number {
89 return Math.max(0, Math.min(1, scroll / SHADOW_SIZE));
90}
91
92export default function EditorAreaDecorations({
93 parent,
94 scroller,
95}: {
96 parent: HTMLElement | undefined;
97 scroller: HTMLElement | undefined;
98}): JSX.Element {
99 const [top, setTop] = useState(0);
100 const [bottom, setBottom] = useState(0);
101 const [guttersWidth, setGuttersWidth] = useState(0);
102 const [topOpacity, setTopOpacity] = useState(0);
103 const [gutterOpacity, setGutterOpacity] = useState(0);
104
105 useEffect(() => {
106 if (parent === undefined || scroller === undefined) {
107 return () => {};
108 }
109 const gutters = scroller.querySelector('.cm-gutters');
110
111 const updateBounds = () => {
112 const parentRect = parent.getBoundingClientRect();
113 const rect = scroller.getBoundingClientRect();
114 setTop(rect.top - parentRect.top);
115 setBottom(parentRect.bottom - rect.bottom);
116 setGuttersWidth(gutters?.clientWidth ?? 0);
117 };
118 updateBounds();
119 const resizeObserver = new ResizeObserver(updateBounds);
120 resizeObserver.observe(scroller);
121 if (gutters !== null) {
122 resizeObserver.observe(gutters);
123 }
124
125 const updateScroll = () => {
126 setTopOpacity(convertToOpacity(scroller.scrollTop));
127 setGutterOpacity(convertToOpacity(scroller.scrollLeft));
128 };
129 updateScroll();
130 scroller.addEventListener('scroll', updateScroll);
131
132 return () => {
133 resizeObserver.disconnect();
134 scroller.removeEventListener('scroll', updateScroll);
135 };
136 }, [parent, scroller, setTop]);
137
138 return (
139 <>
140 <GutterDecoration
141 top={top}
142 bottom={bottom}
143 guttersWidth={guttersWidth}
144 opacity={gutterOpacity}
145 />
146 <TopDecoration visible={top === 0} opacity={topOpacity} />
147 </>
148 );
149}