aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-09-08 02:51:52 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-09-08 15:37:49 +0200
commit6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1 (patch)
treec005aca6da89629fac5d4001a475babd2fcd590c /subprojects/frontend/src
parentfeat(frontend): check for updates periodically (diff)
downloadrefinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.tar.gz
refinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.tar.zst
refinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.zip
feat(frontend): editor area scroll shadow styling
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r--subprojects/frontend/src/editor/EditorArea.tsx31
-rw-r--r--subprojects/frontend/src/editor/EditorAreaDecorations.tsx149
-rw-r--r--subprojects/frontend/src/editor/EditorPane.tsx2
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts11
4 files changed, 179 insertions, 14 deletions
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx
index 7c5ac5fb..174b1205 100644
--- a/subprojects/frontend/src/editor/EditorArea.tsx
+++ b/subprojects/frontend/src/editor/EditorArea.tsx
@@ -1,7 +1,9 @@
1import Box from '@mui/material/Box';
1import { useTheme } from '@mui/material/styles'; 2import { useTheme } from '@mui/material/styles';
2import { observer } from 'mobx-react-lite'; 3import { observer } from 'mobx-react-lite';
3import React, { useCallback, useEffect } from 'react'; 4import React, { useCallback, useEffect, useState } from 'react';
4 5
6import EditorAreaDecorations from './EditorAreaDecorations';
5import type EditorStore from './EditorStore'; 7import type EditorStore from './EditorStore';
6import EditorTheme from './EditorTheme'; 8import EditorTheme from './EditorTheme';
7 9
@@ -13,23 +15,40 @@ export default observer(function EditorArea({
13 const { 15 const {
14 palette: { mode: paletteMode }, 16 palette: { mode: paletteMode },
15 } = useTheme(); 17 } = useTheme();
18 const [parent, setParent] = useState<HTMLElement | undefined>();
19 const [scroller, setScroller] = useState<HTMLElement | undefined>();
16 20
17 useEffect( 21 useEffect(
18 () => editorStore.setDarkMode(paletteMode === 'dark'), 22 () => editorStore.setDarkMode(paletteMode === 'dark'),
19 [editorStore, paletteMode], 23 [editorStore, paletteMode],
20 ); 24 );
21 25
26 const parentRef = useCallback(
27 (value: HTMLElement | null) => setParent(value ?? undefined),
28 [],
29 );
30
22 const editorParentRef = useCallback( 31 const editorParentRef = useCallback(
23 (editorParent: HTMLDivElement | null) => { 32 (editorParent: HTMLDivElement | null) => {
24 editorStore.setEditorParent(editorParent ?? undefined); 33 editorStore.setEditorParent(editorParent ?? undefined);
34 setScroller(editorStore.view?.scrollDOM);
25 }, 35 },
26 [editorStore], 36 [editorStore, setScroller],
27 ); 37 );
28 38
29 return ( 39 return (
30 <EditorTheme 40 <Box
31 showLineNumbers={editorStore.showLineNumbers} 41 ref={parentRef}
32 ref={editorParentRef} 42 position="relative"
33 /> 43 flexGrow={1}
44 flexShrink={1}
45 overflow="auto"
46 >
47 <EditorTheme
48 showLineNumbers={editorStore.showLineNumbers}
49 ref={editorParentRef}
50 />
51 <EditorAreaDecorations parent={parent} scroller={scroller} />
52 </Box>
34 ); 53 );
35}); 54});
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}
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx
index e433e714..bcd324e5 100644
--- a/subprojects/frontend/src/editor/EditorPane.tsx
+++ b/subprojects/frontend/src/editor/EditorPane.tsx
@@ -43,7 +43,7 @@ export default observer(function EditorPane(): JSX.Element {
43 <EditorButtons editorStore={editorStore} /> 43 <EditorButtons editorStore={editorStore} />
44 {showGenerateButton && <GenerateButton editorStore={editorStore} />} 44 {showGenerateButton && <GenerateButton editorStore={editorStore} />}
45 </Toolbar> 45 </Toolbar>
46 <Box flexGrow={1} flexShrink={1} overflow="auto"> 46 <Box display="flex" flexGrow={1} flexShrink={1} overflow="auto">
47 {editorStore === undefined ? ( 47 {editorStore === undefined ? (
48 <EditorLoading /> 48 <EditorLoading />
49 ) : ( 49 ) : (
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts
index dfeeb547..db051d0e 100644
--- a/subprojects/frontend/src/editor/EditorTheme.ts
+++ b/subprojects/frontend/src/editor/EditorTheme.ts
@@ -31,12 +31,7 @@ export default styled('div', {
31 color: theme.palette.text.secondary, 31 color: theme.palette.text.secondary,
32 }, 32 },
33 '.cm-gutters': { 33 '.cm-gutters': {
34 background: `linear-gradient( 34 background: theme.palette.background.default,
35 to right,
36 ${theme.palette.background.default} 0%,
37 ${theme.palette.background.default} calc(100% - 12px),
38 transparent 100%
39 )`,
40 border: 'none', 35 border: 'none',
41 }, 36 },
42 '.cm-content': { 37 '.cm-content': {
@@ -170,7 +165,9 @@ export default styled('div', {
170 '.cm-panels-top': { 165 '.cm-panels-top': {
171 color: theme.palette.text.primary, 166 color: theme.palette.text.primary,
172 borderBottom: `1px solid ${theme.palette.outer.border}`, 167 borderBottom: `1px solid ${theme.palette.outer.border}`,
173 marginBottom: theme.spacing(1), 168 },
169 '.cm-panels-top + div + .cm-scroller': {
170 paddingTop: theme.spacing(0.5),
174 }, 171 },
175 '.cm-panel': { 172 '.cm-panel': {
176 color: theme.palette.text.primary, 173 color: theme.palette.text.primary,