Skip to content

Commit

Permalink
Merge pull request #53 from vimdotmd/refactor-theme-color
Browse files Browse the repository at this point in the history
Refactor: Define theme colors in JS instead of CSS
  • Loading branch information
Thien Do authored Sep 4, 2021
2 parents 00febd1 + 423dc8d commit 70b0016
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 118 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@parcel/transformer-image": "2.0.0-rc.0",
"@parcel/transformer-typescript-tsc": "^2.0.0-rc.0",
"@parcel/validator-typescript": "^2.0.0-rc.0",
"@types/color": "^3.0.2",
"@types/css-font-loading-module": "^0.0.6",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
Expand All @@ -38,6 +39,7 @@
},
"dependencies": {
"@tippyjs/react": "^4.2.5",
"color": "^4.0.1",
"idb-keyval": "^5.1.3",
"modern-normalize": "^1.1.0",
"monaco-editor": "^0.27.0",
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useFile } from "~/src/components/file/state";
import { Layout } from "~/src/components/layout/layout";
import { Toolbar } from "~/src/components/toolbar/toolbar";
import { usePrefs } from "~src/components/prefs/state";
import { ThemeInject } from "~src/components/prefs/theme/inject";
import s from "./app.module.css";
import { AppDrop } from "./drop/drop";
import { useAppDrop } from "./drop/state";
Expand All @@ -20,6 +21,7 @@ export const App = (): JSX.Element => {
return (
<div className={s.app} {...drop.handlers}>
<AppTitle file={file} />
<ThemeInject theme={prefs.theme} />
<div
className={[s.toolbar, toolbar.mute ? s.muted : ""].join(" ")}
ref={toolbar.ref}
Expand Down
21 changes: 0 additions & 21 deletions src/components/editor/state/theme/base.ts

This file was deleted.

27 changes: 16 additions & 11 deletions src/components/editor/state/theme/colors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as monaco from "monaco-editor";
import { ThemeBaseColors } from "./base";
import { ThemeColors } from "~src/components/prefs/theme/theme";

// Generated with "./debug.ts"
/**
* Returns monaco theme's colors (i.e. colors for UI elements such as search
* box and mini map)
*
* Generated with "./debug.ts"
*/
export const getEditorThemeColors = (
base: ThemeBaseColors
theme: ThemeColors
): monaco.editor.IColors => ({
// Overall foreground color. This color is only used if not overridden by a component.
// "foreground": ,
Expand Down Expand Up @@ -102,9 +107,9 @@ export const getEditorThemeColors = (
// Border color of hint boxes in the editor.
// "editorHint.border": ,
// Editor background color.
"editor.background": base.bg,
"editor.background": theme.bg.hex(),
// Editor default foreground color.
"editor.foreground": base.text,
"editor.foreground": theme.text.hex(),
// Background color of editor widgets, such as find/replace.
// "editorWidget.background": ,
// Foreground color of editor widgets, such as find/replace.
Expand Down Expand Up @@ -132,7 +137,7 @@ export const getEditorThemeColors = (
// Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.
// "keybindingLabel.bottomBorder": ,
// Color of the editor selection.
"editor.selectionBackground": `${base.sub}80`,
"editor.selectionbackground": `${theme.sub.hex()}80`,
// Color of the selected text for high contrast.
// "editor.selectionForeground": ,
// Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations.
Expand All @@ -142,9 +147,9 @@ export const getEditorThemeColors = (
// Border color for regions with the same content as the selection.
// "editor.selectionHighlightBorder": ,
// Color of the current search match.
"editor.findMatchBackground": `${base.main}80`,
"editor.findMatchBackground": `${theme.main.hex()}80`,
// Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.
"editor.findMatchHighlightBackground": `${base.main}40`,
"editor.findMatchHighlightBackground": `${theme.main.hex()}40`,
// Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.
// "editor.findRangeHighlightBackground": ,
// Border color of the current search match.
Expand Down Expand Up @@ -288,17 +293,17 @@ export const getEditorThemeColors = (
// Background color for the border around the line at the cursor position.
// "editor.lineHighlightBorder": ,
// Background color of highlighted ranges, like by quick open and find features. The color must not be opaque so as not to hide underlying decorations.
"editor.rangeHighlightBackground": `${base.sub}40`,
"editor.rangeHighlightBackground": `${theme.sub.hex()}40`,
// Background color of the border around highlighted ranges.
// "editor.rangeHighlightBorder": ,
// Background color of highlighted symbol, like for go to definition or go next/previous symbol. The color must not be opaque so as not to hide underlying decorations.
// "editor.symbolHighlightBackground": ,
// Background color of the border around highlighted symbols.
// "editor.symbolHighlightBorder": ,
// Color of the editor cursor.
"editorCursor.foreground": base.caret,
"editorCursor.foreground": theme.caret.hex(),
// The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.
"editorCursor.background": base.text,
"editorCursor.background": theme.text.hex(),
// Color of whitespace characters in the editor.
// "editorWhitespace.foreground": ,
// Color of the editor indentation guides.
Expand Down
50 changes: 29 additions & 21 deletions src/components/editor/state/theme/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as monaco from "monaco-editor";
import { ThemeBaseColors } from "./base";
import { ThemeColors } from "~src/components/prefs/theme/theme";

type Rule = monaco.editor.ITokenThemeRule;

Expand All @@ -17,36 +17,44 @@ interface Options {
code: "duo" | "colorful";
}

/**
* Return monaco theme's rules (i.e. colors for tokens such as keywords and
* comments)
*/
export const getEditorThemeRules = (
base: ThemeBaseColors,
theme: ThemeColors,
options: Options
): Rule[] => {
const rules: Rule[] = [];

// Basic style
rules.push(
{ token: "", foreground: base.text },
{ token: "invalid", foreground: base.error },
{ token: "", foreground: theme.text.hex() },
{ token: "invalid", foreground: theme.error.hex() },
{ token: "emphasis", fontStyle: "italic" },
{ token: "strong", fontStyle: "bold" }
);

// Mute markdown coloring
rules.push(
{ token: "comment.md", foreground: base.sub },
{ token: "keyword.md", foreground: base.text, fontStyle: "bold" },
{ token: "keyword.table.header.md", foreground: base.text },
{ token: "keyword.table.middle.md", foreground: base.text },
{ token: "keyword.table.left.md", foreground: base.text },
{ token: "keyword.table.right.md", foreground: base.text },
{ token: "string.md", foreground: base.text },
{ token: "string.link.md", foreground: base.sub },
{ token: "comment.md", foreground: theme.sub.hex() },
{ token: "keyword.md", foreground: theme.text.hex(), fontStyle: "bold" },
{ token: "keyword.table.header.md", foreground: theme.text.hex() },
{ token: "keyword.table.middle.md", foreground: theme.text.hex() },
{ token: "keyword.table.left.md", foreground: theme.text.hex() },
{ token: "keyword.table.right.md", foreground: theme.text.hex() },
{ token: "string.md", foreground: theme.text.hex() },
{ token: "string.link.md", foreground: theme.sub.hex() },
// Background doesn't work yet: https://github.com/microsoft/monaco-editor/issues/586
{ token: "variable.md", foreground: base.text, background: base.sub },
{ token: "tag.md", foreground: base.sub },
{ token: "string.html.md", foreground: base.sub },
{ token: "delimiter.html.md", foreground: base.sub },
{ token: "attribute.name.html.md", foreground: base.sub }
{
token: "variable.md",
foreground: theme.text.hex(),
background: theme.sub.hex(),
},
{ token: "tag.md", foreground: theme.sub.hex() },
{ token: "string.html.md", foreground: theme.sub.hex() },
{ token: "delimiter.html.md", foreground: theme.sub.hex() },
{ token: "attribute.name.html.md", foreground: theme.sub.hex() }
);

switch (options.code) {
Expand All @@ -67,10 +75,10 @@ export const getEditorThemeRules = (
break;
case "duo":
rules.push(
{ token: "keyword", foreground: base.main },
{ token: "comment", foreground: base.sub },
{ token: "number", foreground: base.main },
{ token: "tag", foreground: base.main }
{ token: "keyword", foreground: theme.main.hex() },
{ token: "comment", foreground: theme.sub.hex() },
{ token: "number", foreground: theme.main.hex() },
{ token: "tag", foreground: theme.main.hex() }
);
break;
}
Expand Down
29 changes: 11 additions & 18 deletions src/components/editor/state/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,32 @@ import * as monaco from "monaco-editor";
import { useEffect } from "react";
import { EditorState } from "~src/components/editor/state/state";
import { PrefsState } from "~src/components/prefs/state";
import { getThemeBaseColors } from "./base";
import { THEME_COLORS } from "~src/components/prefs/theme/theme";
import { getEditorThemeColors } from "./colors";
import { getEditorThemeRules } from "./rules";

const updateTheme = (): void => {
const base = getThemeBaseColors();
monaco.editor.defineTheme("custom", {
base: "vs-dark",
inherit: false,
colors: getEditorThemeColors(base),
rules: getEditorThemeRules(base, { code: "colorful" }),
});
monaco.editor.setTheme("custom");
};

interface Params {
editor: EditorState;
prefs: PrefsState;
}

export const useEditorTheme = (params: Params): void => {
const editor = params.editor.value;
const theme = params.prefs.theme;
const name = params.prefs.theme;

useEffect(() => {
if (editor === null) return;
// This actually does not depend on the "editor" at all, but a global
// option of Monaco. We intentionally ask for the "editor" instance for
// completeness.

// This will get the colors from getComputedStyle, which means it should
// be delayed because it takes time for React to actually update the
// theme class on "html"
window.setTimeout(() => updateTheme(), 0);
}, [theme, editor]);
const theme = THEME_COLORS[name];
monaco.editor.defineTheme("custom", {
base: "vs-dark",
inherit: false,
colors: getEditorThemeColors(theme), // UI colors
rules: getEditorThemeRules(theme, { code: "colorful" }), // Token colors
});
monaco.editor.setTheme("custom");
}, [name, editor]);
};
6 changes: 3 additions & 3 deletions src/components/prefs/prefs.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PrefsState } from "./state";
import { Theme, THEMES } from "./theme/state";
import { ThemeName, THEME_NAMES } from "./theme/theme";

interface Props {
prefs: PrefsState;
Expand All @@ -10,10 +10,10 @@ export const Prefs = (props: Props): JSX.Element => (
<select
value={props.prefs.theme}
onChange={(event) => {
props.prefs.setTheme(event.target.value as Theme);
props.prefs.setTheme(event.target.value as ThemeName);
}}
>
{THEMES.map((theme) => (
{THEME_NAMES.map((theme) => (
<option key={theme} value={theme}>
{theme}
</option>
Expand Down
22 changes: 22 additions & 0 deletions src/components/prefs/theme/inject.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Color from "color";
import { ThemeColors, ThemeName, THEME_COLORS } from "./theme";

interface Props {
theme: ThemeName;
}

const getCss = (theme: ThemeName): string => {
const variables: string[] = [];
const colors = THEME_COLORS[theme];
Object.keys(colors).forEach((key) => {
const color = colors[key as keyof ThemeColors] as Color;
variables.push(`--${key}-color: ${color.hex()};`);
const rgb = `${color.red()}, ${color.green()}, ${color.blue()}`;
variables.push(`--${key}-color-rgb: ${rgb};`);
});
return [":root {", variables.join("\n"), "}"].join("\n");
};

export const ThemeInject = (props: Props): JSX.Element => (
<style>{getCss(props.theme)}</style>
);
27 changes: 5 additions & 22 deletions src/components/prefs/theme/state.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import { useEffect } from "react";
import { SetState } from "~src/utils/state/type";
import { useStorageState } from "~src/utils/state/storage";
import "./styles/bushido.css";
import "./styles/serika-dark.css";

export const THEMES = ["bushido", "serika-dark"] as const;

export type Theme = typeof THEMES[number];
import { SetState } from "~src/utils/state/type";
import { ThemeName } from "./theme";

export interface ThemeState {
theme: Theme;
setTheme: SetState<Theme>;
theme: ThemeName;
setTheme: SetState<ThemeName>;
}

const htmlClass = document.documentElement.classList;

const useThemeApply = (theme: Theme): void => {
useEffect(() => {
htmlClass.add(`theme-${theme}`);
return () => void htmlClass.remove(`theme-${theme}`);
}, [theme]);
};

export const usePrefsTheme = (): ThemeState => {
const [theme, setTheme] = useStorageState<Theme>({
const [theme, setTheme] = useStorageState<ThemeName>({
storageKey: "theme",
defaultValue: "bushido",
});

useThemeApply(theme);

return { theme, setTheme };
};
11 changes: 0 additions & 11 deletions src/components/prefs/theme/styles/bushido.css

This file was deleted.

11 changes: 0 additions & 11 deletions src/components/prefs/theme/styles/serika-dark.css

This file was deleted.

Loading

0 comments on commit 70b0016

Please sign in to comment.