Просмотр исходного кода

Merge branch 'dev/7.0.x' into support/139066-customtheme-classic

satof3 2 лет назад
Родитель
Сommit
eef9952508

+ 37 - 11
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -2,7 +2,9 @@ import React, {
   useCallback, useState, useRef, useEffect,
 } from 'react';
 
-import { useResolvedThemeForEditor } from '@growi/editor';
+import {
+  CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
+} from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -79,7 +81,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     decrement: decrementEditingCommentsNum,
   } = useSWRxEditingCommentsNum();
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
-
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.COMMENT);
   const { resolvedTheme } = useNextThemes();
   mutateResolvedTheme(resolvedTheme);
 
@@ -143,6 +145,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     if (editingCommentsNum != null && editingCommentsNum === 0) {
       mutateIsEnabledUnsavedWarning(false); // must be after clearing comment or else onChange will override bool
     }
+
   }, [initializeSlackEnabled, comment, decrementEditingCommentsNum, mutateIsEnabledUnsavedWarning]);
 
   const cancelButtonClickedHandler = useCallback(() => {
@@ -186,15 +189,17 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
       if (onCommentButtonClicked != null) {
         onCommentButtonClicked();
       }
+
+      // Insert empty string as new comment editor is opened after comment
+      codeMirrorEditor?.initDoc('');
     }
     catch (err) {
       const errorMessage = err.message || 'An unknown error occured when posting comment';
       setError(errorMessage);
     }
   }, [
-    comment, currentCommentId, initializeEditor,
-    isSlackEnabled, onCommentButtonClicked, replyTo, slackChannels,
-    postComment, revisionId, updateComment,
+    currentCommentId, initializeEditor, onCommentButtonClicked, codeMirrorEditor,
+    updateComment, comment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment,
   ]);
 
   const ctrlEnterHandler = useCallback((event) => {
@@ -267,14 +272,32 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     );
   }, []);
 
