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

Merge branch 'dev/7.0.x' into feat/multiple-group-grant-for-page

Futa Arai 2 лет назад
Родитель
Сommit
26c487861f
55 измененных файлов с 1358 добавлено и 346 удалено
  1. 3 0
      apps/app/public/static/locales/en_US/translation.json
  2. 3 0
      apps/app/public/static/locales/ja_JP/translation.json
  3. 3 0
      apps/app/public/static/locales/zh_CN/translation.json
  4. 3 0
      apps/app/src/components/Common/ClosableTextInput.tsx
  5. 37 11
      apps/app/src/components/PageComment/CommentEditor.tsx
  6. 1 1
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  7. 66 55
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  8. 7 2
      apps/app/src/components/PageEditor/PageEditor.tsx
  9. 28 0
      apps/app/src/components/PageHeader/PageHeader.tsx
  10. 130 0
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  11. 35 0
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  12. 76 0
      apps/app/src/components/PageHeader/TextInputForPageTitleAndPath.tsx
  13. 55 0
      apps/app/src/components/PageHeader/page-header-utils.ts
  14. 8 9
      apps/app/src/components/PageSelectModal/PageSelectModal.tsx
  15. 2 1
      apps/app/src/components/PageSelectModal/TreeItemForModal.tsx
  16. 1 1
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  17. 0 2
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  18. 1 1
      apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx
  19. 30 21
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  20. 73 37
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  21. 1 3
      apps/app/src/components/Sidebar/Tag.tsx
  22. 2 2
      apps/app/src/components/TagCloudBox.tsx
  23. 27 0
      apps/app/src/components/TagList.module.scss
  24. 11 6
      apps/app/src/components/TagList.tsx
  25. 0 31
      apps/app/src/styles/_tag.scss
  26. 18 0
      apps/app/src/styles/atoms/_tag.scss
  27. 1 1
      apps/app/src/styles/style-app.scss
  28. 10 10
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts
  29. 3 3
      apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts
  30. 2 2
      apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--sticky-for-guest.cy.ts
  31. 1 1
      packages/core/scss/bootstrap/apply.scss
  32. 12 0
      packages/core/scss/bootstrap/override/_helpers.scss
  33. 8 0
      packages/core/scss/bootstrap/override/helpers/_color-bg.scss
  34. 9 9
      packages/core/scss/bootstrap/theming/_variables.scss
  35. 1 1
      packages/core/scss/bootstrap/utilities.scss
  36. 7 1
      packages/editor/package.json
  37. 21 1
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  38. 3 1
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  39. 1 0
      packages/editor/src/components/index.ts
  40. 3 1
      packages/editor/src/components/playground/Playground.tsx
  41. 49 1
      packages/editor/src/components/playground/PlaygroundController.tsx
  42. 80 0
      packages/editor/src/services/editor-theme/ayu.ts
  43. 86 0
      packages/editor/src/services/editor-theme/cobalt.ts
  44. 26 0
      packages/editor/src/services/editor-theme/index.ts
  45. 32 0
      packages/editor/src/services/editor-theme/original-dark.ts
  46. 35 0
      packages/editor/src/services/editor-theme/original-light.ts
  47. 60 0
      packages/editor/src/services/editor-theme/rose-pine.ts
  48. 1 0
      packages/editor/src/services/index.ts
  49. BIN
      packages/preset-themes/public/images/halloween/halloween-navbar.jpg
  50. BIN
      packages/preset-themes/public/images/halloween/halloween.jpg
  51. 3 0
      packages/preset-themes/src/consts/preset-themes.ts
  52. 67 0
      packages/preset-themes/src/styles/classic.scss
  53. 176 130
      packages/preset-themes/src/styles/halloween.scss
  54. 2 1
      packages/preset-themes/vite.themes.config.ts
  55. 38 0
      yarn.lock

+ 3 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -822,5 +822,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "The attachment could not be found"
+  },
+  "page_select_modal": {
+    "select_page_location": "Select page location"
   }
 }

+ 3 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -855,5 +855,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "アタッチメントが見つかりません"
+  },
+  "page_select_modal": {
+    "select_page_location": "ページの場所を選択"
   }
 }

+ 3 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -825,5 +825,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "没有找到附件"
+  },
+  "page_select_modal": {
+    "select_page_location": "选择页面位置"
   }
 }

+ 3 - 0
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -12,6 +12,7 @@ type ClosableTextInputProps = {
   validationTarget?: string,
   onPressEnter?(inputText: string | null): void
   onClickOutside?(): void
+  handleInputChange?: (string) => void
 }
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
@@ -38,6 +39,8 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     createValidation(inputText);
     setInputText(inputText);
     setIsAbleToShowAlert(true);
