diff options
author | 2022-09-08 02:51:52 +0200 | |
---|---|---|
committer | 2022-09-08 15:37:49 +0200 | |
commit | 6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1 (patch) | |
tree | c005aca6da89629fac5d4001a475babd2fcd590c /subprojects/frontend/src | |
parent | feat(frontend): check for updates periodically (diff) | |
download | refinery-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.tsx | 31 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorAreaDecorations.tsx | 149 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorPane.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorTheme.ts | 11 |
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 @@ | |||
1 | import Box from '@mui/material/Box'; | ||
1 | import { useTheme } from '@mui/material/styles'; | 2 | import { useTheme } from '@mui/material/styles'; |
2 | import { observer } from 'mobx-react-lite'; | 3 | import { observer } from 'mobx-react-lite'; |
3 | import React, { useCallback, useEffect } from 'react'; | 4 | import React, { useCallback, useEffect, useState } from 'react'; |
4 | 5 | ||
6 | import EditorAreaDecorations from './EditorAreaDecorations'; | ||
5 | import type EditorStore from './EditorStore'; | 7 | import type EditorStore from './EditorStore'; |
6 | import EditorTheme from './EditorTheme'; | 8 | import 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 @@ | |||
1 | import { type CSSObject, type Theme, styled } from '@mui/material/styles'; | ||
2 | import React, { memo, useEffect, useState } from 'react'; | ||
3 | |||
4 | const SHADOW_SIZE = 10; | ||
5 | |||
6 | const EditorAreaDecoration = styled('div')({ | ||
7 | position: 'absolute', | ||
8 | pointerEvents: 'none', | ||
9 | }); | ||
10 | |||
11 | function 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 | |||
37 | function 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 | |||
51 | const 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 | |||
67 | const 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 | |||
88 | function convertToOpacity(scroll: number): number { | ||
89 | return Math.max(0, Math.min(1, scroll / SHADOW_SIZE)); | ||
90 | } | ||
91 | |||
92 | export 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, |