From 6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 8 Sep 2022 02:51:52 +0200 Subject: feat(frontend): editor area scroll shadow styling --- subprojects/frontend/package.json | 4 +- subprojects/frontend/src/editor/EditorArea.tsx | 31 ++++- .../frontend/src/editor/EditorAreaDecorations.tsx | 149 +++++++++++++++++++++ subprojects/frontend/src/editor/EditorPane.tsx | 2 +- subprojects/frontend/src/editor/EditorTheme.ts | 11 +- yarn.lock | 20 +-- 6 files changed, 191 insertions(+), 26 deletions(-) create mode 100644 subprojects/frontend/src/editor/EditorAreaDecorations.tsx 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 @@ }, "homepage": "https://refinery.tools", "dependencies": { - "@codemirror/autocomplete": "^6.1.0", + "@codemirror/autocomplete": "^6.1.1", "@codemirror/commands": "^6.1.0", "@codemirror/language": "^6.2.1", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.2.0", "@codemirror/state": "^6.1.1", - "@codemirror/view": "^6.2.2", + "@codemirror/view": "^6.2.3", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", "@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 @@ +import Box from '@mui/material/Box'; import { useTheme } from '@mui/material/styles'; import { observer } from 'mobx-react-lite'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import EditorAreaDecorations from './EditorAreaDecorations'; import type EditorStore from './EditorStore'; import EditorTheme from './EditorTheme'; @@ -13,23 +15,40 @@ export default observer(function EditorArea({ const { palette: { mode: paletteMode }, } = useTheme(); + const [parent, setParent] = useState(); + const [scroller, setScroller] = useState(); useEffect( () => editorStore.setDarkMode(paletteMode === 'dark'), [editorStore, paletteMode], ); + const parentRef = useCallback( + (value: HTMLElement | null) => setParent(value ?? undefined), + [], + ); + const editorParentRef = useCallback( (editorParent: HTMLDivElement | null) => { editorStore.setEditorParent(editorParent ?? undefined); + setScroller(editorStore.view?.scrollDOM); }, - [editorStore], + [editorStore, setScroller], ); return ( - + + + + ); }); 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 @@ +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 ( + <> + + + + ); +} 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 { {showGenerateButton && } - + {editorStore === undefined ? ( ) : ( 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', { color: theme.palette.text.secondary, }, '.cm-gutters': { - background: `linear-gradient( - to right, - ${theme.palette.background.default} 0%, - ${theme.palette.background.default} calc(100% - 12px), - transparent 100% - )`, + background: theme.palette.background.default, border: 'none', }, '.cm-content': { @@ -170,7 +165,9 @@ export default styled('div', { '.cm-panels-top': { color: theme.palette.text.primary, borderBottom: `1px solid ${theme.palette.outer.border}`, - marginBottom: theme.spacing(1), + }, + '.cm-panels-top + div + .cm-scroller': { + paddingTop: theme.spacing(0.5), }, '.cm-panel': { color: theme.palette.text.primary, diff --git a/yarn.lock b/yarn.lock index 57fae9b5..f8b352d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1335,9 +1335,9 @@ __metadata: languageName: node linkType: hard -"@codemirror/autocomplete@npm:^6.1.0": - version: 6.1.0 - resolution: "@codemirror/autocomplete@npm:6.1.0" +"@codemirror/autocomplete@npm:^6.1.1": + version: 6.1.1 + resolution: "@codemirror/autocomplete@npm:6.1.1" dependencies: "@codemirror/language": ^6.0.0 "@codemirror/state": ^6.0.0 @@ -1348,7 +1348,7 @@ __metadata: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.0.0 - checksum: f29b521935c46d233f35afacb968b789a22984032999e905c74f4dfc6225125c787538725f7a7b5be194253cbb79ea0d76c0b0c7163489d31cdbba33156ab540 + checksum: d5dc9f0394d52ed891845f077e3ab5f0b337b1dd6e9735d18dddaafef634ceb4fa0f272b7bcf824a720666af0bc721a0c7b5a3c7cefcd18f1235753f27d4b86c languageName: node linkType: hard @@ -1407,14 +1407,14 @@ __metadata: languageName: node linkType: hard -"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.2": - version: 6.2.2 - resolution: "@codemirror/view@npm:6.2.2" +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.3": + version: 6.2.3 + resolution: "@codemirror/view@npm:6.2.3" dependencies: "@codemirror/state": ^6.0.0 style-mod: ^4.0.0 w3c-keyname: ^2.2.4 - checksum: 6983c51362367d3885961fa233d302e75dfb103cd83ec78e6a044c2420e61466b5e94cfb73a9e840b04765577e8bbb269eb1ad45fa21abd28efda4ae780791db + checksum: 93d6f159c49ffd9276e9b6ba28cef2c41934be7eb68aa49acf1971dede97ee2684bde6daeb7494da8e15f2ef937933c3fa076253127b230c6996db0fe7a79ef2 languageName: node linkType: hard @@ -2000,13 +2000,13 @@ __metadata: version: 0.0.0-use.local resolution: "@refinery/frontend@workspace:subprojects/frontend" dependencies: - "@codemirror/autocomplete": ^6.1.0 + "@codemirror/autocomplete": ^6.1.1 "@codemirror/commands": ^6.1.0 "@codemirror/language": ^6.2.1 "@codemirror/lint": ^6.0.0 "@codemirror/search": ^6.2.0 "@codemirror/state": ^6.1.1 - "@codemirror/view": ^6.2.2 + "@codemirror/view": ^6.2.3 "@emotion/react": ^11.10.4 "@emotion/styled": ^11.10.4 "@fontsource/inter": ^4.5.12 -- cgit v1.2.3-70-g09d2