+
+    props.handleInputChange?.(inputText);
   };
 
   const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {

+ 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

+ 1 - 1
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -73,7 +73,7 @@ const EditorNavbarBottom = (): JSX.Element => {
   const isCollapsedOptionsSelectorEnabled = !isDeviceLargerThanLg;
 
   return (
-    <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
+    <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `} data-testid="grw-editor-navbar-bottom">
       {/* Collapsed SlackNotification */}
       {isSlackConfigured && (
         <Collapse isOpen={isSlackExpanded && !isDeviceLargerThanLg}>

+ 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>

+ 7 - 2
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,
@@ -54,6 +56,7 @@ import { useNextThemes } from '~/stores/use-next-themes';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
+import { PageHeader } from '../PageHeader/PageHeader';
 
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 // import { ConflictDiffModal } from './ConflictDiffModal';
@@ -111,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();
@@ -476,8 +480,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   return (
     <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
-      <div className="flex-expand-vert justify-content-center align-items-center" style={{ minHeight: '72px' }}>
-        <div>Header</div>
+      <div className="flex-expand-vert justify-content-center" style={{ minHeight: '72px' }}>
+        <PageHeader />
       </div>
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert">
@@ -500,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">

+ 28 - 0
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -0,0 +1,28 @@
+import { FC } from 'react';
+
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+
+import { PagePathHeader } from './PagePathHeader';
+import { PageTitleHeader } from './PageTitleHeader';
+
+export const PageHeader: FC = () => {
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: currentPage } = useSWRxCurrentPage();
+
+  if (currentPage == null || currentPagePath == null) {
+    return <></>;
+  }
+
+  return (
+    <>
+      <PagePathHeader
+        currentPagePath={currentPagePath}
+        currentPage={currentPage}
+      />
+      <PageTitleHeader
+        currentPagePath={currentPagePath}
+        currentPage={currentPage}
+      />
+    </>
+  );
+};

+ 130 - 0
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -0,0 +1,130 @@
+import {
+  FC, useEffect, useMemo, useState,
+} from 'react';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+
+import { usePageSelectModal } from '~/stores/modal';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+
+import { PagePathNav } from '../Common/PagePathNav';
+import { PageSelectModal } from '../PageSelectModal/PageSelectModal';
+
+import { TextInputForPageTitleAndPath } from './TextInputForPageTitleAndPath';
+import { usePagePathRenameHandler } from './page-header-utils';
+
+type Props = {
+  currentPagePath: string
+  currentPage: IPagePopulatedToShowRevision
+}
+
+export const PagePathHeader: FC<Props> = (props) => {
+  const { currentPagePath, currentPage } = props;
+
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
+  const [isButtonsShown, setButtonShown] = useState(false);
+  const [inputText, setInputText] = useState('');
+
+  const { data: editorMode } = useEditorMode();
+  const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
+
+  const onRenameFinish = () => {
+    setRenameInputShown(false);
+  };
+
+  const onRenameFailure = () => {
+    setRenameInputShown(true);
+  };
+
+  const pagePathRenameHandler = usePagePathRenameHandler(currentPage, onRenameFinish, onRenameFailure);
+
+  const stateHandler = { isRenameInputShown, setRenameInputShown };
+
+  const isOpened = PageSelectModalData?.isOpened ?? false;
+
+  const isViewMode = editorMode === EditorMode.View;
+  const isEditorMode = !isViewMode;
+
+  const PagePath = useMemo(() => (
+    <>
+      {currentPagePath != null && (
+        <PagePathNav
+          pageId={currentPage._id}
+          pagePath={currentPagePath}
+          isSingleLineMode={isEditorMode}
+        />
+      )}
+    </>
+  ), [currentPage._id, currentPagePath, isEditorMode]);
+
+  const handleInputChange = (inputText: string) => {
+    setInputText(inputText);
+  };
+
+  const handleEditButtonClick = () => {
+    if (isRenameInputShown) {
+      pagePathRenameHandler(inputText);
+    }
+    else {
+      setRenameInputShown(true);
+    }
+  };
+
+  const buttonStyle = isButtonsShown ? '' : 'd-none';
+
+  const clickOutSideHandler = (e) => {
+    const container = document.getElementById('page-path-header');
+
+    if (container && !container.contains(e.target)) {
+      setRenameInputShown(false);
+    }
+  };
+
+  useEffect(() => {
+    document.addEventListener('click', clickOutSideHandler);
+
+    return () => {
+      document.removeEventListener('click', clickOutSideHandler);
+    };
+  }, []);
+
+  return (
+    <>
+      <div
+        id="page-path-header"
+        onMouseLeave={() => setButtonShown(false)}
+      >
+        <div className="row">
+          <div
+            className="col-4"
+            onMouseEnter={() => setButtonShown(true)}
+          >
+            <TextInputForPageTitleAndPath
+              currentPage={currentPage}
+              stateHandler={stateHandler}
+              inputValue={currentPagePath}
+              CustomComponent={PagePath}
+              handleInputChange={handleInputChange}
+            />
+          </div>
+          <div className={`${buttonStyle} col-4 row`}>
+            <div className="col-4">
+              <button type="button" onClick={handleEditButtonClick}>
+                {isRenameInputShown ? <span className="material-symbols-outlined">check_circle</span> : <span className="material-symbols-outlined">edit</span>}
+              </button>
+            </div>
+            <div className="col-4">
+              <button type="button" onClick={openPageSelectModal}>
+                <span className="material-symbols-outlined">account_tree</span>
+              </button>
+            </div>
+          </div>
+          {isOpened
+            && (
+              <PageSelectModal />
+            )}
+        </div>
+      </div>
+    </>
+  );
+};

+ 35 - 0
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -0,0 +1,35 @@
+import { FC, useState, useMemo } from 'react';
+
+import nodePath from 'path';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+
+import { TextInputForPageTitleAndPath } from './TextInputForPageTitleAndPath';
+
+type Props = {
+  currentPagePath: string,
+  currentPage: IPagePopulatedToShowRevision;
+}
+
+
+export const PageTitleHeader: FC<Props> = (props) => {
+  const { currentPagePath, currentPage } = props;
+
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
+  const pageName = nodePath.basename(currentPagePath ?? '') || '/';
+
+  const stateHandler = { isRenameInputShown, setRenameInputShown };
+
+  const PageTitle = useMemo(() => (<div onClick={() => setRenameInputShown(true)}>{pageName}</div>), [pageName]);
+
+  return (
+    <div onBlur={() => setRenameInputShown(false)}>
+      <TextInputForPageTitleAndPath
+        currentPage={currentPage}
+        stateHandler={stateHandler}
+        inputValue={pageName}
+        CustomComponent={PageTitle}
+      />
+    </div>
+  );
+};

+ 76 - 0
apps/app/src/components/PageHeader/TextInputForPageTitleAndPath.tsx

@@ -0,0 +1,76 @@
+import { FC, useCallback } from 'react';
+import type { Dispatch, SetStateAction } from 'react';
+
+import nodePath from 'path';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+
+import { ValidationTarget } from '~/client/util/input-validator';
+
+import ClosableTextInput from '../Common/ClosableTextInput';
+
+
+import { usePagePathRenameHandler } from './page-header-utils';
+
+
+type StateHandler = {
+  isRenameInputShown: boolean
+  setRenameInputShown: Dispatch<SetStateAction<boolean>>
+}
+
+type Props = {
+  currentPage: IPagePopulatedToShowRevision
+  stateHandler: StateHandler
+  inputValue: string
+  CustomComponent: JSX.Element
+  handleInputChange?: (string) => void
+}
+
+export const TextInputForPageTitleAndPath: FC<Props> = (props) => {
+  const {
+    currentPage, stateHandler, inputValue, CustomComponent, handleInputChange,
+  } = props;
+
+  const { t } = useTranslation();
+
+  const { isRenameInputShown, setRenameInputShown } = stateHandler;
+
+  const onRenameFinish = () => {
+    setRenameInputShown(false);
+  };
+
+  const onRenameFailure = () => {
+    setRenameInputShown(true);
+  };
+
+  const pagePathRenameHandler = usePagePathRenameHandler(currentPage, onRenameFinish, onRenameFailure);
+
+  const onPressEnter = useCallback((inputPagePath: string) => {
+
+    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path ?? ''));
+    const newPagePath = nodePath.resolve(parentPath, inputPagePath);
+
+    pagePathRenameHandler(newPagePath);
+
+  }, [currentPage.path, pagePathRenameHandler]);
+
+  return (
+    <>
+      {isRenameInputShown ? (
+        <div className="flex-fill">
+          <ClosableTextInput
+            value={inputValue}
+            placeholder={t('Input page name')}
+            onPressEnter={onPressEnter}
+            validationTarget={ValidationTarget.PAGE}
+            handleInputChange={handleInputChange}
+          />
+        </div>
+      ) : (
+        <>{ CustomComponent }</>
+      )}
+    </>
+  );
+};

+ 55 - 0
apps/app/src/components/PageHeader/page-header-utils.ts

@@ -0,0 +1,55 @@
+import { useCallback } from 'react';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRMUTxCurrentPage } from '~/stores/page';
+import { mutatePageTree, mutatePageList } from '~/stores/page-listing';
+
+export const usePagePathRenameHandler = (
+    currentPage: IPagePopulatedToShowRevision, onRenameFinish?: () => void, onRenameFailure?: () => void,
+): (newPagePath: string) => Promise<void> => {
+
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+  const { t } = useTranslation();
+
+  const currentPagePath = currentPage.path;
+
+  const pagePathRenameHandler = useCallback(async(newPagePath: string) => {
+
+    const onRenamed = (fromPath: string | undefined, toPath: string) => {
+      mutatePageTree();
+      mutatePageList();
+
+      if (currentPagePath === fromPath || currentPagePath === toPath) {
+        mutateCurrentPage();
+      }
+    };
+
+    if (newPagePath === currentPage.path || newPagePath === '') {
+      onRenameFinish?.();
+      return;
+    }
+
+    try {
+      onRenameFinish?.();
+      await apiv3Put('/pages/rename', {
+        pageId: currentPage._id,
+        revisionId: currentPage.revision._id,
+        newPagePath,
+      });
+
+      onRenamed(currentPage.path, newPagePath);
+
+      toastSuccess(t('renamed_pages', { path: currentPage.path }));
+    }
+    catch (err) {
+      onRenameFailure?.();
+      toastError(err);
+    }
+  }, [currentPage._id, currentPage.path, currentPage.revision._id, currentPagePath, mutateCurrentPage, onRenameFailure, onRenameFinish, t]);
+
+  return pagePathRenameHandler;
+};

+ 8 - 9
apps/app/src/components/PageSelectModal/PageSelectModal.tsx

@@ -1,5 +1,6 @@
-import React from 'react';
+import React, { FC } from 'react';
 
+import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter, Button,
 } from 'reactstrap';
@@ -13,7 +14,7 @@ import { ItemsTree } from '../ItemsTree';
 import { TreeItemForModal } from './TreeItemForModal';
 
 
-export const PageSelectModal = () => {
+export const PageSelectModal: FC = () => {
   const {
     data: PageSelectModalData,
     close: closeModal,
@@ -21,6 +22,8 @@ export const PageSelectModal = () => {
 
   const isOpened = PageSelectModalData?.isOpened ?? false;
 
+  const { t } = useTranslation();
+
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
@@ -40,8 +43,9 @@ export const PageSelectModal = () => {
       isOpen={isOpened}
       toggle={() => closeModal()}
       centered
+      size="sm"
     >
-      <ModalHeader toggle={() => closeModal()}>modal</ModalHeader>
+      <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
       <ModalBody>
         <ItemsTree
           CustomTreeItem={TreeItemForModal}
@@ -53,12 +57,7 @@ export const PageSelectModal = () => {
         />
       </ModalBody>
       <ModalFooter>
-        <Button color="primary">
-          Do Something
-        </Button>{' '}
-        <Button color="secondary">
-          Cancel
-        </Button>
+        <Button color="primary" onClick={closeModal}>{t('Done')}</Button>{' '}
       </ModalFooter>
     </Modal>
   );

+ 2 - 1
apps/app/src/components/PageSelectModal/TreeItemForModal.tsx

@@ -10,6 +10,7 @@ type PageTreeItemProps = TreeItemProps & {
 
 export const TreeItemForModal: FC<PageTreeItemProps> = (props) => {
 
+  const { isOpen } = props;
   const { Input: NewPageInput, CreateButton: NewPageCreateButton } = useNewPageInput();
 
   return (
@@ -17,7 +18,7 @@ export const TreeItemForModal: FC<PageTreeItemProps> = (props) => {
       key={props.key}
       targetPathOrId={props.targetPathOrId}
       itemNode={props.itemNode}
-      isOpen
+      isOpen={isOpen}
       isEnableActions={props.isEnableActions}
       isReadOnlyUser={props.isReadOnlyUser}
       onRenamed={props.onRenamed}

+ 1 - 1
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -30,7 +30,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
           <a
             key={tag}
             type="button"
-            className="grw-tag-label badge bg-primary me-2"
+            className="grw-tag badge me-2"
             onClick={() => pushState(`tag:${tag}`)}
           >
             {tag}

+ 0 - 2
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -45,8 +45,6 @@ const PageTreeUnavailable = () => {
 };
 
 export const PageTreeContent = memo(() => {
-  const { t } = useTranslation();
-
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();

+ 1 - 1
apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -158,7 +158,7 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
     <SimpleItem
       targetPathOrId={props.targetPathOrId}
       itemNode={props.itemNode}
-      isOpen
+      isOpen={isOpen}
       isEnableActions={props.isEnableActions}
       isReadOnlyUser={props.isReadOnlyUser}
       onRenamed={props.onRenamed}

+ 30 - 21
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -6,7 +6,7 @@
   transform: translateY(-2px);
 
   .form-check-label::before {
-    padding-left: 19px;
+    padding-left: 5px;
     content: 'L';
   }
 
@@ -17,6 +17,8 @@
 }
 
 .list-group-item :global {
+  font-size: 12px;
+
   .grw-recent-changes-skeleton-small {
     @include grw-skeleton-text($font-size:14px, $line-height:16px);
     max-width: 120px;
@@ -32,26 +34,6 @@
     width: 80px;
   }
 
-  .grw-recent-changes-item-lower {
-    height: 17.5px;
-  }
-  .footstamp-icon {
-    svg {
-      width: 14px;
-      height: 14px;
-      transform: translateY(-3.5px);
-    }
-  }
-
-  .grw-list-counts {
-    height: 14px;
-    font-size: 12px;
-  }
-
-  .grw-formatted-distance-date {
-    font-size: 10px;
-  }
-
   .icon-lock {
     font-size: 14px;
   }
@@ -65,3 +47,30 @@
     max-width: fit-content;
   }
 }
+
+
+.grw-recent-changes-item-lower :global {
+  font-size: 12px;
+
+  .material-symbols-outlined {
+    font-size: 14px;
+  }
+  .grw-formatted-distance-date {
+    font-size: 10px;
+  }
+}
+
+// == Colors
+.grw-former-link a {
+  --bs-link-opacity: .5;
+
+  &:global {
+    &:hover {
+      --bs-link-opacity: 1;
+    }
+  }
+}
+
+.grw-recent-changes-item-lower :global {
+  color: var(--bs-gray-500);
+}

+ 73 - 37
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -2,9 +2,11 @@ import React, {
   memo, useCallback, useEffect,
 } from 'react';
 
-import { isPopulated, type IPageHasId } from '@growi/core';
+import {
+  isPopulated, type IPageHasId,
+} from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
-import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
+import { UserPicture } from '@growi/ui/dist/components';
 
 import { useKeywordManager } from '~/client/services/search-operation';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
@@ -18,6 +20,8 @@ import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 import styles from './RecentChangesSubstance.module.scss';
 
+const formerLinkClass = styles['grw-former-link'];
+const pageItemLowerClass = styles['grw-recent-changes-item-lower'];
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:History');
@@ -33,14 +37,18 @@ type PageItemProps = PageItemLowerProps & {
 
 const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
   return (
-    <div className="d-flex justify-content-between grw-recent-changes-item-lower pt-1">
-      <div className="d-flex">
-        <div className="footstamp-icon me-1 d-inline-block"><FootstampIcon /></div>
-        <div className="me-2 grw-list-counts d-inline-block">{page.seenUsers.length}</div>
-        <div className="icon-bubble me-1 d-inline-block"></div>
-        <div className="me-2 grw-list-counts d-inline-block">{page.commentCount}</div>
+    <div className={`${pageItemLowerClass} d-flex justify-content-between grw-recent-changes-item-lower`}>
+      <div className="d-flex align-items-center">
+        <div className="">
+          <span className="material-symbols-outlined p-0">footprint</span>
+          <span className="grw-list-counts ms-1">{page.seenUsers.length}</span>
+        </div>
+        <div className="ms-2">
+          <span className="material-symbols-outlined p-0">chat</span>
+          <span className="grw-list-counts ms-1">{page.commentCount}</span>
+        </div>
       </div>
-      <div className="grw-formatted-distance-date small mt-auto" data-vrt-blackout-datetime>
+      <div className="grw-formatted-distance-date mt-auto" data-vrt-blackout-datetime>
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
       </div>
     </div>
@@ -48,12 +56,42 @@ const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
 });
 PageItemLower.displayName = 'PageItemLower';
 
+type PageTagsProps = PageItemProps;
+const PageTags = memo((props: PageTagsProps): JSX.Element => {
+  const { page, isSmall, onClickTag } = props;
+
+  if (isSmall || (page.tags.length === 0)) {
+    return <></>;
+  }
+
+  return (
+    <>
+      { page.tags.map((tag) => {
+        if (!isPopulated(tag)) {
+          return <></>;
+        }
+        return (
+          <a
+            key={tag.name}
+            type="button"
+            className="grw-tag badge me-2"
+            onClick={() => onClickTag?.(tag.name)}
+          >
+            {tag.name}
+          </a>
+        );
+      }) }
+    </>
+  );
+});
+PageTags.displayName = 'PageTags';
+
 const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Element => {
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
   const FormerLink = () => (
-    <div className="small">
+    <div className={`${formerLinkClass} small`}>
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
     </div>
   );
@@ -63,39 +101,37 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
     locked = <span><i className="icon-lock ms-2" /></span>;
   }
 
-  const tags = page.tags;
-  const tagElements = tags.map((tag) => {
-    if (!isPopulated(tag)) {
-      return <></>;
-    }
-    return (
-      <a
-        key={tag.name}
-        type="button"
-        className="grw-tag-label badge bg-primary me-2 small"
-        onClick={() => onClickTag?.(tag.name)}
-      >
-        {tag.name}
-      </a>
-    );
-  });
+  const isTagElementsRendered = !(isSmall || (page.tags.length === 0));
 
   return (
-    <li className={`list-group-item ${styles['list-group-item']} ${isSmall ? 'py-2' : 'py-3'} px-0`}>
+    <li className={`list-group-item ${styles['list-group-item']} py-2 px-0`}>
       <div className="d-flex w-100">
+
         <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
+
         <div className="flex-grow-1 ms-2">
-          { !dPagePath.isRoot && <FormerLink /> }
-          <h5 className={isSmall ? 'my-0 text-truncate' : 'my-2'}>
-            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            {locked}
-          </h5>
-          {!isSmall && (
-            <div className="grw-tag-labels mt-1 mb-2">
-              { tagElements }
+          <div className={`row ${isSmall ? 'gy-0' : 'gy-2'}`}>
+
+            <div className="col-12">
+              { !dPagePath.isRoot && <FormerLink /> }
             </div>
-          )}
-          <PageItemLower page={page} />
+
+            <h6 className={`col-12 ${isSmall ? 'mb-0 text-truncate' : 'mb-0'}`}>
+              <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+              {locked}
+            </h6>
+
+            { isTagElementsRendered && (
+              <div className="col-12">
+                <PageTags isSmall={isSmall} page={page} onClickTag={onClickTag} />
+              </div>
+            ) }
+
+            <div className="col-12">
+              <PageItemLower page={page} />
+            </div>
+
+          </div>
         </div>
       </div>
     </li>

+ 1 - 3
apps/app/src/components/Sidebar/Tag.tsx

@@ -81,9 +81,7 @@ const Tag: FC = () => {
 
       <h3 className="my-3">{t('popular_tags')}</h3>
 
-      <div className="text-center">
-        <TagCloudBox tags={tagCloudData} />
-      </div>
+      <TagCloudBox tags={tagCloudData} />
     </div>
   );
 

+ 2 - 2
apps/app/src/components/TagCloudBox.tsx

@@ -31,7 +31,7 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
       <a
         key={tag.name}
         type="button"
-        className="grw-tag-label badge bg-primary me-2"
+        className="grw-tag badge me-2"
         onClick={() => pushState(`tag:${tag.name}`)}
       >
         {tagNameFormat}
@@ -40,7 +40,7 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
   });
 
   return (
-    <div className="grw-popular-tag-labels">
+    <div>
       {tagElements}
     </div>
   );

+ 27 - 0
apps/app/src/components/TagList.module.scss

@@ -0,0 +1,27 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.grw-tag-list :global {
+  .list-group {
+    // remove border
+    --bs-border-width: 0;
+  }
+}
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .grw-tag-list :global {
+    .grw-tag-count {
+      color: var(--bs-gray-600);
+      background-color: var(--grw-highlight-100);
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-tag-list :global {
+    .grw-tag-count {
+      color: var(--bs-gray-500);
+      background-color: var(--grw-highlight-800);
+    }
+  }
+}

+ 11 - 6
apps/app/src/components/TagList.tsx

@@ -9,6 +9,11 @@ import { IDataTagCount } from '~/interfaces/tag';
 
 import PaginationWrapper from './PaginationWrapper';
 
+import styles from './TagList.module.scss';
+
+const moduleClass = styles['grw-tag-list'];
+
+
 type TagListProps = {
   tagData: IDataTagCount[],
   totalTags: number,
@@ -37,11 +42,11 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
         <button
           key={tag._id}
           type="button"
-          className="list-group-item list-group-item-action d-flex"
+          className="list-group-item list-group-item-action d-flex justify-content-between"
           onClick={() => pushState(`tag:${tag.name}`)}
         >
-          <div className="text-truncate list-tag-name">{tag.name}</div>
-          <div className="ms-4 my-auto py-1 px-2 list-tag-count badge bg-primary">{tag.count}</div>
+          <div className="text-truncate grw-tag badge">{tag.name}</div>
+          <div className="grw-tag-count badge">{tag.count}</div>
         </button>
       );
     });
@@ -52,8 +57,8 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
   }
 
   return (
-    <>
-      <div className="list-group text-start mb-5">
+    <div className={moduleClass}>
+      <div className="list-group list-group-flush mb-5">
         {generateTagList(tagData)}
       </div>
       {isPaginationShown
@@ -68,7 +73,7 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
         />
       )
       }
-    </>
+    </div>
   );
 
 };

+ 0 - 31
apps/app/src/styles/_tag.scss

@@ -1,31 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-.tags-page {
-  .list-tag-count {
-    background: rgba(0, 0, 0, 0.08);
-  }
-}
-
-.grw-popular-tag-labels {
-  text-align: left;
-
-  .grw-tag-label {
-    font-size: 10px;
-    font-weight: normal;
-    border-radius: bs.$border-radius;
-  }
-}
-
-#edit-tag-modal {
-  .form-control {
-    height: auto;
-  }
-}
-
-.grw-recent-changes {
-  .grw-tag-label {
-    font-size: 10px;
-    font-weight: normal;
-    border-radius: bs.$border-radius;
-  }
-}

+ 18 - 0
apps/app/src/styles/atoms/_tag.scss

@@ -0,0 +1,18 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+// == Colors
+@include bs.color-mode(light) {
+  .grw-tag.badge {
+    --bs-badge-color: var(--bs-gray-600);
+    background-color: var(--bs-gray-100);
+    border: 1px solid var(--bs-gray-300);
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-tag.badge {
+    --bs-badge-color: var(--bs-gray-500);
+    background-color: var(--bs-gray-800);
+    border: 1px solid var(--bs-gray-600);
+  }
+}
+

+ 1 - 1
apps/app/src/styles/style-app.scss

@@ -7,6 +7,7 @@
 @import 'atoms/spinners';
 @import 'atoms/custom_control';
 @import 'atoms/code';
+@import 'atoms/tag';
 
 // molecules
 @import 'molecules/toastr';
@@ -25,7 +26,6 @@
 @import 'mirror_mode';
 @import 'modal';
 @import 'share-link';
-@import 'tag';
 @import 'installer';
 
 

+ 10 - 10
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts

@@ -19,7 +19,7 @@ context('Access to any page', () => {
       // Scroll the window 250px down is enough to trigger sticky effect
        cy.scrollTo(0, 250);
       // wait until
-      return cy.getByTestid('grw-subnav-switcher').then($elem => !$elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('have.class', 'active');
     });
 
     cy.waitUntilSkeletonDisappear();
@@ -30,7 +30,7 @@ context('Access to any page', () => {
       // Scroll the window back to top
       cy.scrollTo(0, 0);
       // wait until
-      return cy.waitUntil(() => cy.getByTestid('grw-subnav-switcher').then($elem => $elem.hasClass('grw-subnav-switcher-hidden')));
+      return cy.get('.sticky-outer-wrapper').should('not.have.class', 'active');
     });
 
     cy.screenshot(`${ssPrefix}invisible-on-scroll-top`);
@@ -42,7 +42,7 @@ context('Access to any page', () => {
       // Scroll the window 250px down is enough to trigger sticky effect
       cy.scrollTo(0, 250);
       // wait until
-      return () => cy.getByTestid('grw-subnav-switcher').then($elem => !$elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('have.class', 'active');
     });
 
     // Move to /Sandbox page
@@ -51,7 +51,7 @@ context('Access to any page', () => {
     cy.waitUntilSkeletonDisappear();
     cy.collapseSidebar(true);
 
-    cy.waitUntil(() => cy.getByTestid('grw-subnav-switcher').then($elem => $elem.hasClass('grw-subnav-switcher-hidden')));
+    return cy.get('.sticky-outer-wrapper').should('not.have.class', 'active');
     cy.screenshot(`${ssPrefix}not-visible-on-move-to-other-pages`);
   });
 
@@ -61,17 +61,17 @@ context('Access to any page', () => {
       // Scroll the window 250px down is enough to trigger sticky effect
       cy.scrollTo(0, 250);
       // wait until
-      return cy.getByTestid('grw-subnav-switcher').then($elem => !$elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('have.class', 'active');
     });
     cy.waitUntil(() => {
-      cy.getByTestid('grw-subnav-switcher').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('editor-button').as('editorButton').should('be.visible');
         cy.get('@editorButton').click();
       });
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     });
-    cy.get('.grw-editor-navbar-bottom').should('be.visible');
-    cy.get('.CodeMirror').should('be.visible');
+    cy.getByTestid('grw-editor-navbar-bottom').should('be.visible');
+    // cy.get('.CodeMirror').should('be.visible');
     cy.screenshot(`${ssPrefix}open-editor-when-sticky`);
   });
 
@@ -81,11 +81,11 @@ context('Access to any page', () => {
       // Scroll the window 500px down
       cy.scrollTo(0, 500);
       // wait until
-      return cy.getByTestid('grw-subnav-switcher').then($elem => !$elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('have.class', 'active');
     });
     cy.waitUntilSkeletonDisappear();
     cy.viewport(600, 1024);
-    cy.getByTestid('grw-subnav-switcher').within(() => {
+    cy.getByTestid('grw-contextual-sub-nav').within(() => {
       cy.get('#grw-page-editor-mode-manager').should('be.visible');
     })
     cy.screenshot(`${ssPrefix}sticky-on-small-window`);

+ 3 - 3
apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts

@@ -22,7 +22,7 @@ context('Access to page by guest', () => {
     // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
     cy.get('#headers').invoke('removeClass', 'blink');
 
-    cy.waitUntilSkeletonDisappear();
+    // cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
 
@@ -36,7 +36,7 @@ context('Access to page by guest', () => {
 
     cy.get('.math').should('be.visible');
 
-    cy.waitUntilSkeletonDisappear();
+    // cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
 
@@ -44,7 +44,7 @@ context('Access to page by guest', () => {
     cy.visit('/Sandbox#edit');
     cy.collapseSidebar(true);
 
-    cy.waitUntilSkeletonDisappear();
+    // cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-with-edit-hash`);
   })
 

