diff options
-rw-r--r-- | subprojects/frontend/src/App.tsx | 31 | ||||
-rw-r--r-- | subprojects/frontend/src/Refinery.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/TopBar.tsx | 1 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorTheme.ts | 12 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/scrollbarViewPlugin.ts | 74 | ||||
-rw-r--r-- | subprojects/frontend/src/theme/ThemeProvider.tsx | 5 |
6 files changed, 88 insertions, 37 deletions
diff --git a/subprojects/frontend/src/App.tsx b/subprojects/frontend/src/App.tsx index b162e551..cd394345 100644 --- a/subprojects/frontend/src/App.tsx +++ b/subprojects/frontend/src/App.tsx | |||
@@ -1,6 +1,14 @@ | |||
1 | import Box from '@mui/material/Box'; | 1 | import Box from '@mui/material/Box'; |
2 | import CssBaseline from '@mui/material/CssBaseline'; | 2 | import CssBaseline from '@mui/material/CssBaseline'; |
3 | import { StrictMode, Suspense, lazy } from 'react'; | 3 | import { throttle } from 'lodash-es'; |
4 | import { | ||
5 | StrictMode, | ||
6 | Suspense, | ||
7 | lazy, | ||
8 | useState, | ||
9 | useEffect, | ||
10 | useMemo, | ||
11 | } from 'react'; | ||
4 | 12 | ||
5 | import Loading from './Loading'; | 13 | import Loading from './Loading'; |
6 | import type RootStore from './RootStore'; | 14 | import type RootStore from './RootStore'; |
@@ -10,18 +18,37 @@ import ThemeProvider from './theme/ThemeProvider'; | |||
10 | 18 | ||
11 | const Refinery = lazy(() => import('./Refinery.js')); | 19 | const Refinery = lazy(() => import('./Refinery.js')); |
12 | 20 | ||
21 | function useInnerHeight(): number { | ||
22 | const [innerHeight, setInnerHeight] = useState(window.innerHeight); | ||
23 | const resizeHandler = useMemo( | ||
24 | () => throttle(() => setInnerHeight(window.innerHeight), 250), | ||
25 | [], | ||
26 | ); | ||
27 | useEffect(() => { | ||
28 | window.addEventListener('resize', resizeHandler, { passive: true }); | ||
29 | return () => { | ||
30 | window.removeEventListener('resize', resizeHandler); | ||
31 | resizeHandler.cancel(); | ||
32 | }; | ||
33 | }, [resizeHandler]); | ||
34 | return innerHeight; | ||
35 | } | ||
36 | |||
13 | export default function App({ | 37 | export default function App({ |
14 | rootStore, | 38 | rootStore, |
15 | }: { | 39 | }: { |
16 | rootStore: RootStore; | 40 | rootStore: RootStore; |
17 | }): JSX.Element { | 41 | }): JSX.Element { |
42 | // See https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ | ||
43 | const innerHeight = useInnerHeight(); | ||
44 | |||
18 | return ( | 45 | return ( |
19 | <StrictMode> | 46 | <StrictMode> |
20 | <RootStoreProvider rootStore={rootStore}> | 47 | <RootStoreProvider rootStore={rootStore}> |
21 | <ThemeProvider> | 48 | <ThemeProvider> |
22 | <CssBaseline enableColorScheme /> | 49 | <CssBaseline enableColorScheme /> |
23 | <WindowControlsOverlayColor /> | 50 | <WindowControlsOverlayColor /> |
24 | <Box height="100vh" overflow="auto"> | 51 | <Box height={`${innerHeight}px`} overflow="hidden"> |
25 | <Suspense fallback={<Loading />}> | 52 | <Suspense fallback={<Loading />}> |
26 | <Refinery /> | 53 | <Refinery /> |
27 | </Suspense> | 54 | </Suspense> |
diff --git a/subprojects/frontend/src/Refinery.tsx b/subprojects/frontend/src/Refinery.tsx index d6bd6159..93a82ee1 100644 --- a/subprojects/frontend/src/Refinery.tsx +++ b/subprojects/frontend/src/Refinery.tsx | |||
@@ -11,7 +11,7 @@ export default function Refinery(): JSX.Element { | |||
11 | // @ts-expect-error -- notistack has problems with `exactOptionalPropertyTypes | 11 | // @ts-expect-error -- notistack has problems with `exactOptionalPropertyTypes |
12 | <SnackbarProvider TransitionComponent={Grow}> | 12 | <SnackbarProvider TransitionComponent={Grow}> |
13 | <UpdateNotification /> | 13 | <UpdateNotification /> |
14 | <Stack direction="column" height="100vh" overflow="auto"> | 14 | <Stack direction="column" height="100%" overflow="auto"> |
15 | <TopBar /> | 15 | <TopBar /> |
16 | <EditorPane /> | 16 | <EditorPane /> |
17 | </Stack> | 17 | </Stack> |
diff --git a/subprojects/frontend/src/TopBar.tsx b/subprojects/frontend/src/TopBar.tsx index c943f7c4..0f757986 100644 --- a/subprojects/frontend/src/TopBar.tsx +++ b/subprojects/frontend/src/TopBar.tsx | |||
@@ -36,6 +36,7 @@ function useWindowControlsOverlayVisible(): boolean { | |||
36 | 'geometrychange', | 36 | 'geometrychange', |
37 | updateWindowControlsOverlayVisible, | 37 | updateWindowControlsOverlayVisible, |
38 | ); | 38 | ); |
39 | updateWindowControlsOverlayVisible.cancel(); | ||
39 | }; | 40 | }; |
40 | } | 41 | } |
41 | // Nothing to clean up if `windowControlsOverlay` is unsupported. | 42 | // Nothing to clean up if `windowControlsOverlay` is unsupported. |
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index c3cbffc8..89bc8932 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts | |||
@@ -52,6 +52,8 @@ export default styled('div', { | |||
52 | }, | 52 | }, |
53 | }; | 53 | }; |
54 | 54 | ||
55 | const scrollerThumbOpacity = theme.palette.mode === 'dark' ? 0.16 : 0.28; | ||
56 | |||
55 | const generalStyle: CSSObject = { | 57 | const generalStyle: CSSObject = { |
56 | background: theme.palette.background.default, | 58 | background: theme.palette.background.default, |
57 | '&, .cm-editor': { | 59 | '&, .cm-editor': { |
@@ -70,6 +72,7 @@ export default styled('div', { | |||
70 | zIndex: 300, | 72 | zIndex: 300, |
71 | width: 1, | 73 | width: 1, |
72 | marginRight: -1, | 74 | marginRight: -1, |
75 | pointerEvents: 'none', | ||
73 | }, | 76 | }, |
74 | '.cm-scroller': { | 77 | '.cm-scroller': { |
75 | color: theme.palette.text.secondary, | 78 | color: theme.palette.text.secondary, |
@@ -84,12 +87,17 @@ export default styled('div', { | |||
84 | '.cm-scroller-thumb': { | 87 | '.cm-scroller-thumb': { |
85 | position: 'absolute', | 88 | position: 'absolute', |
86 | background: theme.palette.text.secondary, | 89 | background: theme.palette.text.secondary, |
87 | opacity: theme.palette.mode === 'dark' ? 0.16 : 0.28, | 90 | opacity: scrollerThumbOpacity, |
88 | transition: theme.transitions.create('opacity', { | 91 | transition: theme.transitions.create('opacity', { |
89 | duration: theme.transitions.duration.shortest, | 92 | duration: theme.transitions.duration.shortest, |
90 | }), | 93 | }), |
94 | touchAction: 'none', | ||
95 | WebkitTapHighlightColor: 'transparent', | ||
91 | '&:hover': { | 96 | '&:hover': { |
92 | opacity: 0.75, | 97 | opacity: 0.75, |
98 | '@media (hover: none)': { | ||
99 | opacity: scrollerThumbOpacity, | ||
100 | }, | ||
93 | }, | 101 | }, |
94 | '&.active': { | 102 | '&.active': { |
95 | opacity: 1, | 103 | opacity: 1, |
@@ -445,6 +453,7 @@ export default styled('div', { | |||
445 | background: theme.palette.text.primary, | 453 | background: theme.palette.text.primary, |
446 | border: 'none', | 454 | border: 'none', |
447 | cursor: 'pointer', | 455 | cursor: 'pointer', |
456 | WebkitTapHighlightColor: 'transparent', | ||
448 | [theme.breakpoints.down('sm')]: { | 457 | [theme.breakpoints.down('sm')]: { |
449 | margin: '2px 0', | 458 | margin: '2px 0', |
450 | }, | 459 | }, |
@@ -471,6 +480,7 @@ export default styled('div', { | |||
471 | background: 'transparent', | 480 | background: 'transparent', |
472 | border: 'none', | 481 | border: 'none', |
473 | cursor: 'pointer', | 482 | cursor: 'pointer', |
483 | WebkitTapHighlightColor: 'transparent', | ||
474 | // Use an inner `span` element to match the height of other text highlights. | 484 | // Use an inner `span` element to match the height of other text highlights. |
475 | span: { | 485 | span: { |
476 | color: theme.palette.text.secondary, | 486 | color: theme.palette.text.secondary, |
diff --git a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts index 9ee70441..c1eb2bbd 100644 --- a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts +++ b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts | |||
@@ -22,6 +22,43 @@ export const SCROLLBAR_WIDTH = 12; | |||
22 | export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2; | 22 | export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2; |
23 | export const MIN_ANNOTATION_HEIGHT = 1; | 23 | export const MIN_ANNOTATION_HEIGHT = 1; |
24 | 24 | ||
25 | function handleDrag( | ||
26 | element: HTMLElement, | ||
27 | callback: (movementX: number, movementY: number) => void, | ||
28 | ) { | ||
29 | let pointerId: number | undefined; | ||
30 | element.addEventListener('pointerdown', (event) => { | ||
31 | if (pointerId === undefined) { | ||
32 | ({ pointerId } = event); | ||
33 | element.setPointerCapture(pointerId); | ||
34 | element.classList.add(THUMB_ACTIVE_CLASS); | ||
35 | } else { | ||
36 | event.preventDefault(); | ||
37 | // Avoid implicit pointer capture, see https://w3c.github.io/pointerevents/#dfn-implicit-pointer-capture | ||
38 | element.releasePointerCapture(event.pointerId); | ||
39 | } | ||
40 | }); | ||
41 | |||
42 | element.addEventListener('pointermove', (event) => { | ||
43 | if (event.pointerId !== pointerId) { | ||
44 | return; | ||
45 | } | ||
46 | callback(event.movementX, event.movementY); | ||
47 | event.preventDefault(); | ||
48 | }); | ||
49 | |||
50 | function scrollEnd(event: PointerEvent) { | ||
51 | if (event.pointerId !== pointerId) { | ||
52 | return; | ||
53 | } | ||
54 | pointerId = undefined; | ||
55 | element.classList.remove(THUMB_ACTIVE_CLASS); | ||
56 | } | ||
57 | |||
58 | element.addEventListener('pointerup', scrollEnd, { passive: true }); | ||
59 | element.addEventListener('pointercancel', scrollEnd, { passive: true }); | ||
60 | } | ||
61 | |||
25 | export default function scrollbarViewPlugin( | 62 | export default function scrollbarViewPlugin( |
26 | editorStore: EditorStore, | 63 | editorStore: EditorStore, |
27 | ): ViewPlugin<PluginValue> { | 64 | ): ViewPlugin<PluginValue> { |
@@ -63,44 +100,15 @@ export default function scrollbarViewPlugin( | |||
63 | 100 | ||
64 | const thumbY = ownerDocument.createElement('div'); | 101 | const thumbY = ownerDocument.createElement('div'); |
65 | thumbY.className = `${THUMB_CLASS} ${THUMB_Y_CLASS}`; | 102 | thumbY.className = `${THUMB_CLASS} ${THUMB_Y_CLASS}`; |
66 | const scrollY = (event: MouseEvent) => { | 103 | handleDrag(thumbY, (_movementX, movementY) => |
67 | scrollDOM.scrollBy({ top: event.movementY / factorY }); | 104 | scrollDOM.scrollBy({ top: movementY / factorY }), |
68 | event.preventDefault(); | ||
69 | }; | ||
70 | const stopScrollY = () => { | ||
71 | thumbY.classList.remove(THUMB_ACTIVE_CLASS); | ||
72 | window.removeEventListener('mousemove', scrollY); | ||
73 | window.removeEventListener('mouseup', stopScrollY); | ||
74 | }; | ||
75 | thumbY.addEventListener( | ||
76 | 'mousedown', | ||
77 | () => { | ||
78 | thumbY.classList.add(THUMB_ACTIVE_CLASS); | ||
79 | window.addEventListener('mousemove', scrollY); | ||
80 | window.addEventListener('mouseup', stopScrollY, { passive: true }); | ||
81 | }, | ||
82 | { passive: true }, | ||
83 | ); | 105 | ); |
84 | holder.appendChild(thumbY); | 106 | holder.appendChild(thumbY); |
85 | 107 | ||
86 | const thumbX = ownerDocument.createElement('div'); | 108 | const thumbX = ownerDocument.createElement('div'); |
87 | thumbX.className = `${THUMB_CLASS} ${THUMB_X_CLASS}`; | 109 | thumbX.className = `${THUMB_CLASS} ${THUMB_X_CLASS}`; |
88 | const scrollX = (event: MouseEvent) => { | 110 | handleDrag(thumbX, (movementX) => |
89 | scrollDOM.scrollBy({ left: event.movementX / factorX }); | 111 | scrollDOM.scrollBy({ left: movementX / factorX }), |
90 | }; | ||
91 | const stopScrollX = () => { | ||
92 | thumbX.classList.remove(THUMB_ACTIVE_CLASS); | ||
93 | window.removeEventListener('mousemove', scrollX); | ||
94 | window.removeEventListener('mouseup', stopScrollX); | ||
95 | }; | ||
96 | thumbX.addEventListener( | ||
97 | 'mousedown', | ||
98 | () => { | ||
99 | thumbX.classList.add(THUMB_ACTIVE_CLASS); | ||
100 | window.addEventListener('mousemove', scrollX); | ||
101 | window.addEventListener('mouseup', stopScrollX, { passive: true }); | ||
102 | }, | ||
103 | { passive: true }, | ||
104 | ); | 112 | ); |
105 | holder.appendChild(thumbX); | 113 | holder.appendChild(thumbX); |
106 | 114 | ||
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index 9cf870d5..85844d3c 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx | |||
@@ -73,6 +73,11 @@ const typography: TypographyVariantsOptions = { | |||
73 | fontWeightMedium: 600, | 73 | fontWeightMedium: 600, |
74 | fontWeightEditorNormal: 400, | 74 | fontWeightEditorNormal: 400, |
75 | fontWeightEditorBold: 700, | 75 | fontWeightEditorBold: 700, |
76 | button: { | ||
77 | // 24px line height for 14px button text means 36px high buttons. | ||
78 | // Making sure the button has whole pixel height reduces redering errors on Android. | ||
79 | lineHeight: 1.7143, | ||
80 | }, | ||
76 | editor: { | 81 | editor: { |
77 | fontFamily: | 82 | fontFamily: |
78 | '"JetBrains MonoVariable", "JetBrains Mono", "Cascadia Code", "Fira Code", monospace', | 83 | '"JetBrains MonoVariable", "JetBrains Mono", "Cascadia Code", "Fira Code", monospace', |