-  const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
+  // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
+  //   setComment(newValue);
+  //   if (!isClean && !incremented) {
+  //     incrementEditingCommentsNum();
+  //     setIncremented(true);
+  //   }
+  //   mutateIsEnabledUnsavedWarning(!isClean);
+  // }, [mutateIsEnabledUnsavedWarning, incrementEditingCommentsNum, incremented]);
+
+  const onChangeHandler = useCallback((newValue: string) => {
     setComment(newValue);
-    if (!isClean && !incremented) {
+
+    if (!incremented) {
       incrementEditingCommentsNum();
       setIncremented(true);
     }
-    mutateIsEnabledUnsavedWarning(!isClean);
-  }, [mutateIsEnabledUnsavedWarning, incrementEditingCommentsNum, incremented]);
+  }, [incrementEditingCommentsNum, incremented]);
+
+  // initialize CodeMirrorEditor
+  useEffect(() => {
+    if (commentBody == null) {
+      return;
+    }
+    codeMirrorEditor?.initDoc(commentBody);
+  }, [codeMirrorEditor, commentBody]);
+
 
   const renderReady = () => {
     const commentPreview = getCommentHtml();
@@ -311,7 +334,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
           <TabContent activeTab={activeTab}>
             <TabPane tabId="comment_editor">
-              <Editor
+              <CodeMirrorEditorComment
+                onChange={onChangeHandler}
+              />
+              {/* <Editor
                 ref={editorRef}
                 value={commentBody ?? ''} // DO NOT use state
                 isUploadable={isUploadable}
@@ -320,7 +346,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onUpload={uploadHandler}
                 onCtrlEnter={ctrlEnterHandler}
                 isComment
-              />
+              /> */}
               {/*
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
                 See a review comment in https://github.com/weseek/growi/pull/3473

+ 66 - 55
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -14,7 +14,7 @@ import { DEFAULT_THEME, KeyMapMode } from '../../interfaces/editor-settings';
 
 
 const AVAILABLE_THEMES = [
-  'eclipse', 'elegant', 'neo', 'mdn-like', 'material', 'dracula', 'monokai', 'twilight',
+  'DefaultLight', 'Eclipse', 'Basic', 'Ayu', 'Rosé Pine', 'DefaultDark', 'Material', 'Nord', 'Cobalt', 'Kimbie',
 ];
 
 const TYPICAL_INDENT_SIZE = [2, 4];
@@ -22,12 +22,18 @@ const TYPICAL_INDENT_SIZE = [2, 4];
 
 const ThemeSelector = (): JSX.Element => {
 
+  const [isThemeMenuOpened, setIsThemeMenuOpened] = useState(false);
+
   const { data: editorSettings, update } = useEditorSettings();
 
   const menuItems = useMemo(() => (
     <>
       { AVAILABLE_THEMES.map((theme) => {
-        return <button key={theme} className="dropdown-item" type="button" onClick={() => update({ theme })}>{theme}</button>;
+        return (
+          <DropdownItem className="menuitem-label" onClick={() => update({ theme })}>
+            {theme}
+          </DropdownItem>
+        );
       }) }
     </>
   ), [update]);
@@ -39,21 +45,21 @@ const ThemeSelector = (): JSX.Element => {
       <div>
         <span className="input-group-text" id="igt-theme">Theme</span>
       </div>
-      <div className="dropup">
-        <button
-          type="button"
-          className="btn btn-outline-secondary dropdown-toggle"
-          data-bs-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="false"
-          aria-describedby="igt-theme"
-        >
+
+      <Dropdown
+        direction="up"
+        isOpen={isThemeMenuOpened}
+        toggle={() => setIsThemeMenuOpened(!isThemeMenuOpened)}
+      >
+        <DropdownToggle color="outline-secondary" caret>
           {selectedTheme}
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
           {menuItems}
-        </div>
-      </div>
+        </DropdownMenu>
+
+      </Dropdown>
     </div>
   );
 };
@@ -72,9 +78,10 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = {
 
 const KeymapSelector = memo((): JSX.Element => {
 
+  const [isKeyMenuOpened, setIsKeyMenuOpened] = useState(false);
+
   const { data: editorSettings, update } = useEditorSettings();
 
-  Object.keys(KEYMAP_LABEL_MAP);
   const menuItems = useMemo(() => (
     <>
       { (Object.keys(KEYMAP_LABEL_MAP) as KeyMapMode[]).map((keymapMode) => {
@@ -82,7 +89,11 @@ const KeymapSelector = memo((): JSX.Element => {
         const icon = (keymapMode !== 'default')
           ? <img src={`/images/icons/${keymapMode}.png`} width="16px" className="me-2"></img>
           : null;
-        return <button key={keymapMode} className="dropdown-item" type="button" onClick={() => update({ keymapMode })}>{icon}{keymapLabel}</button>;
+        return (
+          <DropdownItem className="menuitem-label" onClick={() => update({ keymapMode })}>
+            {icon}{keymapLabel}
+          </DropdownItem>
+        );
       }) }
     </>
   ), [update]);
@@ -91,24 +102,21 @@ const KeymapSelector = memo((): JSX.Element => {
 
   return (
     <div className="input-group flex-nowrap">
-      <div>
-        <span className="input-group-text" id="igt-keymap">Keymap</span>
-      </div>
-      <div className="dropup">
-        <button
-          type="button"
-          className="btn btn-outline-secondary dropdown-toggle"
-          data-bs-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="false"
-          aria-describedby="igt-keymap"
-        >
-          { editorSettings != null && selectedKeymapMode}
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
+      <span className="input-group-text" id="igt-keymap">Keymap</span>
+      <Dropdown
+        direction="up"
+        isOpen={isKeyMenuOpened}
+        toggle={() => setIsKeyMenuOpened(!isKeyMenuOpened)}
+      >
+        <DropdownToggle color="outline-secondary" caret>
+          {selectedKeymapMode}
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
           {menuItems}
-        </div>
-      </div>
+        </DropdownMenu>
+
+      </Dropdown>
     </div>
   );
 
@@ -123,38 +131,41 @@ type IndentSizeSelectorProps = {
 }
 
 const IndentSizeSelector = memo(({ isIndentSizeForced, selectedIndentSize, onChange }: IndentSizeSelectorProps): JSX.Element => {
+
+  const [isIndentMenuOpened, setIsIndentMenuOpened] = useState(false);
+
   const menuItems = useMemo(() => (
     <>
       { TYPICAL_INDENT_SIZE.map((indent) => {
-        return <button key={indent} className="dropdown-item" type="button" onClick={() => onChange(indent)}>{indent}</button>;
+        return (
+          <DropdownItem className="menuitem-label" onClick={() => onChange(indent)}>
+            {indent}
+          </DropdownItem>
+        );
       }) }
     </>
   ), [onChange]);
 
   return (
     <div className="input-group flex-nowrap">
-      <div>
-        <span className="input-group-text" id="igt-indent">Indent</span>
-      </div>
-      <div className="dropup">
-        <button
-          type="button"
-          className="btn btn-outline-secondary dropdown-toggle"
-          data-bs-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="false"
-          aria-describedby="igt-indent"
-          disabled={isIndentSizeForced}
-        >
+      <span className="input-group-text" id="igt-indent">Indent</span>
+      <Dropdown
+        direction="up"
+        isOpen={isIndentMenuOpened}
+        toggle={() => setIsIndentMenuOpened(!isIndentMenuOpened)}
+        disabled={isIndentSizeForced}
+      >
+        <DropdownToggle color="outline-secondary" caret>
           {selectedIndentSize}
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
           {menuItems}
-        </div>
-      </div>
+        </DropdownMenu>
+
+      </Dropdown>
     </div>
   );
-
 });
 
 IndentSizeSelector.displayName = 'IndentSizeSelector';
@@ -228,7 +239,7 @@ const ConfigurationDropdown = memo((): JSX.Element => {
           <i className="icon-settings"></i>
         </DropdownToggle>
 
-        <DropdownMenu>
+        <DropdownMenu container="body">
           {renderActiveLineMenuItem()}
           {renderMarkdownTableAutoFormattingMenuItem()}
           {/* <DropdownItem divider /> */}
@@ -254,7 +265,7 @@ export const OptionsSelector = (): JSX.Element => {
 
   return (
     <>
-      <div className="d-flex flex-row">
+      <div className="d-flex flex-row zindex-dropdown">
         <span>
           <ThemeSelector />
         </span>

+ 4 - 0
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -16,6 +16,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 
+
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
@@ -28,6 +29,7 @@ import {
   useIsEditable, useIsUploadAllFileAllowed, useIsUploadEnabled, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
+  useEditorSettings,
   useCurrentIndentSize, useIsSlackEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
   useIsConflict,
@@ -112,6 +114,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: isUploadAllFileAllowed } = useIsUploadAllFileAllowed();
   const { data: isUploadEnabled } = useIsUploadEnabled();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { data: editorSettings } = useEditorSettings();
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
   const { mutate: mutateRemotePageId } = useRemoteRevisionId();
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
@@ -501,6 +504,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             onScroll={scrollEditorHandlerThrottle}
             indentSize={currentIndentSize ?? defaultIndentSize}
             acceptedFileType={acceptedFileType}
+            editorTheme={editorSettings?.theme}
           />
         </div>
         <div ref={previewRef} onScroll={scrollPreviewHandlerThrottle} className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">

+ 7 - 1
packages/editor/package.json

@@ -22,15 +22,21 @@
   },
   "devDependencies": {
     "@codemirror/lang-markdown": "^6.2.0",
-    "@codemirror/language-data": "^6.3.1",
     "@codemirror/language": "^6.8.0",
+    "@codemirror/language-data": "^6.3.1",
     "@codemirror/state": "^6.2.1",
     "@codemirror/view": "^6.15.3",
     "@popperjs/core": "^2.11.8",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",
+    "@uiw/codemirror-theme-eclipse": "^4.21.21",
+    "@uiw/codemirror-theme-kimbie": "^4.21.21",
+    "@uiw/codemirror-themes": "^4.21.21",
     "@uiw/react-codemirror": "^4.21.8",
     "bootstrap": "^5.3.1",
+    "cm6-theme-basic-light": "^0.2.0",
+    "cm6-theme-material-dark": "^0.2.0",
+    "cm6-theme-nord": "^0.2.0",
     "codemirror": "^6.0.1",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",

+ 21 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -3,11 +3,12 @@ import {
 } from 'react';
 
 import { indentUnit } from '@codemirror/language';
+import { Prec } from '@codemirror/state';
 import { EditorView } from '@codemirror/view';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
 import { GlobalCodeMirrorEditorKey, AcceptedUploadFileType } from '../../consts';
-import { useFileDropzone, FileDropzoneOverlay } from '../../services';
+import { useFileDropzone, FileDropzoneOverlay, AllEditorTheme } from '../../services';
 import {
   getStrFromBol, adjustPasteData,
 } from '../../services/list-util/markdown-list-util';
@@ -31,6 +32,7 @@ type Props = {
   onUpload?: (files: File[]) => void,
   onScroll?: () => void,
   indentSize?: number,
+  editorTheme?: string,
 }
 
 export const CodeMirrorEditor = (props: Props): JSX.Element => {
@@ -41,6 +43,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     onUpload,
     onScroll,
     indentSize,
+    editorTheme,
   } = props;
 
   const containerRef = useRef(null);
@@ -136,6 +139,23 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
   }, [onScroll, codeMirrorEditor]);
 
+  useEffect(() => {
+    if (editorTheme == null) {
+      return;
+    }
+    if (AllEditorTheme[editorTheme] == null) {
+      return;
+    }
+
+    const extension = AllEditorTheme[editorTheme];
+
+    // React CodeMirror has default theme which is default prec
+    // and extension have to be higher prec here than default theme.
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(Prec.high(extension));
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, editorTheme]);
+
   const {
     getRootProps,
     isDragActive,

+ 3 - 1
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -24,11 +24,12 @@ type Props = {
   onScroll?: () => void,
   acceptedFileType?: AcceptedUploadFileType,
   indentSize?: number,
+  editorTheme?: string,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    onSave, onChange, onUpload, onScroll, acceptedFileType, indentSize,
+    onSave, onChange, onUpload, onScroll, acceptedFileType, indentSize, editorTheme,
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
@@ -73,6 +74,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
       onScroll={onScroll}
       acceptedFileType={acceptedFileTypeNoOpt}
       indentSize={indentSize}
+      editorTheme={editorTheme}
     />
   );
 };

+ 1 - 0
packages/editor/src/components/index.ts

@@ -1,2 +1,3 @@
 export * from './CodeMirrorEditor';
 export * from './CodeMirrorEditorMain';
+export * from './CodeMirrorEditorComment';

+ 3 - 1
packages/editor/src/components/playground/Playground.tsx

@@ -14,6 +14,7 @@ import { Preview } from './Preview';
 export const Playground = (): JSX.Element => {
 
   const [markdownToPreview, setMarkdownToPreview] = useState('');
+  const [editorTheme, setEditorTheme] = useState('');
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
@@ -61,11 +62,12 @@ export const Playground = (): JSX.Element => {
             onUpload={uploadHandler}
             indentSize={4}
             acceptedFileType={AcceptedUploadFileType.ALL}
+            editorTheme={editorTheme}
           />
         </div>
         <div className="flex-expand-vert d-none d-lg-flex bg-light text-dark border-start border-dark-subtle p-3">
           <Preview markdown={markdownToPreview} />
-          <PlaygroundController />
+          <PlaygroundController setEditorTheme={setEditorTheme} />
         </div>
       </div>
       <div className="flex-expand-vert justify-content-center align-items-center bg-dark" style={{ minHeight: '50px' }}>

+ 49 - 1
packages/editor/src/components/playground/PlaygroundController.tsx

@@ -3,6 +3,9 @@ import { useCallback } from 'react';
 import { useForm } from 'react-hook-form';
 
 import { GlobalCodeMirrorEditorKey } from '../../consts';
+import {
+  AllEditorTheme,
+} from '../../services';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 
 export const InitEditorValueRow = (): JSX.Element => {
@@ -68,11 +71,56 @@ export const SetCaretLineRow = (): JSX.Element => {
   );
 };
 
-export const PlaygroundController = (): JSX.Element => {
+type SetThemeRowProps = {
+  setEditorTheme: (value: string) => void,
+}
+const SetThemeRow = (props: SetThemeRowProps): JSX.Element => {
+
+  const { setEditorTheme } = props;
+
+  const createItems = (items: string[]): JSX.Element => {
+    return (
+      <div>
+        { items.map((theme) => {
+          return (
+            <button
+              type="button"
+              className="btn btn-outline-secondary"
+              onClick={() => {
+                setEditorTheme(theme);
+              }}
+            >{theme}
+            </button>
+          );
+        }) }
+      </div>
+    );
+  };
+
+  return (
+    <>
+      <div className="row mt-3">
+        <h2>default</h2>
+        <div className="col">
+          {createItems(Object.keys(AllEditorTheme))}
+        </div>
+      </div>
+    </>
+  );
+};
+
+
+type PlaygroundControllerProps = {
+  setEditorTheme: (value: string) => void
+};
+
+export const PlaygroundController = (props: PlaygroundControllerProps): JSX.Element => {
+  const { setEditorTheme } = props;
   return (
     <div className="container mt-5">
       <InitEditorValueRow />
       <SetCaretLineRow />
+      <SetThemeRow setEditorTheme={setEditorTheme} />
     </div>
   );
 };

+ 80 - 0
packages/editor/src/services/editor-theme/ayu.ts

@@ -0,0 +1,80 @@
+// Ref: https://github.com/vadimdemedes/thememirror/blob/94a6475a9113ec03d880fcb817aadcc5a16e82e4/source/themes/ayu-light.ts
+
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+// Author: Konstantin Pschera
+export const ayu = createTheme({
+  theme: 'light',
+  settings: {
+    background: '#fcfcfc',
+    foreground: '#5c6166',
+    caret: '#ffaa33',
+    selection: '#036dd626',
+    gutterBackground: '#fcfcfc',
+    gutterForeground: '#8a919966',
+    lineHighlight: '#8a91991a',
+  },
+  styles: [
+    {
+      tag: t.comment,
+      color: '#787b8099',
+    },
+    {
+      tag: t.string,
+      color: '#86b300',
+    },
+    {
+      tag: t.regexp,
+      color: '#4cbf99',
+    },
+    {
+      tag: [t.number, t.bool, t.null],
+      color: '#ffaa33',
+    },
+    {
+      tag: t.variableName,
+      color: '#5c6166',
+    },
+    {
+      tag: [t.definitionKeyword, t.modifier],
+      color: '#fa8d3e',
+    },
+    {
+      tag: [t.keyword, t.special(t.brace)],
+      color: '#fa8d3e',
+    },
+    {
+      tag: t.operator,
+      color: '#ed9366',
+    },
+    {
+      tag: t.separator,
+      color: '#5c6166b3',
+    },
+    {
+      tag: t.punctuation,
+      color: '#5c6166',
+    },
+    {
+      tag: [t.definition(t.propertyName), t.function(t.variableName)],
+      color: '#f2ae49',
+    },
+    {
+      tag: [t.className, t.definition(t.typeName)],
+      color: '#22a4e6',
+    },
+    {
+      tag: [t.tagName, t.typeName, t.self, t.labelName],
+      color: '#55b4d4',
+    },
+    {
+      tag: t.angleBracket,
+      color: '#55b4d480',
+    },
+    {
+      tag: t.attributeName,
+      color: '#f2ae49',
+    },
+  ],
+});

+ 86 - 0
packages/editor/src/services/editor-theme/cobalt.ts

@@ -0,0 +1,86 @@
+// Ref: https://github.com/vadimdemedes/thememirror/blob/94a6475a9113ec03d880fcb817aadcc5a16e82e4/source/themes/cobalt.ts
+
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+// Author: Jacob Rus
+export const cobalt = createTheme({
+  theme: 'dark',
+  settings: {
+    background: '#00254b',
+    foreground: '#FFFFFF',
+    caret: '#FFFFFF',
+    selection: '#B36539BF',
+    gutterBackground: '#00254b',
+    gutterForeground: '#FFFFFF70',
+    lineHighlight: '#00000059',
+  },
+  styles: [
+    {
+      tag: t.comment,
+      color: '#0088FF',
+    },
+    {
+      tag: t.string,
+      color: '#3AD900',
+    },
+    {
+      tag: t.regexp,
+      color: '#80FFC2',
+    },
+    {
+      tag: [t.number, t.bool, t.null],
+      color: '#FF628C',
+    },
+    {
+      tag: [t.definitionKeyword, t.modifier],
+      color: '#FFEE80',
+    },
+    {
+      tag: t.variableName,
+      color: '#CCCCCC',
+    },
+    {
+      tag: t.self,
+      color: '#FF80E1',
+    },
+    {
+      tag: [
+        t.className,
+        t.definition(t.propertyName),
+        t.function(t.variableName),
+        t.definition(t.typeName),
+        t.labelName,
+      ],
+      color: '#FFDD00',
+    },
+    {
+      tag: [t.keyword, t.operator],
+      color: '#FF9D00',
+    },
+    {
+      tag: [t.propertyName, t.typeName],
+      color: '#80FFBB',
+    },
+    {
+      tag: t.special(t.brace),
+      color: '#EDEF7D',
+    },
+    {
+      tag: t.attributeName,
+      color: '#9EFFFF',
+    },
+    {
+      tag: t.derefOperator,
+      color: '#fff',
+    },
+    {
+      tag: [t.url, t.escape],
+      color: '#497DBA',
+    },
+    {
+      tag: [t.brace, t.processingInstruction, t.inserted],
+      color: '#7491B4',
+    },
+  ],
+});

+ 26 - 0
packages/editor/src/services/editor-theme/index.ts

@@ -0,0 +1,26 @@
+import { Extension } from '@codemirror/state';
+import { eclipse } from '@uiw/codemirror-theme-eclipse';
+import { kimbie } from '@uiw/codemirror-theme-kimbie';
+import { basicLight } from 'cm6-theme-basic-light';
+import { materialDark as materialDarkCM6 } from 'cm6-theme-material-dark';
+import { nord as nordCM6 } from 'cm6-theme-nord';
+
+import { ayu } from './ayu';
+import { cobalt } from './cobalt';
+import { originalDark } from './original-dark';
+import { originalLight } from './original-light';
+import { rosePine } from './rose-pine';
+
+
+export const AllEditorTheme: Record<string, Extension> = {
+  DefaultLight: originalLight,
+  Eclipse: eclipse,
+  Basic: basicLight,
+  Ayu: ayu,
+  'Rosé Pine': rosePine,
+  DefaultDark: originalDark,
+  Material: materialDarkCM6,
+  Nord: nordCM6,
+  Cobalt: cobalt,
+  Kimbie: kimbie,
+};

+ 32 - 0
packages/editor/src/services/editor-theme/original-dark.ts

@@ -0,0 +1,32 @@
+// Ref: https://github.com/uiwjs/react-codemirror/blob/bf3b862923d0cb04ccf4bb9da0791bdc7fd6d29b/themes/sublime/src/index.ts
+
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+export const originalDark = createTheme({
+  theme: 'dark',
+  settings: {
+    background: '#303841',
+    foreground: '#FFFFFF',
+    caret: '#FBAC52',
+    selection: '#4C5964',
+    selectionMatch: '#3A546E',
+    gutterBackground: '#303841',
+    gutterForeground: '#FFFFFF70',
+    lineHighlight: '#00000059',
+  },
+  styles: [
+    { tag: [t.meta, t.comment], color: '#A2A9B5' },
+    { tag: [t.attributeName, t.keyword], color: '#B78FBA' },
+    { tag: t.function(t.variableName), color: '#5AB0B0' },
+    { tag: [t.string, t.regexp, t.attributeValue], color: '#99C592' },
+    { tag: t.operator, color: '#f47954' },
+    // { tag: t.moduleKeyword, color: 'red' },
+    { tag: [t.tagName, t.modifier], color: '#E35F63' },
+    { tag: [t.number, t.definition(t.tagName), t.className, t.definition(t.variableName)], color: '#fbac52' },
+    { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#E35F63' },
+    { tag: t.variableName, color: '#539ac4' },
+    { tag: [t.propertyName, t.typeName], color: '#629ccd' },
+    { tag: t.propertyName, color: '#36b7b5' },
+  ],
+});

+ 35 - 0
packages/editor/src/services/editor-theme/original-light.ts

@@ -0,0 +1,35 @@
+// Ref: https://github.com/uiwjs/react-codemirror/blob/bf3b862923d0cb04ccf4bb9da0791bdc7fd6d29b/themes/github/src/index.ts
+
+import { Extension } from '@codemirror/state';
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+
+export const originalLight: Extension = createTheme({
+  theme: 'light',
+  settings: {
+    background: '#fff',
+    foreground: '#24292e',
+    selection: '#BBDFFF',
+    selectionMatch: '#BBDFFF',
+    gutterBackground: '#fff',
+    gutterForeground: '#6e7781',
+  },
+  styles: [
+    { tag: [t.standard(t.tagName), t.tagName], color: '#116329' },
+    { tag: [t.comment, t.bracket], color: '#6a737d' },
+    { tag: [t.className, t.propertyName], color: '#6f42c1' },
+    { tag: [t.variableName, t.attributeName, t.number, t.operator], color: '#005cc5' },
+    { tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#d73a49' },
+    { tag: [t.string, t.meta, t.regexp], color: '#032f62' },
+    { tag: [t.name, t.quote], color: '#22863a' },
+    { tag: [t.heading, t.strong], color: '#24292e', fontWeight: 'bold' },
+    { tag: [t.emphasis], color: '#24292e', fontStyle: 'italic' },
+    { tag: [t.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
+    { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#e36209' },
+    { tag: [t.url, t.escape, t.regexp, t.link], color: '#032f62' },
+    { tag: t.link, textDecoration: 'underline' },
+    { tag: t.strikethrough, textDecoration: 'line-through' },
+    { tag: t.invalid, color: '#cb2431' },
+  ],
+});

+ 60 - 0
packages/editor/src/services/editor-theme/rose-pine.ts

@@ -0,0 +1,60 @@
+// Ref: https://github.com/vadimdemedes/thememirror/blob/94a6475a9113ec03d880fcb817aadcc5a16e82e4/source/themes/rose-pine-dawn.ts
+
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+// Author: Rosé Pine
+export const rosePine = createTheme({
+  theme: 'light',
+  settings: {
+    background: '#faf4ed',
+    foreground: '#575279',
+    caret: '#575279',
+    selection: '#6e6a8614',
+    gutterBackground: '#faf4ed',
+    gutterForeground: '#57527970',
+    lineHighlight: '#6e6a860d',
+  },
+  styles: [
+    {
+      tag: t.comment,
+      color: '#9893a5',
+    },
+    {
+      tag: [t.bool, t.null],
+      color: '#286983',
+    },
+    {
+      tag: t.number,
+      color: '#d7827e',
+    },
+    {
+      tag: t.className,
+      color: '#d7827e',
+    },
+    {
+      tag: [t.angleBracket, t.tagName, t.typeName],
+      color: '#56949f',
+    },
+    {
+      tag: t.attributeName,
+      color: '#907aa9',
+    },
+    {
+      tag: t.punctuation,
+      color: '#797593',
+    },
+    {
+      tag: [t.keyword, t.modifier],
+      color: '#286983',
+    },
+    {
+      tag: [t.string, t.regexp],
+      color: '#ea9d34',
+    },
+    {
+      tag: t.variableName,
+      color: '#d7827e',
+    },
+  ],
+});

+ 1 - 0
packages/editor/src/services/index.ts

@@ -1,2 +1,3 @@
 export * from './codemirror-editor';
 export * from './file-dropzone';
+export * from './editor-theme';

+ 38 - 0
yarn.lock

@@ -4458,6 +4458,29 @@
     "@codemirror/state" "^6.0.0"
     "@codemirror/view" "^6.0.0"
 
+"@uiw/codemirror-theme-eclipse@^4.21.21":
+  version "4.21.21"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-eclipse/-/codemirror-theme-eclipse-4.21.21.tgz#d38cf20ce903b7aecefb9dbe1751a240590f154f"
+  integrity sha512-Dp5j4mFPH8UOoH37b2Wc45khNGcyusCDbfRw0jeBAGW258xH4UbHBlEIY+1/z4bloIfoguCyE3nPQnsa/M59Qg==
+  dependencies:
+    "@uiw/codemirror-themes" "4.21.21"
+
+"@uiw/codemirror-theme-kimbie@^4.21.21":
+  version "4.21.21"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-kimbie/-/codemirror-theme-kimbie-4.21.21.tgz#dbdfc23c3957d55015ab5b0463526abffe73d816"
+  integrity sha512-dhWqIz1nsFzqoe5U3jIPeCJ9/c534YMmsGvNq3JJgRjD/KZeV8TSOJfuJNxI6jCskXh149Z5wghKE+FnNp/eUA==
+  dependencies:
+    "@uiw/codemirror-themes" "4.21.21"
+
+"@uiw/codemirror-themes@4.21.21", "@uiw/codemirror-themes@^4.21.21":
+  version "4.21.21"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.21.21.tgz#26efb06ecce9a51aa73d39311c90f8fcb06fdc43"
+  integrity sha512-ljVcMGdaxo75UaH+EqxJ+jLyMVVgeSfW2AKyT1VeLy+4SDpuqNQ7wq5XVxktsG6LH+OvgSFndWXgPANf4+gQcA==
+  dependencies:
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.0.0"
+
 "@uiw/react-codemirror@^4.21.8":
   version "4.21.8"
   resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.21.8.tgz#0b2d833a0c7256c23f83b342463276c762863bad"
@@ -6081,6 +6104,21 @@ clsx@^1.0.4, clsx@^1.1.1:
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
   integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
 
+cm6-theme-basic-light@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz#29d2d6b9675feb7b563b31eda6f3da37d9ae3167"
+  integrity sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==
+
+cm6-theme-material-dark@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/cm6-theme-material-dark/-/cm6-theme-material-dark-0.2.0.tgz#c733243a8a31da5d953fa551b2548f358aa37a64"
+  integrity sha512-H09JZihzg4w0mTtOqo5bQdxItkQWw+ergKlk7BSfwYjaR2nOi+wIN0R+ByAo7bON8GbFODvjTxH3EIqdhovFeA==
+
+cm6-theme-nord@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/cm6-theme-nord/-/cm6-theme-nord-0.2.0.tgz#2a00c47cdf6119b8248dbed8d9b572841bf321a7"
+  integrity sha512-jTh+5nvl+N/5CtTK7UVcrxDCj2AOStvbNM8uP6tx6amq4QaaLDlapjMw+MNzEkvxcPnHY+YM91tbklS2KNlR2w==
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"