+ 2 - 2
apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--sticky-for-guest.cy.ts

@@ -12,7 +12,7 @@ context('Access sticky sub navigation switcher for guest', () => {
       // Scroll page down 250px
       cy.scrollTo(0, 250);
       // wait until
-      return cy.getByTestid('grw-subnav-switcher').then($elem => !$elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('have.class', 'active');
     });
     cy.screenshot(`${ssPrefix}subnav-switcher-is-sticky-on-scroll-down`);
 
@@ -22,7 +22,7 @@ context('Access sticky sub navigation switcher for guest', () => {
       // Scroll page to top
       cy.scrollTo(0, 0);
       // wait until
-      return cy.getByTestid('grw-subnav-switcher').then($elem => $elem.hasClass('grw-subnav-switcher-hidden'));
+      return cy.get('.sticky-outer-wrapper').should('not.have.class', 'active');
     });
     cy.screenshot(`${ssPrefix}subnav-switcher-is-not-sticky-on-scroll-top`);
   });

+ 1 - 1
packages/core/scss/bootstrap/apply.scss

@@ -36,7 +36,7 @@
 @import 'bootstrap/scss/placeholders';
 
 // Helpers
-@import 'bootstrap/scss/helpers';
+@import './override/helpers';
 
 // Utilities
 @import 'bootstrap/scss/utilities/api';

