From efdb6c6620e9ea7f5861efbef7d3462bf3c7bcd7 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 17 Sep 2021 19:10:19 +0200 Subject: Frontend color theme --- language-web/src/main/css/index.scss | 105 ++++++++++++++++----- .../src/main/css/themeVariables.module.scss | 9 ++ language-web/src/main/css/themes.scss | 36 +++++++ language-web/src/main/js/RootStore.tsx | 6 +- language-web/src/main/js/editor/EditorStore.ts | 16 +++- language-web/src/main/js/global.d.ts | 5 + language-web/src/main/js/index.tsx | 32 ++----- language-web/src/main/js/theme/EditorTheme.ts | 47 +++++++++ language-web/src/main/js/theme/ThemeProvider.tsx | 15 +++ language-web/src/main/js/theme/ThemeStore.ts | 53 +++++++++++ 10 files changed, 269 insertions(+), 55 deletions(-) create mode 100644 language-web/src/main/css/themeVariables.module.scss create mode 100644 language-web/src/main/css/themes.scss create mode 100644 language-web/src/main/js/global.d.ts create mode 100644 language-web/src/main/js/theme/EditorTheme.ts create mode 100644 language-web/src/main/js/theme/ThemeProvider.tsx create mode 100644 language-web/src/main/js/theme/ThemeStore.ts (limited to 'language-web/src/main') diff --git a/language-web/src/main/css/index.scss b/language-web/src/main/css/index.scss index c92588b8..21a9c05b 100644 --- a/language-web/src/main/css/index.scss +++ b/language-web/src/main/css/index.scss @@ -1,3 +1,4 @@ +@use 'sass:map'; @use '@fontsource/roboto/scss/mixins' as Roboto; @use '@fontsource/jetbrains-mono/scss/mixins' as JetbrainsMono; @@ -5,14 +6,16 @@ @import 'codemirror/addon/hint/show-hint'; @import 'codemirror/theme/material-darker'; -$robotoWeights: 300, 400, 500, 700; -@each $weight in $robotoWeights { +@import './themes'; + +$fontWeights: 300, 400, 500, 700; +@each $weight in $fontWeights { @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight); @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight, $style: italic); } -$jetbrainsMonoWeights: 400, 700; -@each $weight in $jetbrainsMonoWeights { +$monoFontWeights: 400, 700; +@each $weight in $monoFontWeights { @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight); @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight, $style: italic); } @@ -35,8 +38,78 @@ body { text-rendering: optimizeLegibility; } +@each $themeName, $theme in $themes { + .cm-s-problem-#{$themeName} { + &.CodeMirror { + background: map.get($theme, 'background'); + color: map.get($theme, 'foreground'); + } + + .CodeMirror-gutters { + background: map.get($theme, 'background'); + border: none; + } + + .CodeMirror-cursor { + border-left: 1px solid map.get($theme, 'cursor'); + } + + div.CodeMirror-selected, + &.CodeMirror-focused div.CodeMirror-selected, + .CodeMirror-line::selection, + .CodeMirror-line > span::selection, + .CodeMirror-line > span > span::selection { + background: map.get($theme, 'selection'); + } + + .CodeMirror-guttermarker, + .CodeMirror-guttermarker-subtle, + .CodeMirror-linenumber { + color: map.get($theme, 'lineNumber'); + } + + .CodeMirror-activeline-background { + background: map.get($theme, 'currentLine'); + } + + .CodeMirror-activeline-gutter { + background: map.get($theme, 'currentLine'); + + .CodeMirror-guttermarker, + .CodeMirror-guttermarker-subtle, + .CodeMirror-linenumber { + color: map.get($theme, 'foreground'); + } + } + + .cm-keyword { + color: map.get($theme, 'keyword'); + } + + .cm-number { + color: map.get($theme, 'number'); + } + + .cm-lparen, .cm-rparen { + color: map.get($theme, 'delimiter'); + } + + .problem-predicate, .problem-class, .problem-reference, .problem-enum { + color: map.get($theme, 'predicate'); + } + + .problem-unique-node { + color: map.get($theme, 'uniqueNode'); + } + + .problem-variable { + color: map.get($theme, 'variable'); + } + } +} + .CodeMirror-hints { - background: #333333; + background: #333; border: 0; border-radius: 4px; box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), @@ -107,38 +180,18 @@ li.CodeMirror-hint-active { background: rgba(128, 203, 196, 0.2); } + .xtext-marker_write { background: rgba(255, 229, 100, 0.2); } -.problem-class, .problem-enum { - @extend .cm-type; -} - .problem-abstract { font-style: italic; } -.problem-reference { - @extend .cm-def; -} - .problem-containment { font-weight: 700; } - -.problem-unique-node { - @extend .cm-atom; -} - .problem-new-node { font-style: italic; } - -.problem-variable { - @extend .cm-variable; -} - -.problem-singleton-variable { - opacity: 0.6; -} diff --git a/language-web/src/main/css/themeVariables.module.scss b/language-web/src/main/css/themeVariables.module.scss new file mode 100644 index 00000000..85af4219 --- /dev/null +++ b/language-web/src/main/css/themeVariables.module.scss @@ -0,0 +1,9 @@ +@import './themes'; + +:export { + @each $themeName, $theme in $themes { + @each $variable, $value in $theme { + #{$themeName}--#{$variable}: $value, + } + } +} diff --git a/language-web/src/main/css/themes.scss b/language-web/src/main/css/themes.scss new file mode 100644 index 00000000..0118290d --- /dev/null +++ b/language-web/src/main/css/themes.scss @@ -0,0 +1,36 @@ +$themes: ( + 'dark': ( + 'foreground': #abb2bf, + 'background': #282c34, + 'paper': #21252b, + 'primary': #56b6c2, + 'secondary': #ff5370, + 'keyword': #56b6c2, + 'predicate': #d6e9ff, + 'variable': #c8ae9d, + 'uniqueNode': #d6e9ff, + 'number': #6e88a6, + 'delimiter': #6f7682, + 'cursor': #56b6c2, + 'selection': #3e4452, + 'currentLine': #2c323c, + 'lineNumber': #5c6340, + ), + 'light': ( + 'foreground': #abb2bf, + 'background': #282c34, + 'paper': #21252b, + 'primary': #56b6c2, + 'secondary': #ff5370, + 'keyword': #56b6c2, + 'predicate': #d6e9ff, + 'variable': #c8ae9d, + 'uniqueNode': #d6e9ff, + 'number': #6e88a6, + 'delimiter': #6f7682, + 'cursor': #f3efe7, + 'selection': #3e4452, + 'currentLine': #2c323c, + 'lineNumber': #5c6340, + ), +); diff --git a/language-web/src/main/js/RootStore.tsx b/language-web/src/main/js/RootStore.tsx index 1c3aab2b..88b8a445 100644 --- a/language-web/src/main/js/RootStore.tsx +++ b/language-web/src/main/js/RootStore.tsx @@ -1,12 +1,16 @@ import React, { createContext, useContext } from 'react'; import { EditorStore } from './editor/EditorStore'; +import { ThemeStore } from './theme/ThemeStore'; export class RootStore { editorStore; + themeStore; + constructor() { - this.editorStore = new EditorStore(); + this.themeStore = new ThemeStore(); + this.editorStore = new EditorStore(this.themeStore); } } diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index 3a0c6f87..5da45ac1 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts @@ -1,4 +1,5 @@ import { Editor, EditorConfiguration } from 'codemirror'; +import 'codemirror/addon/selection/active-line'; import { createAtom, makeAutoObservable, @@ -12,6 +13,8 @@ import { removeServices, } from 'xtext/xtext-codemirror'; +import { ThemeStore } from '../theme/ThemeStore'; + const xtextLang = 'problem'; const xtextOptions: IXtextOptions = { @@ -22,10 +25,12 @@ const xtextOptions: IXtextOptions = { const codeMirrorGlobalOptions: EditorConfiguration = { mode: `xtext/${xtextLang}`, indentUnit: 2, - theme: 'material-darker', + styleActiveLine: true, }; export class EditorStore { + themeStore; + atom; editor?: Editor; @@ -36,9 +41,11 @@ export class EditorStore { showLineNumbers = false; - constructor() { + constructor(themeStore: ThemeStore) { + this.themeStore = themeStore; this.atom = createAtom('EditorStore'); makeAutoObservable(this, { + themeStore: false, atom: false, editor: observable.ref, xtextServices: observable.ref, @@ -65,8 +72,8 @@ export class EditorStore { if (this.editor) { removeServices(this.editor); } - this.editor = undefined; - this.xtextServices = undefined; + delete this.editor; + delete this.xtextServices; } /** @@ -89,6 +96,7 @@ export class EditorStore { get codeMirrorOptions(): EditorConfiguration { return { ...codeMirrorGlobalOptions, + theme: this.themeStore.codeMirrorTheme, lineNumbers: this.showLineNumbers, }; } diff --git a/language-web/src/main/js/global.d.ts b/language-web/src/main/js/global.d.ts new file mode 100644 index 00000000..39bda7f3 --- /dev/null +++ b/language-web/src/main/js/global.d.ts @@ -0,0 +1,5 @@ +declare module '*.module.scss' { + const cssVariables: { [key in string]?: string }; + // eslint-disable-next-line import/no-default-export + export default cssVariables; +} diff --git a/language-web/src/main/js/index.tsx b/language-web/src/main/js/index.tsx index 24f0b69d..1f08feeb 100644 --- a/language-web/src/main/js/index.tsx +++ b/language-web/src/main/js/index.tsx @@ -2,11 +2,11 @@ import { CacheProvider } from '@emotion/react'; import React from 'react'; import { render } from 'react-dom'; import CssBaseline from '@material-ui/core/CssBaseline'; -import { ThemeProvider, createTheme } from '@material-ui/core/styles'; import { getCache } from 'tss-react/cache'; import { App } from './App'; import { RootStore, RootStoreProvider } from './RootStore'; +import { ThemeProvider } from './theme/ThemeProvider'; import '../css/index.scss'; @@ -49,31 +49,15 @@ scope Family = 1, Person += 5..10. const rootStore = new RootStore(); rootStore.editorStore.updateValue(initialValue); -const theme = createTheme({ - palette: { - mode: 'dark', - background: { - default: '#212121', - paper: '#2f2f2f', - }, - primary: { - main: '#82aaff', - }, - secondary: { - main: '#ff5370', - }, - }, -}); - const app = ( - - - - + + + + - - - + + + ); render(app, document.getElementById('app')); diff --git a/language-web/src/main/js/theme/EditorTheme.ts b/language-web/src/main/js/theme/EditorTheme.ts new file mode 100644 index 00000000..9420dafa --- /dev/null +++ b/language-web/src/main/js/theme/EditorTheme.ts @@ -0,0 +1,47 @@ +import { PaletteMode } from '@material-ui/core'; + +import cssVariables from '../../css/themeVariables.module.scss'; + +export enum EditorTheme { + Light, + Dark, +} + +export class EditorThemeData { + className: string; + + paletteMode: PaletteMode; + + toggleDarkMode: EditorTheme; + + foreground!: string; + + background!: string; + + paper!: string; + + primary!: string; + + secondary!: string; + + constructor(className: string, paletteMode: PaletteMode, toggleDarkMode: EditorTheme) { + this.className = className; + this.paletteMode = paletteMode; + this.toggleDarkMode = toggleDarkMode; + Reflect.ownKeys(this).forEach((key) => { + if (!Reflect.get(this, key)) { + const cssKey = `${this.className}--${key.toString()}`; + if (cssKey in cssVariables) { + Reflect.set(this, key, cssVariables[cssKey]); + } + } + }); + } +} + +export const DEFAULT_THEME = EditorTheme.Dark; + +export const EDITOR_THEMES: { [key in EditorTheme]: EditorThemeData } = { + [EditorTheme.Light]: new EditorThemeData('light', 'light', EditorTheme.Dark), + [EditorTheme.Dark]: new EditorThemeData('dark', 'dark', EditorTheme.Light), +}; diff --git a/language-web/src/main/js/theme/ThemeProvider.tsx b/language-web/src/main/js/theme/ThemeProvider.tsx new file mode 100644 index 00000000..e7574725 --- /dev/null +++ b/language-web/src/main/js/theme/ThemeProvider.tsx @@ -0,0 +1,15 @@ +import { ThemeProvider as MaterialUiThemeProvider } from '@material-ui/core/styles'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; + +import { useRootStore } from '../RootStore'; + +export const ThemeProvider: React.FC = observer(({ children }) => { + const { themeStore } = useRootStore(); + + return ( + + {children} + + ); +}); diff --git a/language-web/src/main/js/theme/ThemeStore.ts b/language-web/src/main/js/theme/ThemeStore.ts new file mode 100644 index 00000000..0f283c98 --- /dev/null +++ b/language-web/src/main/js/theme/ThemeStore.ts @@ -0,0 +1,53 @@ +import { + Theme, + createTheme, + responsiveFontSizes, +} from '@material-ui/core/styles'; +import { makeAutoObservable } from 'mobx'; + +import { + EditorTheme, + EditorThemeData, + DEFAULT_THEME, + EDITOR_THEMES, +} from './EditorTheme'; + +export class ThemeStore { + currentTheme: EditorTheme = DEFAULT_THEME; + + constructor() { + makeAutoObservable(this); + } + + toggleDarkMode(): void { + this.currentTheme = this.currentThemeData.toggleDarkMode; + } + + private get currentThemeData(): EditorThemeData { + return EDITOR_THEMES[this.currentTheme]; + } + + get materialUiTheme(): Theme { + const themeData = this.currentThemeData; + const materialUiTheme = createTheme({ + palette: { + mode: themeData.paletteMode, + background: { + default: themeData.background, + paper: themeData.paper, + }, + primary: { + main: themeData.primary, + }, + secondary: { + main: themeData.secondary, + }, + }, + }); + return responsiveFontSizes(materialUiTheme); + } + + get codeMirrorTheme(): string { + return `problem-${this.currentThemeData.className}`; + } +} -- cgit v1.2.3-54-g00ecf