diff options
-rw-r--r-- | subprojects/frontend/package.json | 4 | ||||
-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 | ||||
-rw-r--r-- | yarn.lock | 20 |
6 files changed, 191 insertions, 26 deletions
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index af345777..0de7ce47 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -23,13 +23,13 @@ | |||
23 | }, | 23 | }, |
24 | "homepage": "https://refinery.tools", | 24 | "homepage": "https://refinery.tools", |
25 | "dependencies": { | 25 | "dependencies": { |
26 | "@codemirror/autocomplete": "^6.1.0", | 26 | "@codemirror/autocomplete": "^6.1.1", |
27 | "@codemirror/commands": "^6.1.0", | 27 | "@codemirror/commands": "^6.1.0", |
28 | "@codemirror/language": "^6.2.1", | 28 | "@codemirror/language": "^6.2.1", |
29 | "@codemirror/lint": "^6.0.0", | 29 | "@codemirror/lint": "^6.0.0", |
30 | "@codemirror/search": "^6.2.0", | 30 | "@codemirror/search": "^6.2.0", |
31 | "@codemirror/state": "^6.1.1", | 31 | "@codemirror/state": "^6.1.1", |
32 | "@codemirror/view": "^6.2.2", | 32 | "@codemirror/view": "^6.2.3", |
33 | "@emotion/react": "^11.10.4", | 33 | "@emotion/react": "^11.10.4", |
34 | "@emotion/styled": "^11.10.4", | 34 | "@emotion/styled": "^11.10.4", |
35 | "@fontsource/inter": "^4.5.12", | 35 | "@fontsource/inter": "^4.5.12", |
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, |
@@ -1335,9 +1335,9 @@ __metadata: | |||
1335 | languageName: node | 1335 | languageName: node |
1336 | linkType: hard | 1336 | linkType: hard |
1337 | 1337 | ||
1338 | "@codemirror/autocomplete@npm:^6.1.0": | 1338 | "@codemirror/autocomplete@npm:^6.1.1": |
1339 | version: 6.1.0 | 1339 | version: 6.1.1 |
1340 | resolution: "@codemirror/autocomplete@npm:6.1.0" | 1340 | resolution: "@codemirror/autocomplete@npm:6.1.1" |
1341 | dependencies: | 1341 | dependencies: |
1342 | "@codemirror/language": ^6.0.0 | 1342 | "@codemirror/language": ^6.0.0 |
1343 | "@codemirror/state": ^6.0.0 | 1343 | "@codemirror/state": ^6.0.0 |
@@ -1348,7 +1348,7 @@ __metadata: | |||
1348 | "@codemirror/state": ^6.0.0 | 1348 | "@codemirror/state": ^6.0.0 |
1349 | "@codemirror/view": ^6.0.0 | 1349 | "@codemirror/view": ^6.0.0 |
1350 | "@lezer/common": ^1.0.0 | 1350 | "@lezer/common": ^1.0.0 |
1351 | checksum: f29b521935c46d233f35afacb968b789a22984032999e905c74f4dfc6225125c787538725f7a7b5be194253cbb79ea0d76c0b0c7163489d31cdbba33156ab540 | 1351 | checksum: d5dc9f0394d52ed891845f077e3ab5f0b337b1dd6e9735d18dddaafef634ceb4fa0f272b7bcf824a720666af0bc721a0c7b5a3c7cefcd18f1235753f27d4b86c |
1352 | languageName: node | 1352 | languageName: node |
1353 | linkType: hard | 1353 | linkType: hard |
1354 | 1354 | ||
@@ -1407,14 +1407,14 @@ __metadata: | |||
1407 | languageName: node | 1407 | languageName: node |
1408 | linkType: hard | 1408 | linkType: hard |
1409 | 1409 | ||
1410 | "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.2": | 1410 | "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.3": |
1411 | version: 6.2.2 | 1411 | version: 6.2.3 |
1412 | resolution: "@codemirror/view@npm:6.2.2" | 1412 | resolution: "@codemirror/view@npm:6.2.3" |
1413 | dependencies: | 1413 | dependencies: |
1414 | "@codemirror/state": ^6.0.0 | 1414 | "@codemirror/state": ^6.0.0 |
1415 | style-mod: ^4.0.0 | 1415 | style-mod: ^4.0.0 |
1416 | w3c-keyname: ^2.2.4 | 1416 | w3c-keyname: ^2.2.4 |
1417 | checksum: 6983c51362367d3885961fa233d302e75dfb103cd83ec78e6a044c2420e61466b5e94cfb73a9e840b04765577e8bbb269eb1ad45fa21abd28efda4ae780791db | 1417 | checksum: 93d6f159c49ffd9276e9b6ba28cef2c41934be7eb68aa49acf1971dede97ee2684bde6daeb7494da8e15f2ef937933c3fa076253127b230c6996db0fe7a79ef2 |
1418 | languageName: node | 1418 | languageName: node |
1419 | linkType: hard | 1419 | linkType: hard |
1420 | 1420 | ||
@@ -2000,13 +2000,13 @@ __metadata: | |||
2000 | version: 0.0.0-use.local | 2000 | version: 0.0.0-use.local |
2001 | resolution: "@refinery/frontend@workspace:subprojects/frontend" | 2001 | resolution: "@refinery/frontend@workspace:subprojects/frontend" |
2002 | dependencies: | 2002 | dependencies: |
2003 | "@codemirror/autocomplete": ^6.1.0 | 2003 | "@codemirror/autocomplete": ^6.1.1 |
2004 | "@codemirror/commands": ^6.1.0 | 2004 | "@codemirror/commands": ^6.1.0 |
2005 | "@codemirror/language": ^6.2.1 | 2005 | "@codemirror/language": ^6.2.1 |
2006 | "@codemirror/lint": ^6.0.0 | 2006 | "@codemirror/lint": ^6.0.0 |
2007 | "@codemirror/search": ^6.2.0 | 2007 | "@codemirror/search": ^6.2.0 |
2008 | "@codemirror/state": ^6.1.1 | 2008 | "@codemirror/state": ^6.1.1 |
2009 | "@codemirror/view": ^6.2.2 | 2009 | "@codemirror/view": ^6.2.3 |
2010 | "@emotion/react": ^11.10.4 | 2010 | "@emotion/react": ^11.10.4 |
2011 | "@emotion/styled": ^11.10.4 | 2011 | "@emotion/styled": ^11.10.4 |
2012 | "@fontsource/inter": ^4.5.12 | 2012 | "@fontsource/inter": ^4.5.12 |