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

Merge branch 'dev/7.0.x' into imprv/133158-139809-update-tag-edit-modal-design

ryoji-s 2 лет назад
Родитель
Сommit
71eb087225
99 измененных файлов с 1590 добавлено и 501 удалено
  1. 1 1
      .github/workflows/ci-app-prod.yml
  2. 1 1
      .mergify.yml
  3. 29 1
      CHANGELOG.md
  4. 0 4
      apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss
  5. 2 2
      apps/app/_obsolete/src/components/PageEditor/Editor.tsx
  6. 9 4
      apps/app/public/static/locales/en_US/translation.json
  7. 9 4
      apps/app/public/static/locales/ja_JP/translation.json
  8. 9 4
      apps/app/public/static/locales/zh_CN/translation.json
  9. 3 1
      apps/app/src/client/services/create-page/use-create-template-page.ts
  10. 2 0
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  11. 3 0
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  12. 21 10
      apps/app/src/components/Admin/Customize/ThemeColorBox.tsx
  13. 1 1
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  14. 23 17
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  15. 17 4
      apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  16. 12 3
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  17. 5 0
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  18. 2 3
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  19. 2 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  20. 2 4
      apps/app/src/components/CustomNavigation/CustomNav.module.scss
  21. 1 1
      apps/app/src/components/CustomNavigation/CustomNav.tsx
  22. 4 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  23. 3 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  24. 3 1
      apps/app/src/components/PageComment/CommentEditor.tsx
  25. 1 1
      apps/app/src/components/PageControls/_button-styles.scss
  26. 7 2
      apps/app/src/components/PageCreateModal.tsx
  27. 1 1
      apps/app/src/components/PageEditor/Cheatsheet.tsx
  28. 0 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.module.scss
  29. 57 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  30. 3 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  31. 22 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  32. 1 0
      apps/app/src/components/PageEditor/EditorNavbar/index.ts
  33. 19 15
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  34. 11 12
      apps/app/src/components/PageEditor/PageEditor.tsx
  35. 0 53
      apps/app/src/components/PageHeader/EditingUserList.tsx
  36. 1 12
      apps/app/src/components/PageHeader/PageHeader.tsx
  37. 1 1
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  38. 1 0
      apps/app/src/components/PageHeader/index.ts
  39. 6 6
      apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss
  40. 1 1
      apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx
  41. 9 5
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  42. 2 2
      apps/app/src/components/PageTags/PageTags.tsx
  43. 9 5
      apps/app/src/components/PageTags/TagLabels.module.scss
  44. 3 2
      apps/app/src/components/ReactMarkdownComponents/NextLink.tsx
  45. 2 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  46. 8 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts
  47. 2 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx
  48. 33 0
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss
  49. 28 20
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  50. 3 2
      apps/app/src/components/SlackNotification.tsx
  51. 7 3
      apps/app/src/components/TableOfContents.tsx
  52. 4 1
      apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx
  53. 10 10
      apps/app/src/components/UsersHomepageFooter.module.scss
  54. 5 13
      apps/app/src/components/UsersHomepageFooter.tsx
  55. 18 3
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx
  56. 8 6
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  57. 5 2
      apps/app/src/interfaces/apiv3/page.ts
  58. 0 11
      apps/app/src/interfaces/editor-settings.ts
  59. 3 1
      apps/app/src/interfaces/page.ts
  60. 1 1
      apps/app/src/pages/installer.page.tsx
  61. 4 4
      apps/app/src/server/models/editor-settings.ts
  62. 9 2
      apps/app/src/server/models/obsolete-page.js
  63. 5 1
      apps/app/src/server/models/revision.js
  64. 4 2
      apps/app/src/server/routes/apiv3/page/create-page.ts
  65. 12 6
      apps/app/src/server/routes/apiv3/page/update-page.ts
  66. 8 6
      apps/app/src/server/routes/apiv3/user-group.js
  67. 5 5
      apps/app/src/server/service/page/index.ts
  68. 4 4
      apps/app/src/stores/editor.tsx
  69. 2 3
      apps/app/src/styles/_fonts.scss
  70. 4 0
      apps/app/src/styles/molecules/_list-group-item.scss
  71. 2 0
      bin/data-migrations/README.md
  72. 682 0
      bin/data-migrations/src/migrations/v70x/bootstrap5.js
  73. 3 0
      bin/data-migrations/src/migrations/v70x/index.js
  74. 7 4
      packages/core/src/interfaces/growi-theme-metadata.ts
  75. 10 0
      packages/core/src/interfaces/revision.ts
  76. 1 0
      packages/custom-icons/svg/drawer_io.svg
  77. 1 0
      packages/custom-icons/svg/external_link.svg
  78. 1 0
      packages/custom-icons/svg/format_quote.svg
  79. 1 0
      packages/custom-icons/svg/header.svg
  80. 1 8
      packages/custom-icons/svg/recently_created.svg
  81. 1 1
      packages/editor/package.json
  82. 13 49
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  83. 1 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx
  84. 2 2
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  85. 3 5
      packages/editor/src/components/CodeMirrorEditorComment.tsx
  86. 4 14
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  87. 12 3
      packages/editor/src/components/playground/Playground.tsx
  88. 8 0
      packages/editor/src/consts/editor-settings.ts
  89. 1 0
      packages/editor/src/consts/index.ts
  90. 4 62
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  91. 2 1
      packages/editor/src/services/editor-theme/index.ts
  92. 3 3
      packages/editor/src/services/keymaps/index.ts
  93. 2 0
      packages/editor/src/stores/index.ts
  94. 6 20
      packages/editor/src/stores/use-collaborative-editor-mode.ts
  95. 52 0
      packages/editor/src/stores/use-default-extensions.ts
  96. 91 0
      packages/editor/src/stores/use-editor-settings.ts
  97. 155 27
      packages/preset-themes/src/consts/preset-themes.ts
  98. 7 4
      packages/preset-themes/src/interfaces/growi-theme-metadata.ts
  99. 1 1
      yarn.lock

+ 1 - 1
.github/workflows/ci-app-prod.yml

@@ -48,7 +48,7 @@ concurrency:
 
 jobs:
 
-  test-prod-node16:
+  test-prod-node18:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
       node-version: 18.x

+ 1 - 1
.mergify.yml

@@ -6,8 +6,8 @@ pull_request_rules:
       - check-success = "lint (20.x)"
       - check-success = "test (20.x)"
       - check-success = "launch-dev (20.x)"
-      - check-success = "test-prod-node16 / launch-prod"
       - check-success = "test-prod-node18 / launch-prod"