+ 12 - 0
packages/core/scss/bootstrap/override/_helpers.scss

@@ -0,0 +1,12 @@
+@import 'bootstrap/scss/helpers/clearfix';
+@import './helpers/color-bg';
+@import 'bootstrap/scss/helpers/colored-links';
+@import 'bootstrap/scss/helpers/focus-ring';
+@import 'bootstrap/scss/helpers/icon-link';
+@import 'bootstrap/scss/helpers/ratio';
+@import 'bootstrap/scss/helpers/position';
+@import 'bootstrap/scss/helpers/stacks';
+@import 'bootstrap/scss/helpers/visually-hidden';
+@import 'bootstrap/scss/helpers/stretched-link';
+@import 'bootstrap/scss/helpers/text-truncation';
+@import 'bootstrap/scss/helpers/vr';

+ 8 - 0
packages/core/scss/bootstrap/override/helpers/_color-bg.scss

@@ -0,0 +1,8 @@
+// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251
+@each $color, $value in $theme-colors {
+  .text-bg-#{$color} {
+    color: var(--#{$prefix}#{$color}) if($enable-important-utilities, !important, null);
+    background-color: var(--#{$prefix}#{$color}-bg-subtle) if($enable-important-utilities, !important, null);
+    border: 1px solid var(--#{$prefix}#{$color}-border-subtle) if($enable-important-utilities, !important, null);
+  }
+}

+ 9 - 9
packages/core/scss/bootstrap/theming/_variables.scss

@@ -5,15 +5,15 @@
 
 // Color system
 
-$gray-100: #f8f9fa !default;
-$gray-200: #e9ecef !default;
-$gray-300: #dee2e6 !default;
-$gray-400: #ced4da !default;
-$gray-500: #adb5bd !default;
-$gray-600: #6c757d !default;
-$gray-700: #495057 !default;
-$gray-800: #343a40 !default;
-$gray-900: #212529 !default;
+$gray-100: #faf9f8 !default;
+$gray-200: #efeeed !default;
+$gray-300: #e6e5e3 !default;
+$gray-400: #d8d7d5 !default;
+$gray-500: #b2b0ae !default;
+$gray-600: #767371 !default;
+$gray-700: #4d4a48 !default;
+$gray-800: #403c39 !default;
+$gray-900: #26231e !default;
 
 // The contrast ratio to reach against white, to determine if color changes from "light" to "dark". Acceptable values for WCAG 2.0 are 3, 4.5 and 7.
 // See https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast

+ 1 - 1
packages/core/scss/bootstrap/utilities.scss

@@ -1,4 +1,4 @@
 @import 'init';
 
-@import 'bootstrap/scss/helpers';
+@import './override/helpers';
 @import 'bootstrap/scss/utilities/api';

+ 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';

BIN
packages/preset-themes/public/images/halloween/halloween-navbar.jpg


BIN
packages/preset-themes/public/images/halloween/halloween.jpg


+ 3 - 0
packages/preset-themes/src/consts/preset-themes.ts

@@ -18,6 +18,7 @@ export const PresetThemes = {
   NATURE: 'nature',
   SPRING: 'spring',
   WOOD: 'wood',
+  CLASSIC: 'classic',
 } as const;
 export type PresetThemes = typeof PresetThemes[keyof typeof PresetThemes];
 
@@ -45,6 +46,8 @@ export const PresetThemesMetadatas: GrowiThemeMetadata[] = [
     name: PresetThemes.FIRE_RED,      schemeType: BOTH, bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', accent: '#EA5532',
   }, {
     name: PresetThemes.JADE_GREEN,    schemeType: BOTH, bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', accent: '#38B48B',
+  }, {
+    name: PresetThemes.CLASSIC,    schemeType: BOTH, bg: '#FDFDFD', topbar: '#E1E9F4', sidebar: '#E1E9F4', accent: '#439FD8',
   },
   // light only
   {

+ 67 - 0
packages/preset-themes/src/styles/classic.scss

@@ -0,0 +1,67 @@
+:root[data-bs-theme='light'] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
+
+  $primary: #3491CB;
+  $highlight: #B4CAE5;
+
+  @include generate-color-palette('primary', $primary, black, white);
+  @include generate-color-palette('highlight', $highlight,black, white);
+
+  $body-color:                #112744;
+  $body-bg:                   white;
+
+  $body-secondary-color:      rgba($body-color, .75);
+  $body-secondary-bg:         $gray-200;
+
+  $body-tertiary-color:       rgba($body-color, .5);
+  $body-tertiary-bg:          $gray-100;
+
+  $border-color:              var(--grw-highlight-200);
+
+  $link-color:                rgba(25, 56, 186, 1);
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
+
+  --grw-sidebar-nav-btn-color: var(--grw-highlight-600);
+}
+
+:root[data-bs-theme='dark'] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
+
+  $primary: #DB17C2;
+  $highlight: #68829D;
+
+  @include generate-color-palette('primary', $primary, black, white);
+  @include generate-color-palette('highlight', $highlight, black, white);
+
+  $body-color-dark:                   $gray-300;
+  $body-bg-dark:                      #1c1a1a;
+
+  $body-secondary-color-dark:         rgba($body-color-dark, .75);
+  $body-secondary-bg-dark:            $gray-800;
+
+  $body-tertiary-color-dark:          rgba($body-color-dark, .5);
+  $body-tertiary-bg-dark:             mix($gray-800, $gray-900, 50%);
+
+  $border-color-dark:                 var(--grw-highlight-200);
+
+  $link-color-dark:                   mix(#68829D, white, 80%);
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/apply-dark';
+
+  --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
+}

+ 176 - 130
packages/preset-themes/src/styles/halloween.scss

@@ -1,138 +1,184 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
+:root[data-bs-theme] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  $gray-100: #F2ECF0;
+  $gray-200: #E6DAE2;
+  $gray-300: #DAC7D3;
+  $gray-400: #CEB5C5;
+  $gray-500: #C1A2B6;
+  $gray-600: #9F8492;
+  $gray-700: #7C656D;
+  $gray-800: #594749;
+  $gray-900: #362824;
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
 
-@use './variables' as var;
-@use './theme/mixins/page-editor-mode-manager';
-@use './theme/hsl-functions' as hsl;
+  $primary: #AA4A04;
+  $highlight: #51199B;
 
-$bordercolor: #7e0d7e;
+  @include generate-color-palette('primary', $primary, #140700, white);
+  @include generate-color-palette('highlight', $highlight, #240E3E, white);
 
-// colors for overriding bootstrap $theme-colors
+  $body-color-dark:                   $gray-300;
+  $body-bg-dark:                      #1B0A0A;
 
+  $body-secondary-color-dark:         rgba($body-color-dark, .75);
+  $body-secondary-bg-dark:            $gray-800;
 
-.growi:not(.login-page) {
-  // add background-image
-  .page-editor-preview-container {
-    background-image: url('../images/halloween/halloween.jpg');
-  }
-}
+  $body-tertiary-color-dark:          rgba($body-color-dark, .5);
+  $body-tertiary-bg-dark:             mix($gray-800, $gray-900, 50%);
+
+  $border-color-dark:                 $gray-700;
+
+  $link-color-dark:                   $gray-400;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/apply-dark';
+
+  --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
 
 
-:root[data-bs-theme='dark'] {
-  --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
-  --primary-hs: 25,95%;
-  --primary-l: 34%;
-  --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
-  --secondary-hs: 208,7%;
-  --secondary-l: 46%;
-
-  // Background colors
-  --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
-  --bgcolor-global-hs: 0,100%;
-  --bgcolor-global-l: 1%;
-  --bgcolor-inline-code: #1f1f22; //optional
-  --bgcolor-card: var(--bgcolor-global);
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 40%)};
-  --bgcolor-keyword-highlighted: darkviolet;
-
-  // Font colors
-  --color-global: hsl(var(--color-global-hs),var(--color-global-l));
-  --color-global-hs: 42,81%;
-  --color-global-l: 54%;
-  --color-reversal: #eeeeee;
-  // --color-header: #2b2b2b;
-  --color-link: hsl(var(--color-link-hs),var(--color-link-l));
-  --color-link-hs: 262,33%;
-  --color-link-l: 69%;
-  --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
-  --color-link-wiki: var(--color-link);
-  --color-link-wiki-hs: var(--color-link-hs);
-  --color-link-wiki-l: var(--color-link-l);
-  --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 20%)};
-  --color-link-nabvar: #a7a7a7;
-  --color-inline-code: #c7254e; // optional
-
-  // List Group colors
-  --color-list: #979797;
-  --bgcolor-list: transparent;
-  --color-list-hover: var(--primary);
-  // --bgcolor-list-hover: ;
-  // --color-list-active: var(--color-reversal);
-  // --bgcolor-list-active: var(--primary);
-
-  // Search Top
-  --color-search: var(--primary);
-
-  // Navbar
-  --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
-  --bgcolor-navbar-hs: 180,3%;
-  --bgcolor-navbar-l: 93%;
-  --bgcolor-search-top-dropdown: var(--primary);
-  --bgcolor-search-top-dropdown-hs: var(--primary-hs);
-  --bgcolor-search-top-dropdown-l: var(--primary-l);
-  --border-image-navbar: linear-gradient(90deg, #e3b7ff 0%, #134774 100%);
-
-  // Logo colors
-  --bgcolor-logo: #{hsl.darken(var(--primary),10%)};
-  --fillcolor-logo-mark: #dedede;
-
-  // Sidebar
-  --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
-  --bgcolor-sidebar-hs: 197,40%;
-  --bgcolor-sidebar-l: 14%;
-  --bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
-  --text-shadow-sidebar-nav-item-active: 0px 0px 10px #969494; // optional
-
-  // Sidebar contents
-  --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
-  --color-sidebar-context-hs: 262,33%;
-  --color-sidebar-context-l: 69%;
-  --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
-  --bgcolor-sidebar-context-hs: 258,17%;
-  --bgcolor-sidebar-context-l: 20%;
-
-  // Sidebar list group
-  --bgcolor-sidebar-list-group: #2c2926; // optional
-
-  // Sidebar resize button
-  --color-resize-button: #effcfa;
-  --bgcolor-resize-button: var(--primary);
-  --bgcolor-resize-button-hs: var(--primary-hs);
-  --bgcolor-resize-button-l: var(--primary-l);
-  --color-resize-button-hover: #effcfa;
-  --bgcolor-resize-button-hover: #{hsl.lighten(var(--primary), 5%)};
-
-  // Subnavigation
-  --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
-  --bgcolor-subnav-hs: var(--bgcolor-global-hs);
-  --bgcolor-subnav-l: calc(var(--bgcolor-global-l) + 4%);
-
-  // Icon colors
-  --color-editor-icons: var(--color-global);
-
-  // Border colors
-  --border-color-theme: #{bs.$gray-300}; // former: `$navbar-border: $gray-300;`
-  --bordercolor-inline-code: #4d4d4d; // optional
-
-  // admin theme box
-  --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
-
-  &, body {
-    background-image: url('../images/halloween/halloween.jpg');
-  }
-
-  .grw-navbar {
-    background-image: url('../images/halloween/halloween-navbar.jpg') !important;
-  }
-
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.lighten(var(--primary), 35%)}, var(--primary), #{hsl.lighten(var(--primary), 5%)}, #{hsl.darken(var(--primary), 20%)});
-    }
-  }
-
-  // Table
-  .table {
-    color: var(--color-global);
-  }
 }
