generated from nhcarrigan/template
feat: add theme support for code editor
- Add light theme with GitHub-inspired colors - Add high contrast theme with VS Code high contrast colors - Dynamically switch themes when user changes app theme - Use CodeMirror Compartment for efficient theme reconfiguration - Support dark, light, high-contrast, and custom (uses dark) themes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { EditorView, basicSetup } from "codemirror";
|
import { EditorView, basicSetup } from "codemirror";
|
||||||
import { EditorState } from "@codemirror/state";
|
import { EditorState, Compartment } from "@codemirror/state";
|
||||||
import { keymap } from "@codemirror/view";
|
import { keymap } from "@codemirror/view";
|
||||||
import { oneDark } from "@codemirror/theme-one-dark";
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
import { javascript } from "@codemirror/lang-javascript";
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
@@ -32,7 +32,10 @@
|
|||||||
import { toml } from "@codemirror/legacy-modes/mode/toml";
|
import { toml } from "@codemirror/legacy-modes/mode/toml";
|
||||||
import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile";
|
import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile";
|
||||||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||||
|
import { tags } from "@lezer/highlight";
|
||||||
import { editorStore } from "$lib/stores/editor";
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
import type { EditorTab } from "$lib/types/editor";
|
import type { EditorTab } from "$lib/types/editor";
|
||||||
import type { Extension } from "@codemirror/state";
|
import type { Extension } from "@codemirror/state";
|
||||||
|
|
||||||
@@ -40,6 +43,274 @@
|
|||||||
|
|
||||||
let editorContainer: HTMLDivElement;
|
let editorContainer: HTMLDivElement;
|
||||||
let view: EditorView | null = null;
|
let view: EditorView | null = null;
|
||||||
|
let themeCompartment = new Compartment();
|
||||||
|
|
||||||
|
// Subscribe to theme changes
|
||||||
|
const config = configStore.config;
|
||||||
|
|
||||||
|
// Light theme
|
||||||
|
const lightTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
"&": {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
caretColor: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-cursor, .cm-dropCursor": {
|
||||||
|
borderLeftColor: "#24292e",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
},
|
||||||
|
".cm-panels": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-top": {
|
||||||
|
borderBottom: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-bottom": {
|
||||||
|
borderTop: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-searchMatch": {
|
||||||
|
backgroundColor: "#ffdf5d",
|
||||||
|
outline: "1px solid #c4a000",
|
||||||
|
},
|
||||||
|
".cm-searchMatch.cm-searchMatch-selected": {
|
||||||
|
backgroundColor: "#c4a000",
|
||||||
|
},
|
||||||
|
".cm-activeLine": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
},
|
||||||
|
".cm-selectionMatch": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
},
|
||||||
|
".cm-matchingBracket, .cm-nonmatchingBracket": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
outline: "1px solid #888",
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
color: "#6a737d",
|
||||||
|
border: "none",
|
||||||
|
borderRight: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-activeLineGutter": {
|
||||||
|
backgroundColor: "#e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-foldPlaceholder": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "#6a737d",
|
||||||
|
},
|
||||||
|
".cm-tooltip": {
|
||||||
|
border: "1px solid #e1e4e8",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||||
|
borderTopColor: "#ffffff",
|
||||||
|
borderBottomColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete": {
|
||||||
|
"& > ul > li[aria-selected]": {
|
||||||
|
backgroundColor: "#e1e4e8",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const lightHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.keyword, color: "#d73a49" },
|
||||||
|
{
|
||||||
|
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||||
|
color: "#6f42c1",
|
||||||
|
},
|
||||||
|
{ tag: [tags.function(tags.variableName), tags.labelName], color: "#6f42c1" },
|
||||||
|
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#005cc5" },
|
||||||
|
{ tag: [tags.definition(tags.name), tags.separator], color: "#24292e" },
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.typeName,
|
||||||
|
tags.className,
|
||||||
|
tags.number,
|
||||||
|
tags.changed,
|
||||||
|
tags.annotation,
|
||||||
|
tags.modifier,
|
||||||
|
tags.self,
|
||||||
|
tags.namespace,
|
||||||
|
],
|
||||||
|
color: "#e36209",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.operator,
|
||||||
|
tags.operatorKeyword,
|
||||||
|
tags.url,
|
||||||
|
tags.escape,
|
||||||
|
tags.regexp,
|
||||||
|
tags.link,
|
||||||
|
tags.special(tags.string),
|
||||||
|
],
|
||||||
|
color: "#032f62",
|
||||||
|
},
|
||||||
|
{ tag: [tags.meta, tags.comment], color: "#6a737d" },
|
||||||
|
{ tag: tags.strong, fontWeight: "bold" },
|
||||||
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
|
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||||
|
{ tag: tags.link, color: "#032f62", textDecoration: "underline" },
|
||||||
|
{ tag: tags.heading, fontWeight: "bold", color: "#005cc5" },
|
||||||
|
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#005cc5" },
|
||||||
|
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#22863a" },
|
||||||
|
{ tag: tags.invalid, color: "#cb2431" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// High contrast theme
|
||||||
|
const highContrastTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
"&": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
caretColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-cursor, .cm-dropCursor": {
|
||||||
|
borderLeftColor: "#ffffff",
|
||||||
|
borderLeftWidth: "2px",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
},
|
||||||
|
".cm-panels": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-top": {
|
||||||
|
borderBottom: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-bottom": {
|
||||||
|
borderTop: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-searchMatch": {
|
||||||
|
backgroundColor: "#515c6a",
|
||||||
|
outline: "2px solid #ffff00",
|
||||||
|
},
|
||||||
|
".cm-searchMatch.cm-searchMatch-selected": {
|
||||||
|
backgroundColor: "#ffff00",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
".cm-activeLine": {
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
},
|
||||||
|
".cm-selectionMatch": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
},
|
||||||
|
".cm-matchingBracket, .cm-nonmatchingBracket": {
|
||||||
|
backgroundColor: "#515c6a",
|
||||||
|
outline: "2px solid #ffff00",
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#858585",
|
||||||
|
border: "none",
|
||||||
|
borderRight: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-activeLineGutter": {
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-foldPlaceholder": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "#ffff00",
|
||||||
|
},
|
||||||
|
".cm-tooltip": {
|
||||||
|
border: "2px solid #ffffff",
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||||
|
borderTopColor: "#000000",
|
||||||
|
borderBottomColor: "#000000",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete": {
|
||||||
|
"& > ul > li[aria-selected]": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const highContrastHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.keyword, color: "#569cd6", fontWeight: "bold" },
|
||||||
|
{
|
||||||
|
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||||
|
color: "#9cdcfe",
|
||||||
|
},
|
||||||
|
{ tag: [tags.function(tags.variableName), tags.labelName], color: "#dcdcaa" },
|
||||||
|
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#4fc1ff" },
|
||||||
|
{ tag: [tags.definition(tags.name), tags.separator], color: "#ffffff" },
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.typeName,
|
||||||
|
tags.className,
|
||||||
|
tags.number,
|
||||||
|
tags.changed,
|
||||||
|
tags.annotation,
|
||||||
|
tags.modifier,
|
||||||
|
tags.self,
|
||||||
|
tags.namespace,
|
||||||
|
],
|
||||||
|
color: "#4ec9b0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.operator,
|
||||||
|
tags.operatorKeyword,
|
||||||
|
tags.url,
|
||||||
|
tags.escape,
|
||||||
|
tags.regexp,
|
||||||
|
tags.link,
|
||||||
|
tags.special(tags.string),
|
||||||
|
],
|
||||||
|
color: "#d4d4d4",
|
||||||
|
},
|
||||||
|
{ tag: [tags.meta, tags.comment], color: "#6a9955" },
|
||||||
|
{ tag: tags.strong, fontWeight: "bold" },
|
||||||
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
|
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||||
|
{ tag: tags.link, color: "#3794ff", textDecoration: "underline" },
|
||||||
|
{ tag: tags.heading, fontWeight: "bold", color: "#569cd6" },
|
||||||
|
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#569cd6" },
|
||||||
|
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#ce9178" },
|
||||||
|
{ tag: tags.invalid, color: "#f44747" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getThemeExtension(theme: string): Extension {
|
||||||
|
switch (theme) {
|
||||||
|
case "light":
|
||||||
|
return [lightTheme, syntaxHighlighting(lightHighlightStyle)];
|
||||||
|
case "high-contrast":
|
||||||
|
return [highContrastTheme, syntaxHighlighting(highContrastHighlightStyle)];
|
||||||
|
case "dark":
|
||||||
|
case "custom":
|
||||||
|
default:
|
||||||
|
return oneDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getLanguageExtension(language: string): Extension {
|
function getLanguageExtension(language: string): Extension {
|
||||||
const languageMap: Record<string, () => Extension> = {
|
const languageMap: Record<string, () => Extension> = {
|
||||||
@@ -84,6 +355,8 @@
|
|||||||
function createEditor() {
|
function createEditor() {
|
||||||
if (!editorContainer) return;
|
if (!editorContainer) return;
|
||||||
|
|
||||||
|
const currentTheme = $config.theme;
|
||||||
|
|
||||||
const saveKeymap = keymap.of([
|
const saveKeymap = keymap.of([
|
||||||
{
|
{
|
||||||
key: "Mod-s",
|
key: "Mod-s",
|
||||||
@@ -105,7 +378,7 @@
|
|||||||
doc: tab.content,
|
doc: tab.content,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
oneDark,
|
themeCompartment.of(getThemeExtension(currentTheme)),
|
||||||
getLanguageExtension(tab.language),
|
getLanguageExtension(tab.language),
|
||||||
saveKeymap,
|
saveKeymap,
|
||||||
updateListener,
|
updateListener,
|
||||||
@@ -120,10 +393,6 @@
|
|||||||
".cm-content": {
|
".cm-content": {
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
||||||
},
|
},
|
||||||
".cm-gutters": {
|
|
||||||
backgroundColor: "var(--bg-secondary)",
|
|
||||||
borderRight: "1px solid var(--border-color)",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -141,6 +410,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for theme changes and update the editor
|
||||||
|
$: if (view && $config.theme) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: themeCompartment.reconfigure(getThemeExtension($config.theme)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
createEditor();
|
createEditor();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user