+      - check-success = "test-prod-node20 / launch-prod"
     actions:
       merge:
         method: merge

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,36 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.3.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.3.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.3.1](https://github.com/weseek/growi/compare/v6.3.0...v6.3.1) - 2024-02-01
+
+### 💎 Features
+
+* feat: Normalize duplicated root pages to valid paths when server startup (#8414) @miya
+
+### 🚀 Improvement
+
+* imprv: Use unzip stream instead of unzipper (#8378) @ryu-sato
+* imprv: Allow plugin that contain slashes in the branch name to be installed (#8359) @ryu-sato
+
+### 🐛 Bug Fixes
+
+* fix: Page being able to delete completely when not allowed (#8374) @arafubeatbox
+* fix: Logs are not saved when viewing the page (#8406) @miya
+* fix: Preventing duplication of `/user/username` pages (#8413) @WNomunomu
+* fix: Non-admin user cannot rename pages v63x (#8410) @jam411
+* fix: Duplicate root pages are created unintentionally (#8404) @miya
+* fix: Configured auditlog environment variables are not reflected in the administration screen (#8383) @miya
+* fix: plugin is broken after unzipping (#8358) @ryu-sato
+* fix: Keycloak group sync config not loaded on sync execution (#8339) @arafubeatbox
+
+### 🧰 Maintenance
+
+* support: React Testing Library (#8393) @miya
+* ci(deps-dev): bump vite from 4.5.1 to 4.5.2 (#8392) @dependabot
+
 ## [v6.3.0](https://github.com/weseek/growi/compare/v6.2.5...v6.3.0) - 2023-12-14
 
 ### BREAKING CHANGES
@@ -50,6 +77,7 @@
 * imprv: Allow deletion of user homepage when the user is deleted (#8224) @jam411
 
 ### 🐛 Bug Fixes
+
 * fix: Certify shared page attachment middleware (6.2.x) (#8256) @yuki-takei
 
 ### 🧰 Maintenance

+ 0 - 4
apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss

@@ -62,10 +62,6 @@
     background: rgba(0, 0, 0, 0.2);
   }
 
-  .grw-email-sm {
-    font-size: 0.75em;
-  }
-
   .grw-notification-dropdown {
     .dropdown-menu {
       max-width: 70vw;

+ 2 - 2
apps/app/_obsolete/src/components/PageEditor/Editor.tsx

@@ -5,6 +5,7 @@ import React, {
   useEffect,
 } from 'react';
 
+import type { EditorSettings } from '@growi/editor';
 import Dropzone from 'react-dropzone';
 import { useTranslation } from 'react-i18next';
 import {
@@ -12,7 +13,6 @@ import {
 } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { IEditorSettings } from '~/interfaces/editor-settings';
 import { useDefaultIndentSize } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
 import { useIsMobile } from '~/stores/ui';
@@ -36,7 +36,7 @@ export type EditorPropsType = {
   isUploadAllFileAllowed?: boolean,
   onChange?: (newValue: string, isClean?: boolean) => void,
   onUpload?: (file) => void,
-  editorSettings?: IEditorSettings,
+  editorSettings?: EditorSettings,
   indentSize?: number,
   onDragEnter?: (event: any) => void,
   onMarkdownHelpButtonClicked?: () => void,

+ 9 - 4
apps/app/public/static/locales/en_US/translation.json

@@ -112,7 +112,7 @@
   "Create under": "Create page under below:",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <i class='icon-share-alt'></i> ",
+  "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <span className='growi-custom-icons'>external_link</span> ",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
@@ -311,6 +311,11 @@
     }
   },
   "page_edit": {
+    "input_channels": "Input channels",
+    "theme": "Theme",
+    "keymap": "Keymap",
+    "indent": "Indent",
+    "editor_config": "Editor Config",
     "Show active line": "Show active line",
     "auto_format_table": "Auto format table",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
@@ -554,7 +559,7 @@
     "alert_desc1": "On this page, you can select pages with the checkbox and batch convert to the new v5 compatible format from the \"Bulk operation\" button at the top of the screen.",
     "nopages_title": "Congratulations. Ready to use GROWI v5!",
     "nopages_desc1": "Now all the pages you can manage seem to be in v5 compatible format.",
-    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <i class='icon-share-alt'></i></a>.",
+    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <span className='growi-custom-icons'>external_link</span></a>.",
     "modal": {
       "title": "Convert to new v5 compatible format",
       "converting_pages": "Converting pages",
@@ -828,10 +833,10 @@
     "select_page_location": "Select page location"
   },
   "wip_page": {
-    "save_as_wip": "Save as WIP (Currently drafting)",
+    "save_as_wip": "Save as WIP (still being written)",
     "success_save_as_wip": "Successfully saved as a WIP page",
     "fail_save_as_wip": "Failed to save as a WIP page",
-    "alert": "This page is a work in progress",
+    "alert": "This page is still being written",
     "publish_page": "Publish page",
     "success_publish_page": "Page has been published",
     "fail_publish_page": "Failed to publish the Page"

+ 9 - 4
apps/app/public/static/locales/ja_JP/translation.json

@@ -111,7 +111,7 @@
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
-  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><i class='icon-share-alt'></i>を参照ください。",
+  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><span className='growi-custom-icons'>external_link</span>を参照ください。",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "Basic Settings": "基本設定",
@@ -344,6 +344,11 @@
     }
   },
   "page_edit": {
+    "input_channels": "チャンネル名",
+    "theme": "テーマ",
+    "keymap": "キーマップ",
+    "indent": "インデント",
+    "editor_config": "エディタ設定",
     "Show active line": "アクティブ行をハイライト",
     "auto_format_table": "表の自動整形",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
@@ -587,7 +592,7 @@
     "alert_desc1": "このページでは、チェックボックスでページを選択し、画面上部の「一括操作」ボタンから新しい v5 互換形式に一括変換できます。",
     "nopages_title": "おめでとうございます。GROWI v5 を使う準備が完了しました!",
     "nopages_desc1": "今あなたが管理可能なページはすべて v5 互換形式になっているようです。",
-    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード  <i class='icon-share-alt'></i></a> を参照ください。",
+    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード <span className='growi-custom-icons'>external_link</span></a> を参照ください。",
     "modal": {
       "title": "新しい v5 互換形式への変換",
       "converting_pages": "以下のページを変換します",
@@ -861,10 +866,10 @@
     "select_page_location": "ページの場所を選択"
   },
   "wip_page": {
-    "save_as_wip": "WIP (執筆中) として保存",
+    "save_as_wip": "WIP (執筆中) として保存",
     "success_save_as_wip": "WIP ページとして保存しました",
     "fail_save_as_wip": "WIP ページとして保存できませんでした",
-    "alert": "このページは作業途中です",
+    "alert": "このページは執筆途中です",
     "publish_page": "WIP を解除",
     "success_publish_page": "WIP を解除しました",
     "fail_publish_page": "WIP を解除できませんでした"

+ 9 - 4
apps/app/public/static/locales/zh_CN/translation.json

@@ -117,7 +117,7 @@
 	"Create under": "Create page under below:",
   "V5 Page Migration": "转换为V5的兼容性",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <i class='icon-share-alt'></i> ",
+  "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <span className='growi-custom-icons'>external_link</span> ",
 	"Markdown Settings": "Markdown设置",
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
@@ -301,6 +301,11 @@
 		}
 	},
 	"page_edit": {
+    "input_channels": "频道名",
+    "theme": "主题",
+    "keymap": "键表",
+    "indent": "缩进",
+    "editor_config": "编辑器配置",
 		"Show active line": "显示活动行",
 		"auto_format_table": "自动格式化表格",
 		"overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
@@ -557,7 +562,7 @@
     "alert_desc1": "在这一页,你可以用复选框选择页面,并通过屏幕上方的批量操作按钮批量转换为新的v5兼容格式。",
     "nopages_title": "恭喜你。准备使用GROWI v5!",
     "nopages_desc1": "现在你能管理的所有页面似乎都是v5兼容的格式。",
-    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <i class='icon-share-alt'></i></a>.的详细内容。",
+    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <span className='growi-custom-icons'>external_link</span></a>.的详细内容。",
     "modal": {
       "title": "转换为新的v5兼容格式",
       "converting_pages": "转换页面",
@@ -831,10 +836,10 @@
     "select_page_location": "选择页面位置"
   },
   "wip_page": {
-    "save_as_wip": "保存为 WIP(书面)",
+    "save_as_wip": "保存为 WIP(仍在撰写中)",
     "success_save_as_wip": "成功保存为 WIP 页面",
     "fail_save_as_wip": "保存为 WIP 页失败",
-    "alert": "本页面正在制作中",
+    "alert": "本页仍在编写中",
     "publish_page": "发布 WIP",
     "success_publish_page": "WIP 已停用",
     "fail_publish_page": "无法停用 WIP"

+ 3 - 1
apps/app/src/client/services/create-page/use-create-template-page.ts

@@ -1,11 +1,13 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
 
 import type { LabelType } from '~/interfaces/template';
 import { useCurrentPagePath } from '~/stores/page';
 
+
 import { useCreatePageAndTransit } from './use-create-page-and-transit';
 
 type UseCreateTemplatePage = () => {
@@ -25,7 +27,7 @@ export const useCreateTemplatePage: UseCreateTemplatePage = () => {
     if (isLoadingPagePath || !isCreatable) return;
 
     return createAndTransit(
-      { path: normalizePath(`${currentPagePath}/${label}`), wip: false },
+      { path: normalizePath(`${currentPagePath}/${label}`), wip: false, origin: Origin.View },
       { shouldCheckPageExists: true },
     );
   }, [currentPagePath, isCreatable, isLoadingPagePath, createAndTransit]);

+ 2 - 0
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
+import { Origin } from '@growi/core';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
@@ -47,6 +48,7 @@ export const useDrawioModalLauncherForView = (opts?: {
         pageId: currentPage._id,
         revisionId: currentRevisionId,
         body: newMarkdown,
+        origin: Origin.View,
       });
 
       opts?.onSaveSuccess?.();

+ 3 - 0
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
+import { Origin } from '@growi/core';
+
 import type MarkdownTable from '~/client/models/MarkdownTable';
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
 import { useShareLinkId } from '~/stores/context';
@@ -46,6 +48,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
         pageId: currentPage._id,
         revisionId: currentRevisionId,
         body: newMarkdown,
+        origin: Origin.View,
       });
 
       opts?.onSaveSuccess?.();

+ 21 - 10
apps/app/src/components/Admin/Customize/ThemeColorBox.tsx

@@ -15,26 +15,37 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
     isSelected, metadata, onSelected,
   } = props;
   const {
-    name, bg, topbar, sidebar, accent, isPresetTheme,
+    name, lightBg, darkBg, lightSidebar, darkSidebar, lightIcon, darkIcon, createBtn, isPresetTheme,
   } = metadata;
 
   return (
+    // TODO: Display a primary color border when icon is selected
     <div
       id={`theme-option-${name}`}
       className={`theme-option-container d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       onClick={onSelected}
     >
-      <a id={name} role="button" className={`m-0 ${name} theme-button`}>
-        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
-          <g>
-            <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
-            <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
-            <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
-            <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={accent}></path>
-          </g>
+      <a id={name} role="button" className={`m-0 rounded ${name} theme-button`}>
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
+          <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
+          <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
+          <path
+            d="M4.077,20.648,10.164,10.1H22.338l6.087,10.544L22.338,31.19H10.164ZM0,0V52.8l6.436-3.255v-1.8H10L17.189,44.1H6.436V42.044H21.267L32.5,36.364V0Z"
+            fill={lightSidebar}
+          />
+          <path
+            d="M6.436,53.44H26.065V55.5H6.436Zm14.831-11.4h4.8v2.061H17.189L10,47.743H26.065V49.8l-19.629,0v-.259L0,52.8V64H32.5V36.364Z"
+            fill={darkSidebar}
+          />
+          <path d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z" fill={createBtn} />
+          <path d="M6.436,49.543,10,47.742H6.436Z" fill={lightIcon} />
+          <path d="M6.436,44.106H17.189l4.078-2.062H6.436Z" fill={lightIcon} />
+          <path d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z" fill={darkIcon} />
+          <path d="M26.065,44.106V42.044h-4.8L17.19,44.106Z" fill={darkIcon} />
+          <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
         </svg>
       </a>
-      <span className="theme-option-name"><b>{ name }</b></span>
+      <span className="theme-option-name mt-2"><b>{ name }</b></span>
       { !isPresetTheme && <span className="theme-option-badge badge bg-primary mt-1">Plugin</span> }
     </div>
   );

+ 1 - 1
apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -471,7 +471,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           target="_blank"
                           rel="noreferer noreferrer"
                         >
-                          Apache Lucene - Query Parser Syntax <i className="icon-share-alt"></i>
+                          Apache Lucene - Query Parser Syntax <span className="growi-custom-icons">external_link</span>
                         </a>.
                       </p>
                       <div className="accordion" id="accordionId">

+ 23 - 17
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -1,7 +1,9 @@
 import type { FC } from 'react';
 import React, { useCallback, useState, useMemo } from 'react';
 
-import type { IUserGroupHasId } from '@growi/core';
+import {
+  getIdForRef, isPopulated, type IGrantedGroup, type IUserGroupHasId,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -18,9 +20,9 @@ import { PageActionOnGroupDelete } from '~/interfaces/user-group';
  * @extends {React.Component}
  */
 type Props = {
-  userGroups: IUserGroupHasId[],
+  userGroups: IGrantedGroup[],
   deleteUserGroup?: IUserGroupHasId,
-  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => Promise<void> | void,
+  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => Promise<void> | void,
   isShow: boolean,
   onHide?: () => Promise<void> | void,
 };
@@ -71,14 +73,14 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
    * State
    */
   const [actionName, setActionName] = useState<PageActionOnGroupDelete | null>(null);
-  const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
+  const [transferToUserGroup, setTransferToUserGroup] = useState<IGrantedGroup | null>(null);
 
   /*
    * Function
    */
   const resetStates = useCallback(() => {
     setActionName(null);
-    setTransferToUserGroupId('');
+    setTransferToUserGroup(null);
   }, []);
 
   const toggleHandler = useCallback(() => {
@@ -97,8 +99,9 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
   const handleGroupChange = useCallback((e) => {
     const transferToUserGroupId = e.target.value;
-    setTransferToUserGroupId(transferToUserGroupId);
-  }, []);
+    const selectedGroup = userGroups.find(group => getIdForRef(group.item) === transferToUserGroupId) ?? null;
+    setTransferToUserGroup(selectedGroup);
+  }, [userGroups]);
 
   const handleSubmit = useCallback((e) => {
     if (onDelete == null || deleteUserGroup == null || actionName == null) {
@@ -110,9 +113,9 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     onDelete(
       deleteUserGroup._id,
       actionName,
-      transferToUserGroupId,
+      transferToUserGroup,
     );
-  }, [onDelete, deleteUserGroup, actionName, transferToUserGroupId]);
+  }, [onDelete, deleteUserGroup, actionName, transferToUserGroup]);
 
   const renderPageActionSelector = useCallback(() => {
     const options = availableOptions.map((opt) => {
@@ -139,28 +142,31 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     }
 
     const groups = userGroups.filter((group) => {
-      return group._id !== deleteUserGroup._id;
+      return getIdForRef(group.item) !== deleteUserGroup._id;
     });
 
     const options = groups.map((group) => {
-      return <option key={group._id} value={group._id}>{group.name}</option>;
-    });
+      const groupId = getIdForRef(group.item);
+      const groupName = isPopulated(group.item) ? group.item.name : null;
+      return { id: groupId, name: groupName };
+    }).filter(obj => obj.name != null)
+      .map(obj => <option key={obj.id} value={obj.id}>{obj.name}</option>);
 
     const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
       : t('admin:user_group_management.delete_modal.select_group');
 
     return (
       <select
-        name="transferToUserGroupId"
+        name="transferToUserGroup"
         className={`form-control ${actionName === PageActionOnGroupDelete.transfer ? '' : 'd-none'}`}
-        value={transferToUserGroupId}
+        value={transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : ''}
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
         {options}
       </select>
     );
-  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);
+  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroup, handleGroupChange]);
 
   const validateForm = useCallback(() => {
     let isValid = true;
@@ -169,11 +175,11 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       isValid = false;
     }
     else if (actionName === PageActionOnGroupDelete.transfer) {
-      isValid = transferToUserGroupId !== '';
+      isValid = transferToUserGroup != null;
     }
 
     return isValid;
-  }, [actionName, transferToUserGroupId]);
+  }, [actionName, transferToUserGroup]);
 
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>

+ 17 - 4
apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,16 +1,19 @@
 import type { FC } from 'react';
 import React, { useState, useCallback } from 'react';
 
-import type { IUserGroup, IUserGroupHasId } from '@growi/core';
+import {
+  GroupType, getIdForRef, type IGrantedGroup, type IUserGroup, type IUserGroupHasId,
+} from '@growi/core';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { ExternalGroupManagement } from '~/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement';
+import { useSWRxExternalUserGroupList } from '~/features/external-user-group/client/stores/external-user-group';
+import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
-import { PageActionOnGroupDelete } from '~/interfaces/user-group';
 
 
 const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
@@ -26,7 +29,14 @@ export const UserGroupPage: FC = () => {
    * Fetch
    */
   const { data: userGroupList, mutate: mutateUserGroups } = useSWRxUserGroupList();
+  const { data: externalUserGroupList } = useSWRxExternalUserGroupList();
   const userGroups = userGroupList != null ? userGroupList : [];
+  const userGroupsForDeleteModal: IGrantedGroup[] = userGroups.map((group) => {
+    return { item: group, type: GroupType.userGroup };
+  });
+  const externalUserGroupsForDeleteModal: IGrantedGroup[] = externalUserGroupList != null ? externalUserGroupList.map((group) => {
+    return { item: group, type: GroupType.externalUserGroup };
+  }) : [];
   const userGroupIds = userGroups.map(group => group._id);
 
   const { data: userGroupRelationList } = useSWRxUserGroupRelationList(userGroupIds);
@@ -128,11 +138,14 @@ export const UserGroupPage: FC = () => {
     }
   }, [t, mutateUserGroups, hideUpdateModal]);
 
-  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => {
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       await apiv3Delete(`/user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -189,7 +202,7 @@ export const UserGroupPage: FC = () => {
       />
 
       <UserGroupDeleteModal
-        userGroups={userGroups}
+        userGroups={userGroupsForDeleteModal.concat(externalUserGroupsForDeleteModal)}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteUserGroupById}
         isShow={isDeleteModalShown}

+ 12 - 3
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -2,7 +2,9 @@ import React, {
   useState, useCallback, useEffect, useMemo,
 } from 'react';
 
-import type { IUserGroup, IUserGroupHasId } from '@growi/core';
+import {
+  GroupType, getIdForRef, type IGrantedGroup, type IUserGroup, type IUserGroupHasId,
+} from '@growi/core';
 import { objectIdUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -85,6 +87,10 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
 
   const { data: childUserGroupsList, mutate: mutateChildUserGroups, updateChild } = useChildUserGroupList(currentUserGroupId, isExternalGroup);
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
+  const childUserGroupsForDeleteModal: IGrantedGroup[] = childUserGroups.map((group) => {
+    const groupType = isExternalGroup ? GroupType.externalUserGroup : GroupType.userGroup;
+    return { item: group, type: groupType };
+  });
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   const childUserGroupIds = childUserGroups.map(group => group._id);
 
@@ -297,12 +303,15 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     setDeleteModalShown(false);
   }, [setSelectedUserGroup, setDeleteModalShown]);
 
-  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => {
     const url = isExternalGroup ? `/external-user-groups/${deleteGroupId}` : `/user-groups/${deleteGroupId}`;
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       const res = await apiv3Delete(url, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -449,7 +458,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       />
 
       <UserGroupDeleteModal
-        userGroups={childUserGroups}
+        userGroups={childUserGroupsForDeleteModal}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteChildUserGroupById}
         isShow={isDeleteModalShown}

+ 5 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss

@@ -2,3 +2,8 @@
   margin-right: 0.2em;
   margin-left: 0.2em;
 }
+
+.material-symbols-outlined {
+  font-size: 1em;
+  line-height: inherit;
+}

+ 2 - 3
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -41,7 +41,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
-              <span className="material-symbols-outlined">delete</span>
+              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>delete</span>
             </Link>
           </span>
           <span className={`separator ${styles.separator}`}><a href="/">/</a></span>
@@ -51,8 +51,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
           <span className="path-segment">
             <Link href="/" prefetch={false}>
-              {/* TODO: Size adjust */}
-              <span className="material-symbols-outlined">home</span>
+              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>home</span>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
           </span>

+ 2 - 2
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -72,10 +72,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     formerLink = (
-      <>
+      <div className="fs-5">
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <Separator />
-      </>
+      </div>
     );
     latterLink = (
       <>

+ 2 - 4
apps/app/src/components/CustomNavigation/CustomNav.module.scss

@@ -1,3 +1,5 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
 .grw-custom-nav-tab :global {
   .nav-title {
     flex-wrap: nowrap;
@@ -13,8 +15,4 @@
     transition: 0.3s ease-in-out;
   }
 
-  .material-symbols-outlined {
-    margin-right: 6px;
-    font-size: 18px;
-  }
 }

+ 1 - 1
apps/app/src/components/CustomNavigation/CustomNav.tsx

@@ -183,7 +183,7 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
                 className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
               >
                 <NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
-                  { Icon != null && <Icon /> } {i18n}
+                  { Icon != null && <span className="me-1"><Icon /></span> } {i18n}
                 </NavLink>
               </NavItem>
             );

+ 4 - 0
apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss

@@ -9,5 +9,9 @@
   .grw-contextual-sub-navigation {
     position: fixed;
     right: 0;
+
+    // unset colors
+    background-color: unset;
+    backdrop-filter: unset;
   }
 }

+ 3 - 1
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -1,7 +1,9 @@
 import React, { type ReactNode, useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
+
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
 import { useIsNotFound } from '~/stores/page';
@@ -74,7 +76,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
 
     try {
       await createAndTransit(
-        { path, wip: shouldCreateWipPage(path) },
+        { path, wip: shouldCreateWipPage(path), origin: Origin.View },
         { shouldCheckPageExists: true },
       );
     }

+ 3 - 1
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -20,7 +20,7 @@ import {
   useCurrentUser, useIsSlackConfigured, useAcceptedUploadFileType,
 } from '~/stores/context';
 import {
-  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning,
+  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning, useEditorSettings,
 } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
@@ -79,6 +79,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackConfigured } = useIsSlackConfigured();
+  const { data: editorSettings } = useEditorSettings();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const {
     increment: incrementEditingCommentsNum,
@@ -336,6 +337,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onChange={onChangeHandler}
                 onSave={postCommentHandler}
                 onUpload={uploadHandler}
+                editorSettings={editorSettings}
               />
               {/* <Editor
                 ref={editorRef}

+ 1 - 1
apps/app/src/components/PageControls/_button-styles.scss

@@ -2,7 +2,7 @@
 
 %btn-basis {
   --bs-btn-padding-x: 6px;
-  --bs-btn-padding-y: 8px;
+  --bs-btn-padding-y: 6px;
   --bs-btn-line-height: 1em;
   --bs-btn-border-width: 0;
   --bs-btn-box-shadow: none;

+ 7 - 2
apps/app/src/components/PageCreateModal.tsx

@@ -2,6 +2,7 @@ import React, {
   useEffect, useState, useMemo, useCallback,
 } from 'react';
 
+import { Origin } from '@growi/core';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
@@ -94,7 +95,7 @@ const PageCreateModal: React.FC = () => {
   const createTodayPage = useCallback(async() => {
     const joinedPath = [todaysParentPath, todayInput].join('/');
     return createAndTransit(
-      { path: joinedPath, wip: true },
+      { path: joinedPath, wip: true, origin: Origin.View },
       { shouldCheckPageExists: true, onTerminated: closeCreateModal },
     );
   }, [closeCreateModal, createAndTransit, todayInput, todaysParentPath]);
@@ -104,7 +105,11 @@ const PageCreateModal: React.FC = () => {
    */
   const createInputPage = useCallback(async() => {
     return createAndTransit(
-      { path: pageNameInput, optionalParentPath: '/', wip: true },
+      {
+        path: pageNameInput,
+        wip: true,
+        origin: Origin.View,
+      },
       { shouldCheckPageExists: true, onTerminated: closeCreateModal },
     );
   }, [closeCreateModal, createAndTransit, pageNameInput]);

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

@@ -105,7 +105,7 @@ export const Cheatsheet = (): JSX.Element => {
 
         <hr />
         <a href="/Sandbox" className="btn btn-info" target="_blank">
-          <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
+          <span className="growi-custom-icons">external_link</span> {t('sandbox.open_sandbox')}
         </a>
       </div>
     </div>

+ 0 - 0
apps/app/src/components/PageHeader/user-list-popover.module.scss → apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.module.scss


+ 57 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -0,0 +1,57 @@
+import { type FC, useState } from 'react';
+
+import type { IUserHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
+import { Popover, PopoverBody } from 'reactstrap';
+
+import UserPictureList from '../../Common/UserPictureList';
+
+import styles from './EditingUserList.module.scss';
+
+const userListPopoverClass = styles['user-list-popover'] ?? '';
+
+type Props = {
+  userList: IUserHasId[]
+}
+
+export const EditingUserList: FC<Props> = ({ userList }) => {
+  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+  const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
+
+  const firstFourUsers = userList.slice(0, 4);
+  const remainingUsers = userList.slice(4);
+
+  if (userList.length === 0) {
+    return <></>;
+  }
+
+  return (
+    <div className="d-flex flex-column justify-content-end">
+      <div className="d-flex justify-content-end">
+        {firstFourUsers.map(user => (
+          <div className="ms-1">
+            <UserPicture
+              user={user}
+              noLink
+              additionalClassName="border border-info"
+            />
+          </div>
+        ))}
+
+        {remainingUsers.length > 0 && (
+          <div className="ms-1">
+            <button type="button" id="btn-editing-user" className="btn border-0 bg-info-subtle rounded-pill p-0">
+              <span className="fw-bold text-info p-1">+{remainingUsers.length}</span>
+            </button>
+            <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-editing-user" toggle={togglePopover} trigger="legacy">
+              <PopoverBody className={userListPopoverClass}>
+                <UserPictureList users={remainingUsers} />
+              </PopoverBody>
+            </Popover>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};

+ 3 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss

@@ -0,0 +1,3 @@
+.editor-navbar :global {
+  min-height: 72px;
+}

+ 22 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -0,0 +1,22 @@
+
+import { PageHeader } from '~/components/PageHeader';
+import { useEditingUsers } from '~/stores/use-editing-users';
+
+import { EditingUserList } from './EditingUserList';
+
+import styles from './EditorNavbar.module.scss';
+
+const moduleClass = styles['editor-navbar'] ?? '';
+
+export const EditorNavbar = (): JSX.Element => {
+  const { data: editingUsers } = useEditingUsers();
+
+  return (
+    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
+      <PageHeader />
+      <EditingUserList
+        userList={editingUsers?.userList ?? []}
+      />
+    </div>
+  );
+};

+ 1 - 0
apps/app/src/components/PageEditor/EditorNavbar/index.ts

@@ -0,0 +1 @@
+export * from './EditorNavbar';

+ 19 - 15
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -2,8 +2,8 @@ import React, {
   memo, useCallback, useMemo, useState,
 } from 'react';
 
-import type {
-  EditorTheme, KeyMapMode,
+import {
+  type EditorTheme, type KeyMapMode, DEFAULT_KEYMAP, DEFAULT_THEME,
 } from '@growi/editor';
 import { useTranslation } from 'next-i18next';
 import Image from 'next/image';
@@ -14,11 +14,6 @@ import {
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
 
-import {
-  DEFAULT_THEME, DEFAULT_KEYMAP,
-} from '../../interfaces/editor-settings';
-
-
 type RadioListItemProps = {
   onClick: () => void,
   icon?: React.ReactNode,
@@ -91,6 +86,7 @@ const EDITORTHEME_LABEL_MAP: EditorThemeToLabel = {
 
 const ThemeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: editorSettings, update } = useEditorSettings();
   const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME;
 
@@ -106,7 +102,7 @@ const ThemeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX
   ), [update, selectedTheme]);
 
   return (
-    <Selector header="Theme" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.theme')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 ThemeSelector.displayName = 'ThemeSelector';
@@ -125,6 +121,7 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = {
 
 const KeymapSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: editorSettings, update } = useEditorSettings();
   const selectedKeymapMode = editorSettings?.keymapMode ?? DEFAULT_KEYMAP;
 
@@ -144,7 +141,7 @@ const KeymapSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JS
 
 
   return (
-    <Selector header="Keymap" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.keymap')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 KeymapSelector.displayName = 'KeymapSelector';
@@ -154,6 +151,7 @@ const TYPICAL_INDENT_SIZE = [2, 4];
 
 const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
   const listItems = useMemo(() => (
@@ -167,7 +165,7 @@ const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void})
   ), [currentIndentSize, mutateCurrentIndentSize]);
 
   return (
-    <Selector header="Indent" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.indent')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 IndentSizeSelector.displayName = 'IndentSizeSelector';
@@ -260,6 +258,8 @@ type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
 
 export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Element => {
 
+  const { t } = useTranslation();
+
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
   const [status, setStatus] = useState<OptionStatus>(OptionsStatus.Home);
@@ -282,7 +282,7 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
         <span className="material-symbols-outlined py-0 fs-5"> settings </span>
         {
           collapsed ? <></>
-            : <label className="ms-1 me-1">Editor Config</label>
+            : <label className="ms-1 me-1">{t('page_edit.editor_config')}</label>
         }
       </DropdownToggle>
       <DropdownMenu container="body">
@@ -290,21 +290,25 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
           status === OptionsStatus.Home && (
             <div className="d-flex flex-column">
               <label className="text-muted ms-3">
-                Editor Config
+                {t('page_edit.editor_config')}
               </label>
               <hr className="my-1" />
-              <ChangeStateButton onClick={() => setStatus(OptionsStatus.Theme)} header="Theme" data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''} />
+              <ChangeStateButton
+                onClick={() => setStatus(OptionsStatus.Theme)}
+                header={t('page_edit.theme')}
+                data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''}
+              />
               <hr className="my-1" />
               <ChangeStateButton
                 onClick={() => setStatus(OptionsStatus.Keymap)}
-                header="Keymap"
+                header={t('page_edit.keymap')}
                 data={KEYMAP_LABEL_MAP[editorSettings.keymapMode ?? ''] ?? ''}
               />
               <hr className="my-1" />
               <ChangeStateButton
                 disabled={isIndentSizeForced}
                 onClick={() => setStatus(OptionsStatus.Indent)}
-                header="Indent"
+                header={t('page_edit.indent')}
                 data={currentIndentSize.toString() ?? ''}
               />
               <hr className="my-1" />

+ 11 - 12
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -6,7 +6,7 @@ import React, {
 import type EventEmitter from 'events';
 import nodePath from 'path';
 
-import type { IPageHasId, IUserHasId } from '@growi/core';
+import { type IPageHasId, Origin } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
@@ -57,16 +57,15 @@ import { useEditingUsers } from '~/stores/use-editing-users';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
-import { PageHeader } from '../PageHeader/PageHeader';
-
-// import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
-// import { ConflictDiffModal } from './ConflictDiffModal';
+import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
 
 import '@growi/editor/dist/style.css';
 
+// import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
+// import { ConflictDiffModal } from './ConflictDiffModal';
 
 const logger = loggerFactory('growi:PageEditor');
 
@@ -205,9 +204,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
       const { page } = await updatePage({
         pageId,
-        revisionId: currentRevisionId,
         body: codeMirrorEditor?.getDoc() ?? '',
         grant: grantData?.grant,
+        origin: Origin.Editor,
         userRelatedGrantUserGroupIds: grantData?.userRelatedGrantedGroups?.map((group) => {
           return { item: group.id, type: group.type };
         }),
@@ -433,9 +432,9 @@ 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="px-4 py-2">
-        <PageHeader />
-      </div>
+
+      <EditorNavbar />
+
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert">
           <CodeMirrorEditorMain
@@ -448,10 +447,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             user={user ?? undefined}
             pageId={pageId ?? undefined}
             initialValue={initialValue}
-            onOpenEditor={markdown => setMarkdownToPreview(markdown)}
+            editorSettings={editorSettings}
             onEditorsUpdated={onEditorsUpdated}
-            editorTheme={editorSettings?.theme}
-            editorKeymap={editorSettings?.keymapMode}
           />
         </div>
         <div
@@ -477,7 +474,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         />
         */}
       </div>
+
       <EditorNavbarBottom />
+
     </div>
   );
 });

+ 0 - 53
apps/app/src/components/PageHeader/EditingUserList.tsx

@@ -1,53 +0,0 @@
-import { type FC, useState } from 'react';
-
-import type { IUserHasId } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
-import { Popover, PopoverBody } from 'reactstrap';
-
-import UserPictureList from '../Common/UserPictureList';
-
-import popoverStyles from './user-list-popover.module.scss';
-
-type Props = {
-  className: string,
-  userList: IUserHasId[]
-}
-
-export const EditingUserList: FC<Props> = ({ className, userList }) => {
-  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-
-  const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
-
-  const firstFourUsers = userList.slice(0, 4);
-  const remainingUsers = userList.slice(4);
-
-  return (
-    <div className={className}>
-      {userList.length > 0 && (
-        <div className="d-flex justify-content-end">
-          {firstFourUsers.map(user => (
-            <div className="ms-1">
-              <UserPicture
-                user={user}
-                noLink
-                additionalClassName="border border-info"
-              />
-            </div>
-          ))}
-          {remainingUsers.length > 0 && (
-            <div className="ms-1">
-              <button type="button" id="btn-editing-user" className="btn border-0 bg-info-subtle rounded-pill p-0">
-                <span className="fw-bold text-info p-1">+{remainingUsers.length}</span>
-              </button>
-              <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-editing-user" toggle={togglePopover} trigger="legacy">
-                <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
-                  <UserPictureList users={remainingUsers} />
-                </PopoverBody>
-              </Popover>
-            </div>
-          )}
-        </div>
-      )}
-    </div>
-  );
-};

+ 1 - 12
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -1,11 +1,7 @@
 import type { FC } from 'react';
 
-import { DevidedPagePath } from '@growi/core/dist/models';
-
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useEditingUsers } from '~/stores/use-editing-users';
 
-import { EditingUserList } from './EditingUserList';
 import { PagePathHeader } from './PagePathHeader';
 import { PageTitleHeader } from './PageTitleHeader';
 
@@ -15,28 +11,21 @@ const moduleClass = styles['page-header'] ?? '';
 
 export const PageHeader: FC = () => {
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: editingUsers } = useEditingUsers();
 
   if (currentPage == null) {
     return <></>;
   }
 
-  const dPagePath = new DevidedPagePath(currentPage.path, true);
-
   return (
     <div className={moduleClass}>
       <PagePathHeader
         currentPage={currentPage}
       />
-      <div className="row mt-2">
+      <div className="row mt-1">
         <PageTitleHeader
           className="col"
           currentPage={currentPage}
         />
-        <EditingUserList
-          className={`${dPagePath.isRoot ? 'mt-1' : 'col mt-2'}`}
-          userList={editingUsers?.userList ?? []}
-        />
       </div>
     </div>
   );

+ 1 - 1
apps/app/src/components/PageHeader/PagePathHeader.module.scss

@@ -13,7 +13,7 @@
     .btn {
       width: 24px;
       height: 24px;
-      transform: translateY(8px);
+      transform: translateY(12px);
     }
   }
 }

+ 1 - 0
apps/app/src/components/PageHeader/index.ts

@@ -0,0 +1 @@
+export * from './PageHeader';

+ 6 - 6
apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss

@@ -18,12 +18,12 @@
   }
 }
 
-// apply larger font when smaller than lg
-@include bs.media-breakpoint-down(lg) {
-  .btn-page-accessories :global {
-    .material-symbols-outlined {
-      font-size: 2em;
-    }
+// apply font-size
+.btn-page-accessories :global {
+  --bs-btn-font-size: 14px;
+
+  @include bs.media-breakpoint-down(lg) {
+    --bs-btn-font-size: 16px;
   }
 }
 

+ 1 - 1
apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx

@@ -27,7 +27,7 @@ export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
   return (
     <button
       type="button"
-      className={`btn btn-sm btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill`}
+      className={`btn btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill`}
       onClick={onClick}
     >
       <span className="grw-icon d-flex">{icon}</span>

+ 9 - 5
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -1,4 +1,4 @@
-import React, { Suspense, useCallback } from 'react';
+import React, { Suspense, useCallback, useRef } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { getIdForRef, type IPageInfoForOperation } from '@growi/core';
@@ -82,6 +82,8 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
   const { page, isSharedUser } = props;
 
+  const tagsRef = useRef<HTMLDivElement>(null);
+
   const { data: pageInfo } = useSWRxPageInfo(page._id);
 
   const pagePath = page.path;
@@ -93,9 +95,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
     <>
       {/* Tags */}
       { page.revision != null && (
-        <Suspense fallback={<PageTagsSkeleton />}>
-          <Tags pageId={page._id} revisionId={page.revision._id} />
-        </Suspense>
+        <div ref={tagsRef}>
+          <Suspense fallback={<PageTagsSkeleton />}>
+            <Tags pageId={page._id} revisionId={page.revision._id} />
+          </Suspense>
+        </div>
       ) }
 
       <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
@@ -127,7 +131,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       </div>
 
       <div className="d-none d-xl-block">
-        <TableOfContents />
+        <TableOfContents tagsElementHeight={tagsRef.current?.clientHeight} />
         {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
     </>

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

@@ -45,7 +45,7 @@ export const PageTags:FC<Props> = (props: Props) => {
           <NotAvailableForReadOnlyUser>
             <button
               type="button"
-              className={`btn btn-sm btn-outline-secondary rounded-pill ${styles['grw-tag-icon-button']}`}
+              className={`btn btn-edit-tags btn-outline-neutral-secondary rounded-pill ${styles['grw-tag-icon-button']}`}
               onClick={onClickEditTagsButton}
             >
               <span className="material-symbols-outlined">local_offer</span>
@@ -58,7 +58,7 @@ export const PageTags:FC<Props> = (props: Props) => {
           <button
             id="edit-tags-btn-wrapper-for-tooltip"
             type="button"
-            className="btn btn-link text-secondary p-0 border-0"
+            className="btn btn-link btn-edit-tags text-secondary p-0 border-0"
             onMouseEnter={onMouseEnterHandler}
             onMouseLeave={onMouseLeaveHandler}
             onClick={onClickEditTagsButton}

+ 9 - 5
apps/app/src/components/PageTags/TagLabels.module.scss

@@ -14,15 +14,19 @@ $grw-tag-label-font-size: 12px;
     max-height: 5rem;
   }
 
-  // apply larger font when smaller than lg
-  @include bs.media-breakpoint-down(lg) {
-    .material-symbols-outlined {
-      font-size: 2em;
+}
+
+// apply font-size
+.grw-tag-labels :global {
+  .btn-edit-tags {
+    --bs-btn-font-size: 14px;
+
+    @include bs.media-breakpoint-down(lg) {
+      --bs-btn-font-size: 16px;
     }
   }
 }
 
-
 .grw-tag-labels-skeleton :global {
   width: 137px;
   height: calc(#{$grw-tag-label-font-size} + #{bs.$badge-padding-y} * 2);

+ 3 - 2
apps/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,5 +1,6 @@
 import { pagePathUtils } from '@growi/core/dist/utils';
-import Link, { LinkProps } from 'next/link';
+import type { LinkProps } from 'next/link';
+import Link from 'next/link';
 
 import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
@@ -61,7 +62,7 @@ export const NextLink = (props: Props): JSX.Element => {
   if (isExternalLink(href, siteUrl)) {
     return (
       <a id={id} href={href} className={className} target="_blank" rel="noopener noreferrer" {...dataAttributes}>
-        {children}&nbsp;<i className="icon-share-alt small"></i>
+        {children}&nbsp;<span className="growi-custom-icons">external_link</span>
       </a>
     );
   }

+ 2 - 1
apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 
 import { useCreatePageAndTransit } from '~/client/services/create-page';
@@ -10,7 +11,7 @@ export const SidebarNotFound = (): JSX.Element => {
   const { createAndTransit } = useCreatePageAndTransit();
 
   const clickCreateButtonHandler = useCallback(async() => {
-    createAndTransit({ path: '/Sidebar', wip: false });
+    createAndTransit({ path: '/Sidebar', wip: false, origin: Origin.View });
   }, [createAndTransit]);
 
   return (

+ 8 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts

@@ -1,5 +1,7 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
+
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { useCurrentPagePath } from '~/stores/page';
 
@@ -18,7 +20,12 @@ export const useCreateNewPage: UseCreateNewPage = () => {
     if (isLoadingPagePath) return;
 
     return createAndTransit(
-      { parentPath: currentPagePath, optionalParentPath: '/', wip: true },
+      {
+        parentPath: currentPagePath,
+        optionalParentPath: '/',
+        wip: true,
+        origin: Origin.View,
+      },
     );
   }, [createAndTransit, currentPagePath, isLoadingPagePath]);
 

+ 2 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
@@ -32,7 +33,7 @@ export const useCreateTodaysMemo: UseCreateTodaysMemo = () => {
     if (!isCreatable || todaysPath == null) return;
 
     return createAndTransit(
-      { path: todaysPath, wip: true },
+      { path: todaysPath, wip: true, origin: Origin.View },
       { shouldCheckPageExists: true },
     );
   }, [createAndTransit, isCreatable, todaysPath]);

+ 33 - 0
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss

@@ -0,0 +1,33 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@include bs.color-mode(light) {
+  .personal-dropdown-header :global {
+    color: var(--bs-gray-600);
+  }
+
+  .personal-dropdown-item :global {
+    --bs-link-color-rgb:var(--bs-gray-600);
+    color: var(--bs-gray-600);
+  }
+}
+
+@include bs.color-mode(dark) {
+  .personal-dropdown-header :global {
+    color: var(--bs-gray-500);
+  }
+
+  .personal-dropdown-item :global {
+    --bs-link-color-rgb:var(--bs-gray-500);
+    color: var(--bs-gray-500);
+  }
+}
+
+.personal-dropdown-menu :global {
+  --bs-dropdown-font-size: 14px;
+}
+
+.personal-dropdown-header :global {
+  .item-text-email {
+    font-size: 10.5px;
+  }
+}

+ 28 - 20
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -15,6 +15,8 @@ import { useCurrentUser } from '~/stores/context';
 
 import { SkeletonItem } from './SkeletonItem';
 
+import styles from './PersonalDropdown.module.scss';
+
 const ProactiveQuestionnaireModal = dynamic(() => import('~/features/questionnaire/client/components/ProactiveQuestionnaireModal'), { ssr: false });
 
 export const PersonalDropdown = (): JSX.Element => {
@@ -52,42 +54,45 @@ export const PersonalDropdown = (): JSX.Element => {
         <DropdownMenu
           container="body"
           data-testid="personal-dropdown-menu"
+          className={styles['personal-dropdown-menu']}
         >
-          <DropdownItem header>
-            <div className="mt-2">
+          <DropdownItem className={styles['personal-dropdown-header']}>
+            <div className="mt-2 mb-3">
               <UserPicture user={currentUser} size="lg" noLink noTooltip />
             </div>
-            <div className="mt-3 ms-1 fs-5">{currentUser.name}</div>
-            <div className="mt-2 d-flex align-items-center">
-              <span className="material-symbols-outlined me-1">person</span>
-              {currentUser.username}
+            <div className="ms-1 fs-6">{currentUser.name}</div>
+            <div className="d-flex align-items-center my-2">
+              <small className="material-symbols-outlined me-1 pb-0 fs-6">person</small>
+              <span>{currentUser.username}</span>
             </div>
             <div className="d-flex align-items-center">
-              <span className="material-symbols-outlined me-1">mail</span>
-              <span className="grw-email-sm">{currentUser.email}</span>
+              <span className="material-symbols-outlined me-1 pb-0 fs-6">mail</span>
+              <span className="item-text-email">{currentUser.email}</span>
             </div>
           </DropdownItem>
 
-          <DropdownItem divider />
+          <DropdownItem className="my-3" divider />
 
-          <DropdownItem>
+          <DropdownItem className={`my-1 ${styles['personal-dropdown-item']}`}>
             <Link
               href={pagePathUtils.userHomepagePath(currentUser)}
               data-testid="grw-personal-dropdown-menu-user-home"
             >
-              <span className="text-muted">
-                <span className="material-symbols-outlined me-1">home</span>{t('personal_dropdown.home')}
+              <span className="d-flex align-items-center">
+                <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">home</span>
+                <span className="item-text">{t('personal_dropdown.home')}</span>
               </span>
             </Link>
           </DropdownItem>
 
-          <DropdownItem>
+          <DropdownItem className={`my-1 ${styles['personal-dropdown-item']}`}>
             <Link
               href="/me"
               data-testid="grw-personal-dropdown-menu-user-settings"
             >
-              <span className="text-muted">
-                <span className="material-symbols-outlined me-1">build</span>{t('personal_dropdown.settings')}
+              <span className="d-flex align-items-center">
+                <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">discover_tune</span>
+                <span className="item-text">{t('personal_dropdown.settings')}</span>
               </span>
             </Link>
           </DropdownItem>
@@ -95,15 +100,18 @@ export const PersonalDropdown = (): JSX.Element => {
           <DropdownItem
             data-testid="grw-proactive-questionnaire-modal-toggle-btn"
             onClick={() => setQuestionnaireModalOpen(true)}
+            className={`my-1 ${styles['personal-dropdown-item']}`}
           >
-            <span className="text-muted">
-              <span className="material-symbols-outlined me-1">edit</span>{t('personal_dropdown.feedback')}
+            <span className="d-flex align-items-center">
+              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">edit_note</span>
+              <span className="item-text">{t('personal_dropdown.feedback')}</span>
             </span>
           </DropdownItem>
 
-          <DropdownItem onClick={logoutHandler}>
-            <span className="text-muted">
-              <span className="material-symbols-outlined me-1">logout</span>{t('Sign out')}
+          <DropdownItem onClick={logoutHandler} className={`my-1 ${styles['personal-dropdown-item']}`}>
+            <span className="d-flex align-items-center">
+              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">logout</span>
+              <span className="item-text">{t('Sign out')}</span>
             </span>
           </DropdownItem>
         </DropdownMenu>

+ 3 - 2
apps/app/src/components/SlackNotification.tsx

@@ -1,5 +1,6 @@
 /* eslint-disable react/prop-types */
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { PopoverBody, PopoverHeader, UncontrolledPopover } from 'reactstrap';
@@ -56,7 +57,7 @@ export const SlackNotification: FC<SlackNotificationProps> = ({
           id={idForSlackPopover}
           type="text"
           value={slackChannels}
-          placeholder="Input channels"
+          placeholder={t('page_edit.input_channels', 'Input channels')}
           onChange={updateSlackChannelsHandler}
         />
         <UncontrolledPopover trigger="focus" placement="top" target={idForSlackPopover}>

+ 7 - 3
apps/app/src/components/TableOfContents.tsx

@@ -16,7 +16,11 @@ const { isUsersHomepage: _isUsersHomepage } = pagePathUtils;
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 
-const TableOfContents = (): JSX.Element => {
+type Props = {
+  tagsElementHeight?: number
+}
+
+const TableOfContents = ({ tagsElementHeight }: Props): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
 
   const isUsersHomePage = currentPagePath != null && _isUsersHomepage(currentPagePath);
@@ -30,7 +34,7 @@ const TableOfContents = (): JSX.Element => {
 
     // rendererOptions for redo calcViewHeight()
     // see: https://github.com/weseek/growi/pull/6791
-    if (parentElem == null || containerElem == null || rendererOptions == null) {
+    if (parentElem == null || containerElem == null || rendererOptions == null || tagsElementHeight == null) {
       return 0;
     }
     const parentBottom = parentElem.getBoundingClientRect().bottom;
@@ -47,7 +51,7 @@ const TableOfContents = (): JSX.Element => {
     }
     // bottom - revisionToc top
     return bottom - (containerTop + containerPaddingTop);
-  }, [isUsersHomePage, rendererOptions]);
+  }, [isUsersHomePage, rendererOptions, tagsElementHeight]);
 
   return (
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>

+ 4 - 1
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -1,5 +1,7 @@
 import React, { useState, type FC, useCallback } from 'react';
 
+import { Origin } from '@growi/core';
+
 import { createPage } from '~/client/services/page-operation';
 import { useSWRxPageChildren, mutatePageTree } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
@@ -75,6 +77,7 @@ export const useNewPageInput = (): UseNewPageInput => {
         // keep grant info undefined to inherit from parent
         grant: undefined,
         grantUserGroupIds: undefined,
+        origin: Origin.View,
         wip: shouldCreateWipPage(newPagePath),
       });
 
@@ -83,7 +86,7 @@ export const useNewPageInput = (): UseNewPageInput => {
       if (!hasDescendants) {
         stateHandlers?.setIsOpen(true);
       }
-    }, [hasDescendants, mutateChildren, stateHandlers]);
+    }, [hasDescendants, stateHandlers]);
 
     const submittionFailedHandler = useCallback(() => {
       setProcessingSubmission(false);

+ 10 - 10
apps/app/src/components/UsersHomepageFooter.module.scss

@@ -4,7 +4,10 @@ $grw-sidebar-content-footer-height: 50px;
 
 .user-page-footer :global {
   .grw-user-page-list-m {
-    .list-group{
+    .growi-custom-icons {
+      font-size: 1.1em;
+    }
+    .list-group {
       .list-group-item {
         .grw-visible-on-hover {
           display: none;
@@ -15,20 +18,19 @@ $grw-sidebar-content-footer-height: 50px;
             display: block;
           }
         }
-        .grw-triangle-container{
+        .grw-triangle-container {
           svg {
             width: 12px;
             height: 12px;
           }
         }
-        svg{
+        svg {
           width: 20px;
           height: 20px;
         }
         min-height: 40px;
         border-radius: 0px;
 
-
         &.grw-bookmark-item-list {
           .picture {
             width: 16px;
@@ -40,17 +42,15 @@ $grw-sidebar-content-footer-height: 50px;
               height: 20px;
             }
           }
-          svg{
+          svg {
             width: 14px;
             height: 14px;
           }
-          .grw-foldertree-control{
+          .grw-foldertree-control {
             margin-left: 1rem;
           }
         }
       }
-
-
     }
 
     .grw-foldertree-item-container {
@@ -58,7 +58,7 @@ $grw-sidebar-content-footer-height: 50px;
         max-width: 25%;
       }
     }
-    .grw-foldertree-title-anchor{
+    .grw-foldertree-title-anchor {
       width: fit-content !important;
       margin-right: 20px;
     }
@@ -67,7 +67,7 @@ $grw-sidebar-content-footer-height: 50px;
       height: 35px;
       margin-bottom: 6px;
     }
-    .new-bookmark-folder{
+    .new-bookmark-folder {
       max-height: 30px;
       svg {
         width: 18px;

+ 5 - 13
apps/app/src/components/UsersHomepageFooter.tsx

@@ -2,7 +2,6 @@ import React, { useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import styles from '~/components/UsersHomepageFooter.module.scss';
@@ -13,8 +12,8 @@ import { CompressIcon } from './Icons/CompressIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 
 export type UsersHomepageFooterProps = {
-  creatorId: string,
-}
+  creatorId: string;
+};
 
 export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Element => {
   const { t } = useTranslation();
@@ -30,15 +29,8 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
           <span style={{ fontSize: '1.3em' }} className="material-symbols-outlined">bookmark</span>
           {t('footer.bookmarks')}
           <span className="ms-auto ps-2 ">
-            <button
-              type="button"
-              className={`btn btn-sm grw-expand-compress-btn ${isExpanded ? 'active' : ''}`}
-              onClick={() => setIsExpanded(!isExpanded)}
-            >
-              { isExpanded
-                ? <ExpandIcon />
-                : <CompressIcon />
-              }
+            <button type="button" className={`btn btn-sm grw-expand-compress-btn ${isExpanded ? 'active' : ''}`} onClick={() => setIsExpanded(!isExpanded)}>
+              {isExpanded ? <ExpandIcon /> : <CompressIcon />}
             </button>
           </span>
         </h2>
@@ -49,7 +41,7 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">
         <h2 id="recently-created-list" className="grw-user-page-header border-bottom pb-2 mb-3">
-          <i id="recent-created-icon" className="me-1"><RecentlyCreatedIcon /></i>
+          <span className="growi-custom-icons me-1">recently_created</span>
           {t('footer.recently_created')}
         </h2>
         <div id="user-created-list" className={`page-list ${styles['page-list']}`}>

+ 18 - 3
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -1,6 +1,8 @@
 import type { FC } from 'react';
 import { useCallback, useMemo, useState } from 'react';
 
+import type { IGrantedGroup } from '@growi/core';
+import { GroupType, getIdForRef } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
@@ -11,17 +13,25 @@ import { UserGroupModal } from '~/components/Admin/UserGroup/UserGroupModal';
 import { UserGroupTable } from '~/components/Admin/UserGroup/UserGroupTable';
 import CustomNav from '~/components/CustomNavigation/CustomNav';
 import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import { useIsAclEnabled } from '~/stores/context';
+import { useSWRxUserGroupList } from '~/stores/user-group';
 
 import { useSWRxChildExternalUserGroupList, useSWRxExternalUserGroupList, useSWRxExternalUserGroupRelationList } from '../../stores/external-user-group';
 
 import { KeycloakGroupManagement } from './KeycloakGroupManagement';
 import { LdapGroupManagement } from './LdapGroupManagement';
-import { PageActionOnGroupDelete } from '~/interfaces/user-group';
 
 export const ExternalGroupManagement: FC = () => {
   const { data: externalUserGroupList, mutate: mutateExternalUserGroups } = useSWRxExternalUserGroupList();
+  const { data: userGroupList } = useSWRxUserGroupList();
   const externalUserGroups = externalUserGroupList != null ? externalUserGroupList : [];
+  const externalUserGroupsForDeleteModal: IGrantedGroup[] = externalUserGroups.map((group) => {
+    return { item: group, type: GroupType.externalUserGroup };
+  });
+  const userGroupsForDeleteModal: IGrantedGroup[] = userGroupList != null ? userGroupList.map((group) => {
+    return { item: group, type: GroupType.userGroup };
+  }) : [];
   const externalUserGroupIds = externalUserGroups.map(group => group._id);
 
   const { data: externalUserGroupRelationList } = useSWRxExternalUserGroupRelationList(externalUserGroupIds);
@@ -93,11 +103,16 @@ export const ExternalGroupManagement: FC = () => {
     }
   }, [t, mutateExternalUserGroups, hideUpdateModal]);
 
-  const deleteExternalUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteExternalUserGroupById = useCallback(async(
+      deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null,
+  ) => {
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       await apiv3Delete(`/external-user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -154,7 +169,7 @@ export const ExternalGroupManagement: FC = () => {
       />
 
       <UserGroupDeleteModal
-        userGroups={externalUserGroups}
+        userGroups={userGroupsForDeleteModal.concat(externalUserGroupsForDeleteModal)}
         deleteUserGroup={selectedExternalUserGroup}
         onDelete={deleteExternalUserGroupById}
         isShow={isDeleteModalShown}

+ 8 - 6
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -150,17 +150,19 @@ module.exports = (crowi: Crowi): Router => {
   router.delete('/:id', loginRequiredStrictly, adminRequired, validators.delete, apiV3FormValidator, addActivity,
     async(req: AuthorizedRequest, res: ApiV3Response) => {
       const { id: deleteGroupId } = req.params;
-      const { transferToUserGroupId } = req.query;
+      const { transferToUserGroupId, transferToUserGroupType } = req.query;
       const actionName = req.query.actionName as PageActionOnGroupDelete;
 
-      const transferGroupInfo = transferToUserGroupId != null ? {
-        item: transferToUserGroupId as string,
-        type: GroupType.externalUserGroup,
-      } : undefined;
+      const transferToUserGroup = typeof transferToUserGroupId === 'string'
+        && (transferToUserGroupType === GroupType.userGroup || transferToUserGroupType === GroupType.externalUserGroup)
+        ? {
+          item: transferToUserGroupId,
+          type: transferToUserGroupType,
+        } : undefined;
 
       try {
         const userGroups = await (crowi.userGroupService as UserGroupService)
-          .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo, ExternalUserGroup, ExternalUserGroupRelation);
+          .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferToUserGroup, ExternalUserGroup, ExternalUserGroupRelation);
 
         const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
         activityEvent.emit('update', res.locals.activity._id, parameters);

+ 5 - 2
apps/app/src/interfaces/apiv3/page.ts

@@ -1,5 +1,5 @@
 import type {
-  IPageHasId, IRevisionHasId, ITag,
+  IPageHasId, IRevisionHasId, ITag, Origin,
 } from '@growi/core';
 
 import type { IOptionsForCreate, IOptionsForUpdate } from '../page';
@@ -12,6 +12,8 @@ export type IApiv3PageCreateParams = IOptionsForCreate & {
   body?: string,
   pageTags?: string[],
 
+  origin?: Origin,
+
   isSlackEnabled?: boolean,
   slackChannels?: string,
 };
@@ -24,9 +26,10 @@ export type IApiv3PageCreateResponse = {
 
 export type IApiv3PageUpdateParams = IOptionsForUpdate & {
   pageId: string,
-  revisionId: string,
+  revisionId?: string,
   body: string,
 
+  origin?: Origin,
   isSlackEnabled?: boolean,
   slackChannels?: string,
 };

+ 0 - 11
apps/app/src/interfaces/editor-settings.ts

@@ -1,11 +0,0 @@
-import { type EditorTheme, type KeyMapMode } from '@growi/editor';
-
-export const DEFAULT_KEYMAP = 'default';
-export const DEFAULT_THEME = 'defaultlight';
-
-export interface IEditorSettings {
-  theme: undefined | EditorTheme,
-  keymapMode: undefined | KeyMapMode,
-  styleActiveLine: boolean,
-  autoFormatMarkdownTable: boolean,
-}

+ 3 - 1
apps/app/src/interfaces/page.ts

@@ -1,5 +1,5 @@
 import type {
-  GroupType, IGrantedGroup, IPageHasId, Nullable, PageGrant,
+  GroupType, IGrantedGroup, IPageHasId, Nullable, PageGrant, Origin,
 } from '@growi/core';
 
 import type { IPageOperationProcessData } from './page-operation';
@@ -33,6 +33,7 @@ export type IDeleteManyPageApiv3Result = {
 };
 
 export type IOptionsForUpdate = {
+  origin?: Origin
   grant?: PageGrant,
   userRelatedGrantUserGroupIds?: IGrantedGroup[],
   // isSyncRevisionToHackmd?: boolean,
@@ -44,5 +45,6 @@ export type IOptionsForCreate = {
   grantUserGroupIds?: IGrantedGroup[],
   overwriteScopesOfDescendants?: boolean,
 
+  origin?: Origin
   wip?: boolean,
 };

+ 1 - 1
apps/app/src/pages/installer.page.tsx

@@ -47,7 +47,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
         i18n: t('installer.tab'),
       },
       external_accounts: {
-        Icon: () => <i className="icon-fw icon-share-alt"></i>,
+        Icon: () => <span className="growi-custom-icons">external_link</span>,
         Content: DataTransferForm,
         i18n: tCommons('g2g_data_transfer.tab'),
       },

+ 4 - 4
apps/app/src/server/models/editor-settings.ts

@@ -1,13 +1,13 @@
+import type { EditorSettings } from '@growi/editor';
+import type { Model, Document } from 'mongoose';
 import {
-  Schema, Model, Document,
+  Schema,
 } from 'mongoose';
 
-import { IEditorSettings } from '~/interfaces/editor-settings';
-
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-export interface EditorSettingsDocument extends IEditorSettings, Document {
+export interface EditorSettingsDocument extends EditorSettings, Document {
   userId: Schema.Types.ObjectId,
 }
 export type EditorSettingsModel = Model<EditorSettingsDocument>

+ 9 - 2
apps/app/src/server/models/obsolete-page.js

@@ -1,4 +1,4 @@
-import { PageGrant, GroupType } from '@growi/core';
+import { GroupType, Origin } from '@growi/core';
 import { templateChecker, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import escapeStringRegexp from 'escape-string-regexp';
 
@@ -141,7 +141,14 @@ export const getPageSchema = (crowi) => {
     return relations.map((relation) => { return relation.relatedTag.name });
   };
 
-  pageSchema.methods.isUpdatable = function(previousRevision) {
+  pageSchema.methods.isUpdatable = async function(previousRevision, origin) {
+    const populatedPageDataWithRevisionOrigin = await this.populate('revision', 'origin');
+    const latestRevisionOrigin = populatedPageDataWithRevisionOrigin.revision.origin;
+    const ignoreLatestRevision = origin === Origin.Editor && (latestRevisionOrigin === Origin.Editor || latestRevisionOrigin === Origin.View);
+    if (ignoreLatestRevision) {
+      return true;
+    }
+
     const revision = this.latestRevision || this.revision;
     // comparing ObjectId with string
     // eslint-disable-next-line eqeqeq

+ 5 - 1
apps/app/src/server/models/revision.js

@@ -1,3 +1,5 @@
+import { allOrigin } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 // disable no-return-await for model functions
@@ -29,6 +31,7 @@ module.exports = function(crowi) {
     format: { type: String, default: 'markdown' },
     author: { type: ObjectId, ref: 'User' },
     hasDiffToPrev: { type: Boolean },
+    origin: { type: String, enum: allOrigin },
   }, {
     timestamps: { createdAt: true, updatedAt: false },
   });
@@ -38,7 +41,7 @@ module.exports = function(crowi) {
     return this.updateMany({ pageId }, { $set: updateData });
   };
 
-  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, options) {
+  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, origin, options) {
     const Revision = this;
 
     if (!options) {
@@ -56,6 +59,7 @@ module.exports = function(crowi) {
     newRevision.body = body;
     newRevision.format = format;
     newRevision.author = user._id;
+    newRevision.origin = origin;
     if (pageData.revision != null) {
       newRevision.hasDiffToPrev = body !== previousBody;
     }

+ 4 - 2
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -1,3 +1,4 @@
+import { allOrigin } from '@growi/core';
 import type {
   IPage, IUser, IUserHasId,
 } from '@growi/core';
@@ -117,6 +118,7 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
     body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
     body('wip').optional().isBoolean().withMessage('wip must be boolean'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
   ];
 
 
@@ -227,10 +229,10 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
       let createdPage;
       try {
         const {
-          grant, grantUserGroupIds, overwriteScopesOfDescendants, wip,
+          grant, grantUserGroupIds, overwriteScopesOfDescendants, wip, origin,
         } = req.body;
 
-        const options: IOptionsForCreate = { overwriteScopesOfDescendants, wip };
+        const options: IOptionsForCreate = { overwriteScopesOfDescendants, wip, origin };
         if (grant != null) {
           options.grant = grant;
           options.grantUserGroupIds = grantUserGroupIds;

+ 12 - 6
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -1,3 +1,4 @@
+import { allOrigin } from '@growi/core';
 import type {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
@@ -8,7 +9,7 @@ import { body } from 'express-validator';
 import mongoose from 'mongoose';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
-import type { IApiv3PageUpdateParams } from '~/interfaces/apiv3';
+import { type IApiv3PageUpdateParams } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type Crowi from '~/server/crowi';
@@ -63,7 +64,8 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const validator: ValidationChain[] = [
     body('pageId').exists().not().isEmpty({ ignore_whitespace: true })
       .withMessage("'pageId' must be specified"),
-    body('revisionId').exists().not().isEmpty({ ignore_whitespace: true })
+    body('revisionId').optional().exists().not()
+      .isEmpty({ ignore_whitespace: true })
       .withMessage("'revisionId' must be specified"),
     body('body').exists().isString()
       .withMessage("The empty value is not allowd for the 'body'"),
@@ -72,6 +74,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     body('overwriteScopesOfDescendants').optional().isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
     body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
   ];
 
 
@@ -101,7 +104,8 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const { revisionId, isSlackEnabled, slackChannels } = req.body;
     if (isSlackEnabled) {
       try {
-        const results = await crowi.userNotificationService.fire(updatedPage, req.user, slackChannels, 'update', { previousRevision: revisionId });
+        const option = revisionId != null ? { previousRevision: revisionId } : undefined;
+        const results = await crowi.userNotificationService.fire(updatedPage, req.user, slackChannels, 'update', option);
         results.forEach((result) => {
           if (result.status === 'rejected') {
             logger.error('Create user notification failed', result.reason);
@@ -120,7 +124,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity,
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
-      const { pageId, revisionId, body } = req.body;
+      const {
+        pageId, revisionId, body, origin,
+      } = req.body;
 
       // check page existence
       const isExist = await Page.count({ _id: pageId }) > 0;
@@ -130,7 +136,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
       // check revision
       const currentPage = await Page.findByIdAndViewer(pageId, req.user);
-      if (currentPage != null && !currentPage.isUpdatable(revisionId)) {
+      if (currentPage != null && !currentPage.isUpdatable(revisionId, origin)) {
         const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),
@@ -146,7 +152,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
       let updatedPage;
       try {
         const { grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants } = req.body;
-        const options: IOptionsForUpdate = { overwriteScopesOfDescendants };
+        const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin };
         if (grant != null) {
           options.grant = grant;
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;

+ 8 - 6
apps/app/src/server/routes/apiv3/user-group.js

@@ -429,15 +429,17 @@ module.exports = (crowi) => {
    */
   router.delete('/:id', loginRequiredStrictly, adminRequired, validator.delete, apiV3FormValidator, addActivity, async(req, res) => {
     const { id: deleteGroupId } = req.params;
-    const { actionName, transferToUserGroupId } = req.query;
+    const { actionName, transferToUserGroupId, transferToUserGroupType } = req.query;
 
-    const transferGroupInfo = transferToUserGroupId != null ? {
-      item: transferToUserGroupId,
-      type: GroupType.userGroup,
-    } : undefined;
+    const transferToUserGroup = typeof transferToUserGroupId === 'string'
+        && (transferToUserGroupType === GroupType.userGroup || transferToUserGroupType === GroupType.externalUserGroup)
+      ? {
+        item: transferToUserGroupId,
+        type: transferToUserGroupType,
+      } : undefined;
 
     try {
-      const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo);
+      const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferToUserGroup);
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

+ 5 - 5
apps/app/src/server/service/page/index.ts

@@ -3825,7 +3825,7 @@ class PageService implements IPageService {
 
     // Create revision
     const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
-    const newRevision = Revision.prepareRevision(savedPage, body, null, user);
+    const newRevision = Revision.prepareRevision(savedPage, body, null, user, options.origin);
     savedPage = await pushRevision(savedPage, newRevision, user);
     await savedPage.populateDataToShowRevision();
 
@@ -3919,7 +3919,7 @@ class PageService implements IPageService {
     page.applyScope(user, grant, grantUserGroupIds);
 
     let savedPage = await page.save();
-    const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
+    const newRevision = Revision.prepareRevision(savedPage, body, null, user, undefined, { format });
     savedPage = await pushRevision(savedPage, newRevision, user);
     await savedPage.populateDataToShowRevision();
 
@@ -4214,15 +4214,15 @@ class PageService implements IPageService {
     let savedPage = await newPageData.save();
 
     // Update body
-    const isBodyPresent = body != null && previousBody != null;
+    const isBodyPresent = body != null;
     const shouldUpdateBody = isBodyPresent;
     if (shouldUpdateBody) {
-      const newRevision = await Revision.prepareRevision(newPageData, body, previousBody, user);
+      const origin = options.origin;
+      const newRevision = await Revision.prepareRevision(newPageData, body, previousBody, user, origin);
       savedPage = await pushRevision(savedPage, newRevision, user);
       await savedPage.populateDataToShowRevision();
     }
 
-
     this.pageEvent.emit('update', savedPage, user);
 
     // Update ex children's parent

+ 4 - 4
apps/app/src/stores/editor.tsx

@@ -2,12 +2,12 @@ import { useCallback } from 'react';
 
 import { type Nullable } from '@growi/core';
 import { withUtils, type SWRResponseWithUtils } from '@growi/core/dist/swr';
+import type { EditorSettings } from '@growi/editor';
 import useSWR, { type SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import type { IEditorSettings } from '~/interfaces/editor-settings';
 import type { SlackChannels } from '~/interfaces/user-trigger-notification';
 
 import {
@@ -29,13 +29,13 @@ export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Er
 
 
 type EditorSettingsOperation = {
-  update: (updateData: Partial<IEditorSettings>) => Promise<void>,
+  update: (updateData: Partial<EditorSettings>) => Promise<void>,
 }
 
 // TODO: Enable localStorageMiddleware
 //   - Unabling localStorageMiddleware occurrs a flickering problem when loading theme.
 //   - see: https://github.com/weseek/growi/pull/6781#discussion_r1000285786
-export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, IEditorSettings, Error> => {
+export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, EditorSettings, Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
@@ -51,7 +51,7 @@ export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperatio
     },
   );
 
-  return withUtils<EditorSettingsOperation, IEditorSettings, Error>(swrResult, {
+  return withUtils<EditorSettingsOperation, EditorSettings, Error>(swrResult, {
     update: async(updateData) => {
       const { data, mutate } = swrResult;
 

+ 2 - 3
apps/app/src/styles/_fonts.scss

@@ -6,9 +6,8 @@
 
 .material-symbols-outlined {
   display: inline-block;
-  padding-bottom: 3px;
   font-family: var(--grw-font-family-material-symbols-outlined);
-  font-size: 24px;  /* Preferred icon size */
+  font-size: 1.5em;  /* Preferred icon size */
   font-style: normal;
   font-weight: normal;
   line-height: 1;
@@ -16,7 +15,7 @@
   letter-spacing: normal;
   word-wrap: normal;
   white-space: nowrap;
-  vertical-align: middle;
+  vertical-align: bottom;
   direction: ltr;
 
   &.fill {

+ 4 - 0
apps/app/src/styles/molecules/_list-group-item.scss

@@ -1,5 +1,9 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+.list-group-item.active {
+  --bs-list-group-active-color: var(--bs-list-group-color);
+}
+
 @include bs.color-mode(light) {
   .list-group-item-action {
     --bs-list-group-action-hover-bg: var(--grw-primary-100);

+ 2 - 0
bin/data-migrations/README.md

@@ -46,6 +46,8 @@ reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E6%9C%AA%E5
 - `v60x` or `v60x/index`: Migration for all notations in v6.0.x series
 - `v61x/mdcont`: Migration for mdcont notation only([reference](https://docs.growi.org/ja/admin-guide/upgrading/61x.html#%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4-%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%AB%E8%87%AA%E5%8B%95%E4%BB%98%E4%B8%8E%E3%81%95%E3%82%8C%E3%82%8B-mdcont-%E3%83%95%E3%82%9A%E3%83%AC%E3%83%95%E3%82%A3%E3%82%AF%E3%82%B9%E3%81%AE%E5%BB%83%E6%AD%A2))
 - `v61x` or `v61x/index`: Migration for all notations in v6.1.x series
+- `v70x/bootstrap5`: Migration for Bootstrap4 to Bootstrap5 
+- `v70x` or `v70x/index`: Migration for all notations in v7.0.x series
 - `custom`: You can define your own processors and apply them to `revision` (see "Advanced" below for details)
 
 ### Optional

+ 682 - 0
bin/data-migrations/src/migrations/v70x/bootstrap5.js

@@ -0,0 +1,682 @@
+/**
+ * @typedef {import('../../types').MigrationModule} MigrationModule
+ */
+
+// Script for migrating from Bootstrap4 to Bootstrap5 syntax
+// Inspired by https://github.com/coliff/bootstrap-5-migrate-tool/blob/main/gulpfile.js
+
+module.exports = [
+  /**
+   * @type {MigrationModule}
+   */
+  (body) => {
+    let replacedBody = body;
+
+    replacedBody = replacedBody.replace(
+      // eslint-disable-next-line max-len
+      /\sdata-(animation|autohide|boundary|container|content|custom-class|delay|dismiss|display|html|interval|keyboard|method|offset|pause|placement|popper-config|reference|ride|selector|slide(-to)?|target|template|title|toggle|touch|trigger|wrap)=/g,
+      (match, p1) => {
+        if (p1 === 'toggle' && match.includes('data-bs-toggle="')) {
+          return match;
+        }
+        return ` data-bs-${p1}=`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(/\[data-toggle=/g, '[data-bs-toggle=');
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-danger\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-danger${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-dark\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-dark${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-info\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-info${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-light\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-light${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-pill\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-pill${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-primary\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-primary${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-secondary\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-secondary${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-success\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-success${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bbadge-warning\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-bg-warning${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bborder-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}border-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bborder-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}border-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bclose\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}btn-close${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-control-input\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-check-input${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-control-label\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-check-label${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-control custom-checkbox\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-check${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-control custom-radio\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-check${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-file-input\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-control${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-file-label\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-label${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-range\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-range${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-select-sm\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-select-sm${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-select-lg\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-select-lg${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-select\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-select${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bcustom-control custom-switch\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-check form-switch${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-sm-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-sm-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-md-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-md-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-lg-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-lg-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-xl-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-xl-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-sm-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-sm-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-md-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-md-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-lg-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-lg-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropdown-menu-xl-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropdown-menu-xl-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropleft\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropstart${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bdropright\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}dropend${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-sm-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-sm-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-md-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-md-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-lg-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-lg-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-xl-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-xl-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-sm-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-sm-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-md-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-md-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-lg-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-lg-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfloat-xl-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}float-xl-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-italic\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fst-italic${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-weight-bold\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fw-bold${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-weight-bolder\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fw-bolder${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-weight-light\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fw-light${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-weight-lighter\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fw-lighter${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bfont-weight-normal\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}fw-normal${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bform-control-file\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-control${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bform-control-range\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}form-range${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bform-group\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}mb-3${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bform-inline\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}d-flex align-items-center${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bform-row\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}row${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bjumbotron-fluid\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-0 px-0${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bjumbotron\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}bg-light mb-4 rounded-2 py-5 px-3${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bmedia-body\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}flex-grow-1${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bmedia\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}d-flex${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bml-\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ms-${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bml-n\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ms-n${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bmr-\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}me-${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bmr-n\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}me-n${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bno-gutters\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}g-0${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bpl-\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ps-${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bpr-\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}pe-${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bpre-scrollable\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}overflow-y-scroll${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive-item\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive-16by9\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ratio-16x9${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive-1by1\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ratio-1x1${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive-21by9\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ratio-21x9${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive-4by3\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ratio-4x3${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bembed-responsive\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}ratio${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\brounded-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\brounded-lg\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-3${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\brounded-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\brounded-sm\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}rounded-1${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bsr-only-focusable\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}visually-hidden-focusable${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\bsr-only\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}visually-hidden${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-hide\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}d-none${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-sm-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-sm-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-md-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-md-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-lg-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-lg-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-xl-left\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-xl-start${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-sm-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-sm-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-md-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-md-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-lg-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-lg-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-xl-right\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}text-xl-end${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /(<[^>]*class\s*=\s*['"][^'"]*)\btext-monospace\b([^'"]*['"])/g,
+      (_, p1, p2) => {
+        return `${p1}font-monospace${p2}`;
+      },
+    );
+
+    replacedBody = replacedBody.replace(
+      /<select([^>]*)\bclass=['"]([^'"]*)form-control(-lg|-sm)?([^'"]*)['"]([^>]*)>/g, '<select$1class="$2form-select$3$4"$5>',
+    );
+
+    replacedBody = replacedBody.replace(/<select([^>]*)\bclass=['"]([^'"]*)form-control\b([^'"]*['"])/g, '<select$1class="$2form-select$3');
+
+    replacedBody = replacedBody.replace('<span aria-hidden="true">&times;</span>', '');
+
+    return replacedBody;
+  },
+];

+ 3 - 0
bin/data-migrations/src/migrations/v70x/index.js

@@ -0,0 +1,3 @@
+const bootstrap5 = require('./bootstrap5');
+
+module.exports = [...bootstrap5];

+ 7 - 4
packages/core/src/interfaces/growi-theme-metadata.ts

@@ -10,9 +10,12 @@ export type GrowiThemeMetadata = {
   name: string,
   manifestKey: string,
   schemeType: GrowiThemeSchemeType,
-  bg: string,
-  topbar: string,
-  sidebar: string,
-  accent: string,
+  lightBg: string,
+  darkBg: string,
+  lightSidebar: string,
+  darkSidebar: string,
+  lightIcon: string,
+  darkIcon: string,
+  createBtn: string,
   isPresetTheme?: boolean,
 };

+ 10 - 0
packages/core/src/interfaces/revision.ts

@@ -1,12 +1,22 @@
 import type { HasObjectId } from './has-object-id';
 import type { IUser } from './user';
 
+export const Origin = {
+  View: 'view',
+  Editor: 'editor',
+} as const;
+
+export type Origin = typeof Origin[keyof typeof Origin];
+
+export const allOrigin = Object.values(Origin);
+
 export type IRevision = {
   body: string,
   author: IUser,
   hasDiffToPrev: boolean;
   createdAt: Date,
   updatedAt: Date,
+  origin?: Origin,
 }
 
 export type IRevisionHasId = IRevision & HasObjectId;

+ 1 - 0
packages/custom-icons/svg/drawer_io.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a,.c{fill:none;}.a{stroke:#707070;}.b{clip-path:url(#a);}</style><clipPath id="a"><rect class="a" width="24" height="24" transform="translate(-14953 -18037)"/></clipPath></defs><g class="b" transform="translate(14953 18037)"><path class="c" d="M70.158,9.041H67.433L65.921,6.46a.909.909,0,0,0,.35-.706V1.477a.942.942,0,0,0-.95-.932H60.932a.942.942,0,0,0-.95.932V5.755a.911.911,0,0,0,.35.706l-1.511,2.58H56.057a.942.942,0,0,0-.949.932v4.277a.942.942,0,0,0,.949.932h4.389a.941.941,0,0,0,.949-.932V9.972a.941.941,0,0,0-.949-.932h-.107l1.38-2.354h2.815l1.379,2.354H65.77a.942.942,0,0,0-.95.932v4.277a.942.942,0,0,0,.95.932h4.388a.942.942,0,0,0,.95-.932V9.972a.942.942,0,0,0-.95-.932m-10.08,4.848H56.425V10.333h3.653ZM61.3,1.838h3.653V5.394H61.3Zm8.491,12.051H66.137V10.333h3.653Z" transform="translate(-15004.108 -18032.863)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/external_link.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="21.999" height="21.999" viewBox="0 0 21.999 21.999"><defs><style>.a{fill:none;}</style></defs><path class="a" d="M18.533,13.9h1.787v5.981A2.114,2.114,0,0,1,18.2,22H2.116A2.115,2.115,0,0,1,0,19.883V3.8A2.115,2.115,0,0,1,2.116,1.68H8.1V3.467H2.116a.329.329,0,0,0-.328.328V19.883a.329.329,0,0,0,.328.328H18.2a.329.329,0,0,0,.328-.328ZM11.788,0V1.7h7.252L7.094,13.641l1.264,1.265L20.3,2.96v7.253H22V0Z"/></svg>

+ 1 - 0
packages/custom-icons/svg/format_quote.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:none;}.b{clip-path:url(#a);}</style><clipPath id="a"><rect class="a" width="24" height="24" transform="translate(376 36)"/></clipPath></defs><g class="b" transform="translate(-376 -36)"><path class="a" d="M32.359,16.068v1.823H47.21V16.068Zm0-5.327H47.21V8.919H32.359Zm0-7.152H47.21V1.767H32.359ZM27.1,19.659h1.822V0H27.1Z" transform="translate(350.789 38.172)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/header.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:#fff;}.b{clip-path:url(#a);}.c{fill:none;}</style><clipPath id="a"><rect class="a" width="24" height="24" transform="translate(-14908 -18037)"/></clipPath></defs><g class="b" transform="translate(14908 18037)"><path class="c" d="M10.813.26v7H2.008v-7H0v16H2.008v-7h8.805v7h2.008V.26Z" transform="translate(-14902.41 -18033.26)"/></g></svg>

+ 1 - 8
packages/custom-icons/svg/recently_created.svg

@@ -1,8 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21.569" viewBox="0 0 21 21.569">
-  <g id="8883" data-name="8883" transform="translate(-288.73 -162.502)">
-    <path id="14245" data-name="14245" d="M18.841,15.3a4.123,4.123,0,1,1-1.028-2.714H16.271v1.158h3.213V10.53H18.326v.929A5.261,5.261,0,1,0,20,15.3Z" transform="translate(289.23 163.002)" fill="#C4C2BD" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1"/>
-    <path id="14246" data-name="14246" d="M14.151,12.351v3.165l2.021,2.312.872-.762L15.31,15.081v-2.73Z" transform="translate(289.23 163.002)" fill="#C4C2BD" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1"/>
-    <path id="14247" data-name="14247" d="M16.933,4.241a1.645,1.645,0,0,0,.489-1.19,1.582,1.582,0,0,0-.469-1.19L15.6.5a1.631,1.631,0,0,0-2.36,0L12.075,1.667l3.706,3.726Z" transform="translate(289.23 163.002)" fill="#C4C2BD" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1"/>
-    <path id="14248" data-name="14248" d="M10.932,2.819,0,13.751v3.706H3.706L14.638,6.525ZM3,15.81H1.647V14.459l9.3-9.3.682.683.667.667Z" transform="translate(289.23 163.002)" fill="#C4C2BD" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1"/>
-  </g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="20.001" height="20.568" viewBox="0 0 20.001 20.568"><defs><style>.a{fill:none;}</style></defs><path class="a" d="M15437.46,17968.3a5.262,5.262,0,0,1,8.868-3.844v-.93h1.154v3.217h-3.212v-1.158h1.543a4.109,4.109,0,1,0,1.028,2.715H15448a5.271,5.271,0,0,1-10.541,0Zm4.69.217v-3.168h1.159v2.734l1.732,1.98-.87.762Zm-14.15,1.939v-3.7l10.93-10.936,3.708,3.709-10.934,10.93Zm1.646-3v1.354h1.35l9.3-9.3-.668-.668-.681-.682Zm10.43-12.787,1.159-1.164a1.633,1.633,0,0,1,2.363,0l1.354,1.357a1.578,1.578,0,0,1,.469,1.186,1.647,1.647,0,0,1-.487,1.191l-1.154,1.15Z" transform="translate(-15428 -17953)"/></svg>

+ 1 - 1
packages/editor/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/editor",
-  "version": "6.2.0-RC.0",
+  "version": "7.0.0-RC.0",
   "license": "MIT",
   "type": "module",
   "module": "dist/index.js",

+ 13 - 49
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -1,21 +1,22 @@
 import {
-  forwardRef, useMemo, useRef, useEffect, useState,
+  forwardRef, useMemo, useRef, useEffect,
 } from 'react';
 
 import { indentUnit } from '@codemirror/language';
-import { Prec, Extension } from '@codemirror/state';
-import { EditorView } from '@codemirror/view';
+import {
+  EditorView,
+} from '@codemirror/view';
 import { AcceptedUploadFileType } from '@growi/core';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
-import { GlobalCodeMirrorEditorKey } from '../../consts';
+import { EditorSettings, GlobalCodeMirrorEditorKey } from '../../consts';
 import {
-  useFileDropzone, FileDropzoneOverlay, getEditorTheme, type EditorTheme, getKeymap, type KeyMapMode,
+  useFileDropzone, FileDropzoneOverlay,
 } from '../../services';
 import {
   adjustPasteData, getStrFromBol,
 } from '../../services/paste-util/paste-markdown-util';
-import { useCodeMirrorEditorIsolated } from '../../stores';
+import { useDefaultExtensions, useCodeMirrorEditorIsolated, useEditorSettings } from '../../stores';
 
 import { Toolbar } from './Toolbar';
 
@@ -31,8 +32,7 @@ const CodeMirrorEditorContainer = forwardRef<HTMLDivElement>((props, ref) => {
 export type CodeMirrorEditorProps = {
   acceptedUploadFileType?: AcceptedUploadFileType,
   indentSize?: number,
-  editorTheme?: EditorTheme,
-  editorKeymap?: KeyMapMode,
+  editorSettings?: EditorSettings,
   onChange?: (value: string) => void,
   onSave?: () => void,
   onUpload?: (files: File[]) => void,
@@ -48,8 +48,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     editorKey,
     acceptedUploadFileType = AcceptedUploadFileType.NONE,
     indentSize,
-    editorTheme,
-    editorKeymap,
+    editorSettings,
     onChange,
     onSave,
     onUpload,
@@ -65,6 +64,9 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   }, [onChange]);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey, containerRef.current, cmProps);
 
+  useDefaultExtensions(codeMirrorEditor);
+  useEditorSettings(codeMirrorEditor, editorSettings, onSave);
+
   useEffect(() => {
     if (indentSize == null) {
       return;
@@ -76,6 +78,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
   }, [codeMirrorEditor, indentSize]);
 
+
   useEffect(() => {
     const handlePaste = (event: ClipboardEvent) => {
       event.preventDefault();
@@ -150,45 +153,6 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   }, [onScroll, codeMirrorEditor]);
 
 
-  const [themeExtension, setThemeExtension] = useState<Extension | undefined>(undefined);
-  useEffect(() => {
-    const settingTheme = async(name?: EditorTheme) => {
-      setThemeExtension(await getEditorTheme(name ?? 'defaultlight'));
-    };
-    settingTheme(editorTheme);
-  }, [codeMirrorEditor, editorTheme, setThemeExtension]);
-
-  useEffect(() => {
-    if (themeExtension == null) {
-      return;
-    }
-    // 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(themeExtension));
-    return cleanupFunction;
-  }, [codeMirrorEditor, themeExtension]);
-
-
-  const [keymapExtension, setKeymapExtension] = useState<Extension | undefined>(undefined);
-  useEffect(() => {
-    const settingKeyMap = async(name?: KeyMapMode) => {
-      setKeymapExtension(await getKeymap(name ?? 'default'));
-    };
-    settingKeyMap(editorKeymap);
-
-  }, [codeMirrorEditor, editorKeymap, setKeymapExtension]);
-
-  useEffect(() => {
-    if (keymapExtension == null) {
-      return;
-    }
-
-    // Prevent these Keybind from overwriting the originally defined keymap.
-    const cleanupFunction = codeMirrorEditor?.appendExtensions(Prec.low(keymapExtension));
-    return cleanupFunction;
-
-  }, [codeMirrorEditor, keymapExtension, onSave]);
-
   const {
     getRootProps,
     getInputProps,

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -14,7 +14,7 @@ export const DiagramButton = (props: Props): JSX.Element => {
   }, [editorKey, openDrawioModal]);
   return (
     <button type="button" className="btn btn-toolbar-button" onClick={onClickDiagramButton}>
-      <span className="material-symbols-outlined fs-5">lan</span>
+      <span className="growi-custom-icons">drawer_io</span>
     </button>
   );
 };

+ 2 - 2
packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -69,7 +69,7 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             <span className="material-symbols-outlined fs-5">format_strikethrough</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('#', true)}>
-            <span className="material-symbols-outlined fs-5">block</span>
+            <span className="growi-custom-icons">header</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertMarkdownElements('`', '`')}>
             <span className="material-symbols-outlined fs-5">code</span>
@@ -81,7 +81,7 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             <span className="material-symbols-outlined fs-5">format_list_numbered</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('>')}>
-            <span className="material-symbols-outlined fs-5">block</span>
+            <span className="growi-custom-icons">format_quote</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('- [ ]')}>
             <span className="material-symbols-outlined fs-5">checklist</span>

+ 3 - 5
packages/editor/src/components/CodeMirrorEditorComment.tsx

@@ -18,8 +18,7 @@ type Props = CodeMirrorEditorProps & object
 
 export const CodeMirrorEditorComment = (props: Props): JSX.Element => {
   const {
-    acceptedUploadFileType,
-    onSave, onChange, onUpload,
+    onSave, ...otherProps
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.COMMENT);
@@ -57,9 +56,8 @@ export const CodeMirrorEditorComment = (props: Props): JSX.Element => {
   return (
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.COMMENT}
-      acceptedUploadFileType={acceptedUploadFileType}
-      onChange={onChange}
-      onUpload={onUpload}
+      onSave={onSave}
+      {...otherProps}
     />
   );
 };

+ 4 - 14
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -21,21 +21,18 @@ type Props = CodeMirrorEditorProps & {
   user?: IUserHasId,
   pageId?: string,
   initialValue?: string,
-  onOpenEditor?: (markdown: string) => void,
   onEditorsUpdated?: (userList: IUserHasId[]) => void,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    acceptedUploadFileType,
-    indentSize, user, pageId, initialValue,
-    editorTheme, editorKeymap,
-    onSave, onChange, onUpload, onScroll, onOpenEditor, onEditorsUpdated,
+    user, pageId, initialValue,
+    onSave, onEditorsUpdated, ...otherProps
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
-  useCollaborativeEditorMode(user, pageId, initialValue, onOpenEditor, onEditorsUpdated, codeMirrorEditor);
+  useCollaborativeEditorMode(user, pageId, initialValue, onEditorsUpdated, codeMirrorEditor);
 
   // setup additional extensions
   useEffect(() => {
@@ -67,18 +64,11 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     return cleanupFunction;
   }, [codeMirrorEditor, onSave]);
 
-
   return (
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}
-      onChange={onChange}
       onSave={onSave}
-      onUpload={onUpload}
-      onScroll={onScroll}
-      acceptedUploadFileType={acceptedUploadFileType}
-      indentSize={indentSize}
-      editorTheme={editorTheme}
-      editorKeymap={editorKeymap}
+      {...otherProps}
     />
   );
 };

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

@@ -5,7 +5,7 @@ import {
 import { AcceptedUploadFileType } from '@growi/core';
 import { toast } from 'react-toastify';
 
-import { GlobalCodeMirrorEditorKey } from '../../consts';
+import { EditorSettings, GlobalCodeMirrorEditorKey } from '../../consts';
 import type { EditorTheme, KeyMapMode } from '../../services';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 import { CodeMirrorEditorMain } from '../CodeMirrorEditorMain';
@@ -18,6 +18,7 @@ export const Playground = (): JSX.Element => {
   const [markdownToPreview, setMarkdownToPreview] = useState('');
   const [editorTheme, setEditorTheme] = useState<EditorTheme>('defaultlight');
   const [editorKeymap, setEditorKeymap] = useState<KeyMapMode>('default');
+  const [editorSettings, setEditorSettings] = useState<EditorSettings>();
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
@@ -34,6 +35,15 @@ export const Playground = (): JSX.Element => {
     codeMirrorEditor?.setCaretLine();
   }, [codeMirrorEditor]);
 
+  useEffect(() => {
+    setEditorSettings({
+      theme: editorTheme,
+      keymapMode: editorKeymap,
+      styleActiveLine: true,
+      autoFormatMarkdownTable: true,
+    });
+  }, [setEditorSettings, editorKeymap, editorTheme]);
+
   // set handler to save with shortcut key
   const saveHandler = useCallback(() => {
     // eslint-disable-next-line no-console
@@ -65,8 +75,7 @@ export const Playground = (): JSX.Element => {
             onUpload={uploadHandler}
             indentSize={4}
             acceptedUploadFileType={AcceptedUploadFileType.ALL}
-            editorTheme={editorTheme}
-            editorKeymap={editorKeymap}
+            editorSettings={editorSettings}
           />
         </div>
         <div className="flex-expand-vert d-none d-lg-flex bg-light text-dark border-start border-dark-subtle p-3">

+ 8 - 0
packages/editor/src/consts/editor-settings.ts

@@ -0,0 +1,8 @@
+import { EditorTheme, KeyMapMode } from '../services';
+
+export interface EditorSettings {
+  theme: undefined | EditorTheme,
+  keymapMode: undefined | KeyMapMode,
+  styleActiveLine: boolean,
+  autoFormatMarkdownTable: boolean,
+}

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

@@ -1,2 +1,3 @@
 export * from './global-code-mirror-editor-key';
 export * from './ydoc-awareness-user-color';
+export * from './editor-settings';

+ 4 - 62
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -1,27 +1,11 @@
 import { useMemo } from 'react';
 
-import { indentWithTab, defaultKeymap, deleteCharBackward } from '@codemirror/commands';
 import {
-  markdown, markdownLanguage,
-} from '@codemirror/lang-markdown';
-import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language';
-import { languages } from '@codemirror/language-data';
-import {
-  EditorState, Prec, type Extension,
+  EditorState,
 } from '@codemirror/state';
-import { keymap, EditorView } from '@codemirror/view';
-import type { Command } from '@codemirror/view';
-import { tags } from '@lezer/highlight';
+import { EditorView } from '@codemirror/view';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 import deepmerge from 'ts-deepmerge';
-// see: https://github.com/yjs/y-codemirror.next#example
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-import { yUndoManagerKeymap } from 'y-codemirror.next';
-
-import { emojiAutocompletionSettings } from '../../extensions/emojiAutocompletionSettings';
-import { insertNewlineContinueMarkup } from '../../list-util/insert-newline-continue-markup';
-import { insertNewRowToMarkdownTable, isInTable } from '../../table-util/insert-new-row-to-table-markdown';
 
 import { useAppendExtensions, type AppendExtensions } from './utils/append-extensions';
 import { useFocus, type Focus } from './utils/focus';
@@ -35,34 +19,6 @@ import { useReplaceText, type ReplaceText } from './utils/replace-text';
 import { useSetCaretLine, type SetCaretLine } from './utils/set-caret-line';
 
 
-const onPressEnter: Command = (editor) => {
-
-  if (isInTable(editor)) {
-    insertNewRowToMarkdownTable(editor);
-    return true;
-  }
-
-  insertNewlineContinueMarkup(editor);
-
-  return true;
-};
-
-// set new markdownKeymap instead of default one
-// https://github.com/codemirror/lang-markdown/blob/main/src/index.ts#L17
-const markdownKeymap = [
-  { key: 'Backspace', run: deleteCharBackward },
-  { key: 'Enter', run: onPressEnter },
-];
-
-const markdownHighlighting = HighlightStyle.define([
-  { tag: tags.heading1, class: 'cm-header-1 cm-header' },
-  { tag: tags.heading2, class: 'cm-header-2 cm-header' },
-  { tag: tags.heading3, class: 'cm-header-3 cm-header' },
-  { tag: tags.heading4, class: 'cm-header-4 cm-header' },
-  { tag: tags.heading5, class: 'cm-header-5 cm-header' },
-  { tag: tags.heading6, class: 'cm-header-6 cm-header' },
-]);
-
 type UseCodeMirrorEditorUtils = {
   initDoc: InitDoc,
   appendExtensions: AppendExtensions,
@@ -81,28 +37,12 @@ export type UseCodeMirrorEditor = {
 } & UseCodeMirrorEditorUtils;
 
 
-const defaultExtensions: Extension[] = [
-  EditorView.lineWrapping,
-  markdown({ base: markdownLanguage, codeLanguages: languages, addKeymap: false }),
-  keymap.of(markdownKeymap),
-  keymap.of([indentWithTab]),
-  Prec.lowest(keymap.of(defaultKeymap)),
-  syntaxHighlighting(markdownHighlighting),
-  Prec.lowest(syntaxHighlighting(defaultHighlightStyle)),
-  emojiAutocompletionSettings,
-  keymap.of(yUndoManagerKeymap),
-];
-
-
 export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor => {
 
   const mergedProps = useMemo(() => {
     return deepmerge(
       props ?? {},
       {
-        extensions: [
-          defaultExtensions,
-        ],
         // Reset settings of react-codemirror.
         // Extensions are defined first will be used if they have the same priority.
         // If extensions conflict, disable them here.
@@ -113,6 +53,8 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
         basicSetup: {
           defaultKeymap: false,
           dropCursor: false,
+          highlightActiveLine: false,
+          highlightActiveLineGutter: false,
           // Disabled react-codemirror history for Y.UndoManager
           history: false,
         },

+ 2 - 1
packages/editor/src/services/editor-theme/index.ts

@@ -1,6 +1,6 @@
 import { Extension } from '@codemirror/state';
 
-export const getEditorTheme = async(themeName: EditorTheme): Promise<Extension> => {
+export const getEditorTheme = async(themeName?: EditorTheme): Promise<Extension> => {
   switch (themeName) {
     case 'eclipse':
       return (await import('@uiw/codemirror-theme-eclipse')).eclipse;
@@ -37,5 +37,6 @@ const EditorTheme = {
   kimbie: 'kimbie',
 } as const;
 
+export const DEFAULT_THEME = 'defaultlight';
 export const AllEditorTheme = Object.values(EditorTheme);
 export type EditorTheme = typeof EditorTheme[keyof typeof EditorTheme]

+ 3 - 3
packages/editor/src/services/keymaps/index.ts

@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
 import { keymap } from '@codemirror/view';
 
 
-export const getKeymap = async(keyMapName: KeyMapMode, onSave?: () => void): Promise<Extension> => {
+export const getKeymap = async(keyMapName?: KeyMapMode, onSave?: () => void): Promise<Extension> => {
   switch (keyMapName) {
     case 'vim':
       return (await import('./vim')).vimKeymap(onSave);
@@ -10,9 +10,8 @@ export const getKeymap = async(keyMapName: KeyMapMode, onSave?: () => void): Pro
       return (await import('@replit/codemirror-emacs')).emacs();
     case 'vscode':
       return keymap.of((await import('@replit/codemirror-vscode-keymap')).vscodeKeymap);
-    case 'default':
-      return keymap.of((await import('@codemirror/commands')).defaultKeymap);
   }
+  return keymap.of((await import('@codemirror/commands')).defaultKeymap);
 };
 
 const KeyMapMode = {
@@ -22,5 +21,6 @@ const KeyMapMode = {
   vscode: 'vscode',
 } as const;
 
+export const DEFAULT_KEYMAP = 'default';
 export const AllKeyMap = Object.values(KeyMapMode);
 export type KeyMapMode = typeof KeyMapMode[keyof typeof KeyMapMode];

+ 2 - 0
packages/editor/src/stores/index.ts

@@ -1,3 +1,5 @@
 export * from './codemirror-editor';
 export * from './use-resolved-theme';
 export * from './use-collaborative-editor-mode';
+export * from './use-editor-settings';
+export * from './use-default-extensions';

+ 6 - 20
packages/editor/src/stores/use-collaborative-editor-mode.ts

@@ -23,18 +23,16 @@ export const useCollaborativeEditorMode = (
     user?: IUserHasId,
     pageId?: string,
     initialValue?: string,
-    onOpenEditor?: (markdown: string) => void,
     onEditorsUpdated?: (userList: IUserHasId[]) => void,
     codeMirrorEditor?: UseCodeMirrorEditor,
 ): void => {
   const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
   const [provider, setProvider] = useState<SocketIOProvider | null>(null);
-  const [isInit, setIsInit] = useState(false);
   const [cPageId, setCPageId] = useState(pageId);
 
   const { data: socket } = useGlobalSocket();
 
-  const cleanupYDocAndProvider = () => {
+  const cleanupYDoc = () => {
     if (cPageId === pageId) {
       return;
     }
@@ -49,7 +47,6 @@ export const useCollaborativeEditorMode = (
     // TODO: catch ydoc:sync:error GlobalSocketEventName.YDocSyncError
     socket?.off(GlobalSocketEventName.YDocSync);
 
-    setIsInit(false);
     setCPageId(pageId);
 
     // reset editors
@@ -112,35 +109,24 @@ export const useCollaborativeEditorMode = (
   };
 
   const setupYDocExtensions = () => {
-    if (ydoc == null || provider == null) {
+    if (ydoc == null || provider == null || codeMirrorEditor == null) {
       return;
     }
 
     const ytext = ydoc.getText('codemirror');
     const undoManager = new Y.UndoManager(ytext);
 
-    const cleanup = codeMirrorEditor?.appendExtensions?.([
+    codeMirrorEditor.initDoc(ytext.toString());
+
+    const cleanup = codeMirrorEditor.appendExtensions([
       yCollab(ytext, provider.awareness, { undoManager }),
     ]);
 
     return cleanup;
   };
 
-  const initializeEditor = () => {
-    if (ydoc == null || onOpenEditor == null || isInit === true) {
-      return;
-    }
-
-    const ytext = ydoc.getText('codemirror');
-    codeMirrorEditor?.initDoc(ytext.toString());
-    onOpenEditor(ytext.toString());
-
-    setIsInit(true);
-  };
-
-  useEffect(cleanupYDocAndProvider, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
+  useEffect(cleanupYDoc, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
   useEffect(setupYDoc, [provider, ydoc]);
   useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
   useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
-  useEffect(initializeEditor, [codeMirrorEditor, isInit, onOpenEditor, ydoc]);
 };

+ 52 - 0
packages/editor/src/stores/use-default-extensions.ts

@@ -0,0 +1,52 @@
+import { indentWithTab, defaultKeymap, deleteCharBackward } from '@codemirror/commands';
+import {
+  markdown, markdownLanguage,
+} from '@codemirror/lang-markdown';
+import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language';
+import { languages } from '@codemirror/language-data';
+import {
+  Prec, type Extension,
+} from '@codemirror/state';
+import { keymap, EditorView, KeyBinding } from '@codemirror/view';
+import { tags } from '@lezer/highlight';
+// see: https://github.com/yjs/y-codemirror.next#example
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import { yUndoManagerKeymap } from 'y-codemirror.next';
+
+import type { UseCodeMirrorEditor } from '../services';
+import { emojiAutocompletionSettings } from '../services/extensions/emojiAutocompletionSettings';
+
+
+// set new markdownKeymap instead of default one
+// https://github.com/codemirror/lang-markdown/blob/main/src/index.ts#L17
+const markdownKeymap: KeyBinding[] = [
+  { key: 'Backspace', run: deleteCharBackward },
+];
+
+const markdownHighlighting = HighlightStyle.define([
+  { tag: tags.heading1, class: 'cm-header-1 cm-header' },
+  { tag: tags.heading2, class: 'cm-header-2 cm-header' },
+  { tag: tags.heading3, class: 'cm-header-3 cm-header' },
+  { tag: tags.heading4, class: 'cm-header-4 cm-header' },
+  { tag: tags.heading5, class: 'cm-header-5 cm-header' },
+  { tag: tags.heading6, class: 'cm-header-6 cm-header' },
+]);
+
+const defaultExtensions: Extension[] = [
+  EditorView.lineWrapping,
+  markdown({ base: markdownLanguage, codeLanguages: languages, addKeymap: false }),
+  keymap.of(markdownKeymap),
+  keymap.of([indentWithTab]),
+  Prec.lowest(keymap.of(defaultKeymap)),
+  keymap.of(yUndoManagerKeymap),
+  syntaxHighlighting(markdownHighlighting),
+  Prec.lowest(syntaxHighlighting(defaultHighlightStyle)),
+  emojiAutocompletionSettings,
+];
+
+export const useDefaultExtensions = (
+    codeMirrorEditor?: UseCodeMirrorEditor,
+): void => {
+  codeMirrorEditor?.appendExtensions([defaultExtensions]);
+};

+ 91 - 0
packages/editor/src/stores/use-editor-settings.ts

@@ -0,0 +1,91 @@
+import { useEffect, useCallback, useState } from 'react';
+
+import { Prec, Extension } from '@codemirror/state';
+import {
+  keymap, type Command, highlightActiveLine, highlightActiveLineGutter,
+} from '@codemirror/view';
+
+import type { EditorSettings } from '../consts';
+import type { UseCodeMirrorEditor, EditorTheme, KeyMapMode } from '../services';
+import { getEditorTheme, getKeymap } from '../services';
+import { insertNewlineContinueMarkup } from '../services/list-util/insert-newline-continue-markup';
+import { insertNewRowToMarkdownTable, isInTable } from '../services/table-util/insert-new-row-to-table-markdown';
+
+export const useEditorSettings = (
+    codeMirrorEditor?: UseCodeMirrorEditor,
+    editorSetings?: EditorSettings,
+    onSave?: () => void,
+): void => {
+
+  useEffect(() => {
+    if (editorSetings?.styleActiveLine == null) {
+      return;
+    }
+    const extensions = (editorSetings?.styleActiveLine) ? [[highlightActiveLine(), highlightActiveLineGutter()]] : [[]];
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(extensions);
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, editorSetings?.styleActiveLine]);
+
+  const onPressEnter: Command = useCallback((editor) => {
+    if (isInTable(editor) && editorSetings?.autoFormatMarkdownTable) {
+      insertNewRowToMarkdownTable(editor);
+      return true;
+    }
+    insertNewlineContinueMarkup(editor);
+    return true;
+  }, [editorSetings?.autoFormatMarkdownTable]);
+
+
+  useEffect(() => {
+
+    const extension = keymap.of([
+      { key: 'Enter', run: onPressEnter },
+    ]);
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(extension);
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, onPressEnter]);
+
+  const [themeExtension, setThemeExtension] = useState<Extension | undefined>(undefined);
+  useEffect(() => {
+    const settingTheme = async(name?: EditorTheme) => {
+      setThemeExtension(await getEditorTheme(name));
+    };
+    settingTheme(editorSetings?.theme);
+  }, [codeMirrorEditor, editorSetings?.theme, setThemeExtension]);
+
+  useEffect(() => {
+    if (themeExtension == null) {
+      return;
+    }
+    // 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(themeExtension));
+    return cleanupFunction;
+  }, [codeMirrorEditor, themeExtension]);
+
+
+  const [keymapExtension, setKeymapExtension] = useState<Extension | undefined>(undefined);
+  useEffect(() => {
+    const settingKeyMap = async(name?: KeyMapMode) => {
+      setKeymapExtension(await getKeymap(name, onSave));
+    };
+    settingKeyMap(editorSetings?.keymapMode);
+
+  }, [codeMirrorEditor, editorSetings?.keymapMode, setKeymapExtension, onSave]);
+
+  useEffect(() => {
+    if (keymapExtension == null) {
+      return;
+    }
+
+    // Prevent these Keybind from overwriting the originally defined keymap.
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(Prec.low(keymapExtension));
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, keymapExtension]);
+
+};

+ 155 - 27
packages/preset-themes/src/consts/preset-themes.ts

@@ -4,13 +4,13 @@ const { BOTH, LIGHT, DARK } = GrowiThemeSchemeType;
 
 export const PresetThemes = {
   DEFAULT: 'default',
-  ANTARCTIC: 'antarctic',
+  // ANTARCTIC: 'antarctic',
   BLACKBOARD: 'blackboard',
-  CHRISTMAS: 'christmas',
+  // CHRISTMAS: 'christmas',
   FIRE_RED: 'fire-red',
   FUTURE: 'future',
   HALLOWEEN: 'halloween',
-  HUFFLEPUFF: 'hufflepuff',
+  // HUFFLEPUFF: 'hufflepuff',
   ISLAND: 'island',
   JADE_GREEN: 'jade-green',
   KIBELA: 'kibela',
@@ -28,10 +28,13 @@ export const DefaultThemeMetadata: GrowiThemeMetadata = {
   name: PresetThemes.DEFAULT,
   manifestKey: `src/styles/${PresetThemes.DEFAULT}.scss`,
   schemeType: BOTH,
-  bg: '#ffffff',
-  topbar: '#2a2929',
-  sidebar: '#122c55',
-  accent: '#209fd8',
+  lightBg: '#FFFFFF',
+  darkBg: '#1C1A1A',
+  lightSidebar: '#F8F7F7',
+  darkSidebar: '#434240',
+  lightIcon: '#8A8887',
+  darkIcon: '#D1D0CC',
+  createBtn: '#007EB0',
   isPresetTheme: true,
 };
 
@@ -39,39 +42,164 @@ export const PresetThemesMetadatas: GrowiThemeMetadata[] = [
   // support both of light and dark
   DefaultThemeMetadata,
   {
-    name: PresetThemes.MONO_BLUE,     schemeType: BOTH, bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', accent: '#00587A',
-  }, {
-    name: PresetThemes.HUFFLEPUFF,    schemeType: BOTH, bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', accent: '#993439',
-  }, {
-    name: PresetThemes.FIRE_RED,      schemeType: BOTH, bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', accent: '#EA5532',
+    name: PresetThemes.MONO_BLUE,
+    schemeType: BOTH,
+    lightBg: '#FFFFFF',
+    darkBg: '#16202C',
+    lightSidebar: '#EDFAFD',
+    darkSidebar: '#0A2E53',
+    lightIcon: '#36819C',
+    darkIcon: '#69B0C7',
+    createBtn: '#439CB9',
+  },
+  //  {
+  //   name: PresetThemes.HUFFLEPUFF,
+  //   schemeType: BOTH,
+  //   lightBg: '#FFFEFD',
+  //   darkBg: '#26231E',
+  //   lightSidebar: '#FEEBA5',
+  //   darkSidebar: '#5C4209',
+  //   lightIcon: '#B88512',
+  //   darkIcon: '#EBB845',
+  //   createBtn: '#403C39',
+  // },
+  {
+    name: PresetThemes.FIRE_RED,
+    schemeType: BOTH,
+    lightBg: '#FFFFFF',
+    darkBg: '#120700',
+    lightSidebar: '#FADDD6',
+    darkSidebar: '#5A4F4A',
+    lightIcon: '#94351E',
+    darkIcon: '#EE775B',
+    createBtn: '#EA5532',
   }, {
-    name: PresetThemes.JADE_GREEN,    schemeType: BOTH, bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', accent: '#38B48B',
+    name: PresetThemes.JADE_GREEN,
+    schemeType: BOTH,
+    lightBg: '#FFFFFF',
+    darkBg: '#120700',
+    lightSidebar: '#F1F3F2',
+    darkSidebar: '#4B4E4C',
+    lightIcon: '#3A8F6F',
+    darkIcon: '#5FC2A2',
+    createBtn: '#49B38A',
   }, {
-    name: PresetThemes.CLASSIC,    schemeType: BOTH, bg: '#FDFDFD', topbar: '#E1E9F4', sidebar: '#E1E9F4', accent: '#439FD8',
+    name: PresetThemes.CLASSIC,
+    schemeType: BOTH,
+    lightBg: '#FFFFFF',
+    darkBg: '#151A1F',
+    lightSidebar: '#F0F4FA',
+    darkSidebar: '#29343F',
+    lightIcon: '#53687E',
+    darkIcon: '#869BB1',
+    createBtn: '#3491CB',
   },
   // light only
   {
-    name: PresetThemes.NATURE,        schemeType: LIGHT, bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', accent: '#460039',
-  }, {
-    name: PresetThemes.WOOD,          schemeType: LIGHT, bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', accent: '#aaa45f',
+    name: PresetThemes.NATURE,
+    schemeType: LIGHT,
+    lightBg: '#FFFFFF',
+    darkBg: '#FAF9F8',
+    lightSidebar: '#EBF9CC',
+    darkSidebar: '#D8F399',
+    lightIcon: '#3F8421',
+    darkIcon: '#1F4210',
+    createBtn: '#4FA529',
   }, {
-    name: PresetThemes.ISLAND,        schemeType: LIGHT, bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', accent: 'rgba(183, 226, 219, 1)',
+    name: PresetThemes.WOOD,
+    schemeType: LIGHT,
+    lightBg: '#FFFFF5',
+    darkBg: '#FDFAF0',
+    lightSidebar: '#EAE3D4',
+    darkSidebar: '#DCCBA6',
+    lightIcon: '#86651A',
+    darkIcon: '#43320D',
+    createBtn: '#A77E21',
   }, {
-    name: PresetThemes.CHRISTMAS,     schemeType: LIGHT, bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', accent: '#d3c665',
-  }, {
-    name: PresetThemes.ANTARCTIC,     schemeType: LIGHT, bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', accent: '#fa9913',
-  }, {
-    name: PresetThemes.SPRING,        schemeType: LIGHT, bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', accent: '#67a856',
+    name: PresetThemes.ISLAND,
+    schemeType: LIGHT,
+    lightBg: '#FFFFFF',
+    darkBg: '#E8F7FA',
+    lightSidebar: '#F3EEE0',
+    darkSidebar: '#E8DDC0',
+    lightIcon: '#51C2D3',
+    darkIcon: '#204D54',
+    createBtn: '#51C2D3',
+  },
+  //  {
+  //   name: PresetThemes.CHRISTMAS,
+  //   schemeType: LIGHT,
+  //   lightBg: '#212836',
+  //   darkBg: '#323D52',
+  //   lightSidebar: '#2E3E27',
+  //   darkSidebar: '#455D3B',
+  //   lightIcon: '#DC7870',
+  //   darkIcon: '#E7A59F',
+  //   createBtn: '#B90606',
+  // },
+  //  {
+  //   name: PresetThemes.ANTARCTIC,
+  //   schemeType: LIGHT,
+  //   lightBg: '#FAFEFF',
+  //   darkBg: '#E5FAFF',
+  //   lightSidebar: '#EDF4FC',
+  //   darkSidebar: '#DBE9F9',
+  //   lightIcon: '#2631AF',
+  //   darkIcon: '#131857',
+  //   createBtn: '#303DDB',
+  // },
+  {
+    name: PresetThemes.SPRING,
+    schemeType: LIGHT,
+    lightBg: '#FFFFFF',
+    darkBg: '#FFFDF5',
+    lightSidebar: '#FEE7EB',
+    darkSidebar: '#F9CED7',
+    lightIcon: '#D76F7D',
+    darkIcon: '#8A423F',
+    createBtn: '#6ABA55',
   }, {
-    name: PresetThemes.KIBELA,        schemeType: LIGHT, bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', accent: '#b5cbf79c',
+    name: PresetThemes.KIBELA,
+    schemeType: LIGHT,
+    lightBg: '#FFFFFF',
+    darkBg: '#F5F5F5',
+    lightSidebar: '#FFFFFF',
+    darkSidebar: '#F5F5F5',
+    lightIcon: '#737373',
+    darkIcon: '#525252',
+    createBtn: '#3080C0',
   },
   // dark only
   {
-    name: PresetThemes.FUTURE,        schemeType: DARK, bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', accent: '#00b5b7',
+    name: PresetThemes.FUTURE,
+    schemeType: DARK,
+    lightBg: '#092627',
+    darkBg: '#1F4C4D',
+    lightSidebar: '#2a2929',
+    darkSidebar: '#27413D',
+    lightIcon: '#3F999B',
+    darkIcon: '#99E5E6',
+    createBtn: '#03A2A8',
   }, {
-    name: PresetThemes.HALLOWEEN,     schemeType: DARK, bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', accent: '#e9af2b',
+    name: PresetThemes.HALLOWEEN,
+    schemeType: DARK,
+    lightBg: '#240E3E',
+    darkBg: '#2F1155',
+    lightSidebar: '#2F1155',
+    darkSidebar: '#3B136C',
+    lightIcon: '#8C3C03',
+    darkIcon: '#DDB69B',
+    createBtn: '#AA4A04',
   }, {
-    name: PresetThemes.BLACKBOARD,    schemeType: DARK, bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', accent: '#DA8506',
+    name: PresetThemes.BLACKBOARD,
+    schemeType: DARK,
+    lightBg: '#223323',
+    darkBg: '#213E23',
+    lightSidebar: '#1B431C',
+    darkSidebar: '#29652B',
+    lightIcon: '#C89D3B',
+    darkIcon: '#E3CE9D',
+    createBtn: '#BA840A',
   },
 ]
   // fill in missing information

+ 7 - 4
packages/preset-themes/src/interfaces/growi-theme-metadata.ts

@@ -9,9 +9,12 @@ export type GrowiThemeMetadata = {
   name: string,
   manifestKey: string,
   schemeType: GrowiThemeSchemeType,
-  bg: string,
-  topbar: string,
-  sidebar: string,
-  accent: string,
+  lightBg: string,
+  darkBg: string,
+  lightSidebar: string,
+  darkSidebar: string,
+  lightIcon: string,
+  darkIcon: string,
+  createBtn: string,
   isPresetTheme?: boolean,
 };

+ 1 - 1
yarn.lock

@@ -1840,7 +1840,7 @@
   version "7.0.0-RC.0"
 
 "@growi/editor@link:packages/editor":
-  version "6.2.0-RC.0"
+  version "7.0.0-RC.0"
   dependencies:
     markdown-table "^3.0.3"
     react "^18.2.0"