+
+// @use '@growi/core/scss/bootstrap/init' as bs;
+
+// @use './variables' as var;
+// @use './theme/mixins/page-editor-mode-manager';
+// @use './theme/hsl-functions' as hsl;
+
+// $bordercolor: #7e0d7e;
+
+// // colors for overriding bootstrap $theme-colors
+
+
+// .growi:not(.login-page) {
+//   // add background-image
+//   .page-editor-preview-container {
+//     background-image: url('../images/halloween/halloween.jpg');
+//   }
+// }
+
+
+// :root[data-bs-theme='dark'] {
+//   --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
+//   --primary-hs: 25,95%;
+//   --primary-l: 34%;
+//   --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
+//   --secondary-hs: 208,7%;
+//   --secondary-l: 46%;
+
+//   // Background colors
+//   --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
+//   --bgcolor-global-hs: 0,100%;
+//   --bgcolor-global-l: 1%;
+//   --bgcolor-inline-code: #1f1f22; //optional
+//   --bgcolor-card: var(--bgcolor-global);
+//   --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 40%)};
+//   --bgcolor-keyword-highlighted: darkviolet;
+
+//   // Font colors
+//   --color-global: hsl(var(--color-global-hs),var(--color-global-l));
+//   --color-global-hs: 42,81%;
+//   --color-global-l: 54%;
+//   --color-reversal: #eeeeee;
+//   // --color-header: #2b2b2b;
+//   --color-link: hsl(var(--color-link-hs),var(--color-link-l));
+//   --color-link-hs: 262,33%;
+//   --color-link-l: 69%;
+//   --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
+//   --color-link-wiki: var(--color-link);
+//   --color-link-wiki-hs: var(--color-link-hs);
+//   --color-link-wiki-l: var(--color-link-l);
+//   --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 20%)};
+//   --color-link-nabvar: #a7a7a7;
+//   --color-inline-code: #c7254e; // optional
+
+//   // List Group colors
+//   --color-list: #979797;
+//   --bgcolor-list: transparent;
+//   --color-list-hover: var(--primary);
+//   // --bgcolor-list-hover: ;
+//   // --color-list-active: var(--color-reversal);
+//   // --bgcolor-list-active: var(--primary);
+
+//   // Search Top
+//   --color-search: var(--primary);
+
+//   // Navbar
+//   --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
+//   --bgcolor-navbar-hs: 180,3%;
+//   --bgcolor-navbar-l: 93%;
+//   --bgcolor-search-top-dropdown: var(--primary);
+//   --bgcolor-search-top-dropdown-hs: var(--primary-hs);
+//   --bgcolor-search-top-dropdown-l: var(--primary-l);
+//   --border-image-navbar: linear-gradient(90deg, #e3b7ff 0%, #134774 100%);
+
+//   // Logo colors
+//   --bgcolor-logo: #{hsl.darken(var(--primary),10%)};
+//   --fillcolor-logo-mark: #dedede;
+
+//   // Sidebar
+//   --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
+//   --bgcolor-sidebar-hs: 197,40%;
+//   --bgcolor-sidebar-l: 14%;
+//   --bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
+//   --text-shadow-sidebar-nav-item-active: 0px 0px 10px #969494; // optional
+
+//   // Sidebar contents
+//   --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
+//   --color-sidebar-context-hs: 262,33%;
+//   --color-sidebar-context-l: 69%;
+//   --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
+//   --bgcolor-sidebar-context-hs: 258,17%;
+//   --bgcolor-sidebar-context-l: 20%;
+
+//   // Sidebar list group
+//   --bgcolor-sidebar-list-group: #2c2926; // optional
+
+//   // Sidebar resize button
+//   --color-resize-button: #effcfa;
+//   --bgcolor-resize-button: var(--primary);
+//   --bgcolor-resize-button-hs: var(--primary-hs);
+//   --bgcolor-resize-button-l: var(--primary-l);
+//   --color-resize-button-hover: #effcfa;
+//   --bgcolor-resize-button-hover: #{hsl.lighten(var(--primary), 5%)};
+
+//   // Subnavigation
+//   --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
+//   --bgcolor-subnav-hs: var(--bgcolor-global-hs);
+//   --bgcolor-subnav-l: calc(var(--bgcolor-global-l) + 4%);
+
+//   // Icon colors
+//   --color-editor-icons: var(--color-global);
+
+//   // Border colors
+//   --border-color-theme: #{bs.$gray-300}; // former: `$navbar-border: $gray-300;`
+//   --bordercolor-inline-code: #4d4d4d; // optional
+
+//   // admin theme box
+//   --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
+
+//   &, body {
+//     background-image: url('../images/halloween/halloween.jpg');
+//   }
+
+//   .grw-navbar {
+//     background-image: url('../images/halloween/halloween-navbar.jpg') !important;
+//   }
+
+//   // Button
+//   .btn-group.grw-page-editor-mode-manager {
+//     .btn.btn-outline-primary {
+//       @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.lighten(var(--primary), 35%)}, var(--primary), #{hsl.lighten(var(--primary), 5%)}, #{hsl.darken(var(--primary), 20%)});
+//     }
+//   }
+
+//   // Table
+//   .table {
+//     color: var(--color-global);
+//   }
+// }

+ 2 - 1
packages/preset-themes/vite.themes.config.ts

@@ -16,7 +16,7 @@ export default defineConfig(({ mode }) => {
           '/src/styles/default.scss',
           '/src/styles/fire-red.scss',
           '/src/styles/future.scss',
-          // '/src/styles/halloween.scss',
+          '/src/styles/halloween.scss',
           // '/src/styles/hufflepuff.scss',
           '/src/styles/kibela.scss',
           '/src/styles/island.scss',
@@ -25,6 +25,7 @@ export default defineConfig(({ mode }) => {
           '/src/styles/nature.scss',
           '/src/styles/spring.scss',
           '/src/styles/wood.scss',
+          '/src/styles/classic.scss',
         ],
         output: {
           assetFileNames: isProd

+ 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"