Selaa lähdekoodia

Merge branch 'master' into feat/page-bulk-export

Futa Arai 2 vuotta sitten
vanhempi
sitoutus
e1c64afc69
100 muutettua tiedostoa jossa 923 lisäystä ja 3710 poistoa
  1. 16 16
      .github/workflows/ci-app.yml
  2. 15 15
      .github/workflows/ci-slackbot-proxy.yml
  3. 1 1
      .github/workflows/codeql-analysis.yml
  4. 2 2
      .github/workflows/draft-release.yml
  5. 2 2
      .github/workflows/list-unhealthy-branches.yml
  6. 5 6
      .github/workflows/release-rc-scheduled.yml
  7. 1 1
      .github/workflows/release-rc.yml
  8. 3 3
      .github/workflows/release-slackbot-proxy.yml
  9. 6 6
      .github/workflows/release.yml
  10. 2 2
      .github/workflows/reusable-app-build-image.yml
  11. 17 17
      .github/workflows/reusable-app-prod.yml
  12. 4 4
      .github/workflows/reusable-app-reg-suit.yml
  13. 29 1
      CHANGELOG.md
  14. 0 1
      apps/app/.eslintignore
  15. 0 1191
      apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx
  16. 0 126
      apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.module.scss
  17. 0 65
      apps/app/_obsolete/src/components/PageEditor/CommentMentionHelper.ts
  18. 0 344
      apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx
  19. 0 174
      apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx
  20. 0 672
      apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss
  21. 0 534
      apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss
  22. 0 22
      apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss
  23. 0 11
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  24. 1 1
      apps/app/docker/README.md
  25. 0 1
      apps/app/docker/codebuild/buildspec.yml
  26. 2 2
      apps/app/package.json
  27. 2 0
      apps/app/public/static/locales/en_US/translation.json
  28. 2 0
      apps/app/public/static/locales/ja_JP/translation.json
  29. 2 0
      apps/app/public/static/locales/zh_CN/translation.json
  30. 7 0
      apps/app/src/client/services/create-page/create-page.ts
  31. 1 0
      apps/app/src/client/services/create-page/index.ts
  32. 3 1
      apps/app/src/client/services/create-page/use-create-page-and-transit.tsx
  33. 0 8
      apps/app/src/client/services/page-operation.ts
  34. 1 0
      apps/app/src/client/services/upload-attachments/index.ts
  35. 39 0
      apps/app/src/client/services/upload-attachments/upload-attachments.ts
  36. 1 1
      apps/app/src/client/util/apiv3-client.ts
  37. 2 1
      apps/app/src/client/util/toastr.ts
  38. 4 4
      apps/app/src/components/Admin/UserManagement.tsx
  39. 9 9
      apps/app/src/components/Admin/Users/UserTable.tsx
  40. 5 2
      apps/app/src/components/Comments.tsx
  41. 17 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  42. 5 5
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  43. 2 1
      apps/app/src/components/CompleteUserRegistration.tsx
  44. 1 1
      apps/app/src/components/DescendantsPageListModal.tsx
  45. 8 0
      apps/app/src/components/ExpandOrContractButton.module.scss
  46. 9 2
      apps/app/src/components/ExpandOrContractButton.tsx
  47. 3 1
      apps/app/src/components/FontFamily/use-growi-custom-icons.tsx
  48. 1 1
      apps/app/src/components/FontFamily/use-lato.tsx
  49. 2 1
      apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx
  50. 1 1
      apps/app/src/components/FontFamily/use-source-han-code-jp.tsx
  51. 14 0
      apps/app/src/components/Layout/RawLayout.module.scss
  52. 8 3
      apps/app/src/components/Layout/RawLayout.tsx
  53. 44 0
      apps/app/src/components/LoginForm/ExternalAuthButton.tsx
  54. 9 6
      apps/app/src/components/LoginForm/LoginForm.module.scss
  55. 15 48
      apps/app/src/components/LoginForm/LoginForm.tsx
  56. 1 0
      apps/app/src/components/LoginForm/index.ts
  57. 1 1
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  58. 29 2
      apps/app/src/components/PageComment.module.scss
  59. 14 14
      apps/app/src/components/PageComment.tsx
  60. 32 23
      apps/app/src/components/PageComment/Comment.module.scss
  61. 16 20
      apps/app/src/components/PageComment/Comment.tsx
  62. 2 2
      apps/app/src/components/PageComment/CommentControl.tsx
  63. 21 20
      apps/app/src/components/PageComment/CommentEditor.module.scss
  64. 50 89
      apps/app/src/components/PageComment/CommentEditor.tsx
  65. 0 7
      apps/app/src/components/PageComment/CommentPreview.module.scss
  66. 3 1
      apps/app/src/components/PageComment/CommentPreview.tsx
  67. 0 6
      apps/app/src/components/PageComment/ReplyComments.module.scss
  68. 1 1
      apps/app/src/components/PageComment/ReplyComments.tsx
  69. 46 0
      apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss
  70. 73 0
      apps/app/src/components/PageComment/SwitchingButtonGroup.tsx
  71. 19 8
      apps/app/src/components/PageComment/_comment-inheritance.scss
  72. 1 1
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  73. 1 1
      apps/app/src/components/PageControls/LikeButtons.tsx
  74. 1 2
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  75. 3 2
      apps/app/src/components/PageControls/SubscribeButton.tsx
  76. 1 1
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  77. 3 3
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  78. 11 11
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  79. 3 3
      apps/app/src/components/PageEditor/MarkdownTableDataImportForm.tsx
  80. 31 34
      apps/app/src/components/PageEditor/PageEditor.tsx
  81. 22 8
      apps/app/src/components/PageEditor/ScrollSyncHelper.tsx
  82. 1 1
      apps/app/src/components/PageEditor/_page-editor-inheritance.scss
  83. 1 2
      apps/app/src/components/PageHeader/PageHeader.tsx
  84. 3 3
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  85. 6 5
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  86. 11 1
      apps/app/src/components/PageHeader/PageTitleHeader.module.scss
  87. 35 8
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  88. 14 12
      apps/app/src/components/PageHistory/PageRevisionTable.tsx
  89. 2 21
      apps/app/src/components/PageHistory/RevisionDiff.module.scss
  90. 48 26
      apps/app/src/components/PageHistory/RevisionDiff.tsx
  91. 0 5
      apps/app/src/components/PageStatusAlert.module.scss
  92. 22 7
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  93. 8 0
      apps/app/src/components/RevisionComparer/RevisionComparer.module.scss
  94. 26 26
      apps/app/src/components/RevisionComparer/RevisionComparer.tsx
  95. 1 1
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  96. 3 3
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  97. 6 2
      apps/app/src/components/Sidebar/Sidebar.tsx
  98. 1 1
      apps/app/src/components/Sidebar/SidebarBrandLogo.tsx
  99. 9 0
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss
  100. 18 10
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss

+ 16 - 16
.github/workflows/ci-app.yml

@@ -30,16 +30,16 @@ jobs:
         node-version: [20.x]
         node-version: [20.x]
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
           cache-dependency-path: '**/yarn.lock'
           cache-dependency-path: '**/yarn.lock'
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -49,7 +49,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -79,7 +79,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -101,9 +101,9 @@ jobs:
           - 27017/tcp
           - 27017/tcp
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
@@ -111,7 +111,7 @@ jobs:
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
         id: cache-dependencies
         id: cache-dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -121,7 +121,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -143,7 +143,7 @@ jobs:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
 
       - name: Upload coverage report as artifact
       - name: Upload coverage report as artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: Coverage Report
           name: Coverage Report
           path: |
           path: |
@@ -161,7 +161,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -183,9 +183,9 @@ jobs:
           - 27017/tcp
           - 27017/tcp
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
@@ -193,7 +193,7 @@ jobs:
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
         id: cache-dependencies
         id: cache-dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -203,7 +203,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -238,7 +238,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo

+ 15 - 15
.github/workflows/ci-slackbot-proxy.yml

@@ -32,16 +32,16 @@ jobs:
         node-version: [20.x]
         node-version: [20.x]
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -51,7 +51,7 @@ jobs:
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -81,7 +81,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -107,16 +107,16 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
           MYSQL_DATABASE: growi-slackbot-proxy
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -126,7 +126,7 @@ jobs:
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -166,7 +166,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -192,9 +192,9 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
           MYSQL_DATABASE: growi-slackbot-proxy
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -212,7 +212,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -226,7 +226,7 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -269,7 +269,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo

+ 1 - 1
.github/workflows/codeql-analysis.yml

@@ -43,7 +43,7 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
 
     # Initializes the CodeQL tools for scanning.
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
     - name: Initialize CodeQL

+ 2 - 2
.github/workflows/draft-release.yml

@@ -23,7 +23,7 @@ jobs:
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
       - name: Retrieve information from package.json
       - name: Retrieve information from package.json
         uses: myrotvorets/info-from-package-json-action@1.2.0
         uses: myrotvorets/info-from-package-json-action@1.2.0
@@ -47,7 +47,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
         with:
           fetch-depth: 0
           fetch-depth: 0
 
 

+ 2 - 2
.github/workflows/list-unhealthy-branches.yml

@@ -10,11 +10,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         fetch-depth: 0
         fetch-depth: 0
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '18'
         node-version: '18'
 
 

+ 5 - 6
.github/workflows-archived/release-rc-v7.yml → .github/workflows/release-rc-scheduled.yml

@@ -1,10 +1,9 @@
-name: Release Docker Images for RC (for dev/7.0.x)
+name: Release Docker Images for RC (for master)
 
 
 on:
 on:
-  push:
-    branches:
-      - dev/7.0.x
-
+  schedule:
+    # Weekdays at 24:00hrs (JST) Executed
+    - cron: '0 15 * * 1-5'
 
 
 concurrency:
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -21,7 +20,7 @@ jobs:
       TAGS_GHCR: ${{ steps.meta-ghcr.outputs.tags }}
       TAGS_GHCR: ${{ steps.meta-ghcr.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 1 - 1
.github/workflows/release-rc.yml

@@ -20,7 +20,7 @@ jobs:
       TAGS: ${{ steps.meta.outputs.tags }}
       TAGS: ${{ steps.meta.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 3 - 3
.github/workflows/release-slackbot-proxy.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
@@ -89,11 +89,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '18'
         node-version: '18'
         cache: 'yarn'
         cache: 'yarn'

+ 6 - 6
.github/workflows/release.yml

@@ -18,11 +18,11 @@ jobs:
       RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
       RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
         cache: 'yarn'
         cache: 'yarn'
@@ -84,7 +84,7 @@ jobs:
       TAGS: ${{ steps.meta.outputs.tags }}
       TAGS: ${{ steps.meta.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0
@@ -133,7 +133,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
@@ -158,11 +158,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
         cache: 'yarn'
         cache: 'yarn'

+ 2 - 2
.github/workflows/reusable-app-build-image.yml

@@ -33,7 +33,7 @@ jobs:
         platform: [amd64, arm64]
         platform: [amd64, arm64]
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Configure AWS Credentials
     - name: Configure AWS Credentials
       uses: aws-actions/configure-aws-credentials@v4
       uses: aws-actions/configure-aws-credentials@v4
@@ -51,5 +51,5 @@ jobs:
         CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/amazonlinux2-x86_64-standard:4.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
         CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/amazonlinux2-x86_64-standard:4.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
         CODEBUILD__environmentTypeOverride: ${{ (matrix.platform == 'amd64' && 'LINUX_CONTAINER') || 'ARM_CONTAINER' }}
         CODEBUILD__environmentTypeOverride: ${{ (matrix.platform == 'amd64' && 'LINUX_CONTAINER') || 'ARM_CONTAINER' }}
         CODEBUILD__environmentVariablesOverride: '[
         CODEBUILD__environmentVariablesOverride: '[
-          { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "docker.io/${{ inputs.image-name }}:${{ inputs.tag-temporary }}-${{ matrix.platform }}" },
+          { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "docker.io/${{ inputs.image-name }}:${{ inputs.tag-temporary }}-${{ matrix.platform }}" }
         ]'
         ]'

+ 17 - 17
.github/workflows/reusable-app-prod.yml

@@ -26,12 +26,12 @@ jobs:
       PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
       PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         # retrieve local font files
         # retrieve local font files
         lfs: true
         lfs: true
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -49,7 +49,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -63,7 +63,7 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           node_modules/.cache/turbo
           node_modules/.cache/turbo
@@ -100,15 +100,15 @@ jobs:
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
 
     - name: Upload production files as artifact
     - name: Upload production files as artifact
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
         path: ${{ steps.archive-prod-files.outputs.file }}
         path: ${{ steps.archive-prod-files.outputs.file }}
 
 
     - name: Upload report as artifact
     - name: Upload report as artifact
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
-        name: Bundle Analyzing Report
+        name: Bundle Analyzing Report (node${{ inputs.node-version }})
         path: |
         path: |
           apps/app/.next/analyze/client.html
           apps/app/.next/analyze/client.html
           apps/app/.next/analyze/server.html
           apps/app/.next/analyze/server.html
@@ -141,9 +141,9 @@ jobs:
           discovery.type: single-node
           discovery.type: single-node
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -161,7 +161,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -174,7 +174,7 @@ jobs:
         yarn --production
         yarn --production
 
 
     - name: Download production files artifact
     - name: Download production files artifact
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
 
 
@@ -229,12 +229,12 @@ jobs:
           discovery.type: single-node
           discovery.type: single-node
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Install fonts
     - name: Install fonts
       run: sudo apt install fonts-noto
       run: sudo apt install fonts-noto
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -252,7 +252,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -261,7 +261,7 @@ jobs:
           node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
           node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Cache/Restore Cypress files
     - name: Cache/Restore Cypress files
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           ~/.cache/Cypress
           ~/.cache/Cypress
@@ -276,7 +276,7 @@ jobs:
         yarn cypress install
         yarn cypress install
 
 
     - name: Download production files artifact
     - name: Download production files artifact
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
 
 
@@ -323,7 +323,7 @@ jobs:
 
 
     - name: Upload results
     - name: Upload results
       if: always()
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         name: ${{ inputs.cypress-report-artifact-name }}
         path: |
         path: |

+ 4 - 4
.github/workflows/reusable-app-reg-suit.yml

@@ -49,12 +49,12 @@ jobs:
       EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
       EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ inputs.checkout-ref }}
         ref: ${{ inputs.checkout-ref }}
         fetch-depth: 0
         fetch-depth: 0
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -72,7 +72,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -86,7 +86,7 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Download screenshots taken by cypress
     - name: Download screenshots taken by cypress
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         name: ${{ inputs.cypress-report-artifact-name }}
         path: apps/app/test/cypress
         path: apps/app/test/cypress

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,37 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.1...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.0.1](https://github.com/weseek/growi/compare/v7.0.0...v7.0.1) - 2024-04-02
+
+### 🚀 Improvement
+
+* imprv: PagePathNav and PagePathHeader styles (#8643) @yuki-takei
+* imprv: Prevent tooltip flickering (#8642) @yuki-takei
+* imprv: PersonalDropdown style (#8641) @yuki-takei
+* imprv: Support color scheme in Page History (diff2html) (#8637) @yuki-takei
+* imprv: Disable RequestedAuthnContext in SAML authentication (v7.0.x) (#8635) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Admin users badge color in /admin/users (#8655) @satof3
+* fix: Custom logo style (#8656) @satof3
+* fix: Error when creating TTL index (#8653) @miya
+* fix: Login form style is broken (#8651) @yuki-takei
+* fix: Login buttons for external auth provider does not work (#8648) @yuki-takei
+* fix: Set `d-none` when the sidebar is closed in order to prevent scrollbars from appearing (#8640) @yuki-takei
+* fix: Style for Handsontable in dark mode (#8639) @yuki-takei
+* fix: Supress PageAccessoriesModal rerendering for keeping radio button selections of PageHistory (#8638) @yuki-takei
+* fix: Uncorrect update bookmark button clicked on page control (#8608) @jam411
+* fix: Counting comments when removing the thread (#8624) @yukendev
+
+### 🧰 Maintenance
+
+* support: Add light and dark badge color (#8652) @satof3
+* support: Omit configurations for publishing to GitHub Container Registry(ghcr.io) (#8628) @yuki-takei
+
 ## [v7.0.0](https://github.com/weseek/growi/compare/v6.3.2...v7.0.0) - 2024-03-27
 ## [v7.0.0](https://github.com/weseek/growi/compare/v6.3.2...v7.0.0) - 2024-03-27
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES

+ 0 - 1
apps/app/.eslintignore

@@ -1,4 +1,3 @@
-/_obsolete/**
 /dist/**
 /dist/**
 /transpiled/**
 /transpiled/**
 /public/**
 /public/**

+ 0 - 1191
apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -1,1191 +0,0 @@
-import React, { useCallback, memo } from 'react';
-
-import { commands } from 'codemirror';
-import * as loadCssSync from 'load-css-file';
-import PropTypes from 'prop-types';
-import { Button } from 'reactstrap';
-import * as loadScript from 'simple-load-script';
-import { throttle, debounce } from 'throttle-debounce';
-import urljoin from 'url-join';
-
-import InterceptorManager from '~/services/interceptor-manager';
-import {
-  useHandsontableModal, useDrawioModal, useTemplateModal, useLinkEditModal,
-} from '~/stores/modal';
-import loggerFactory from '~/utils/logger';
-
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
-
-import AbstractEditor from './AbstractEditor';
-import CommentMentionHelper from './CommentMentionHelper';
-import EditorIcon from './EditorIcon';
-import EmojiPicker from './EmojiPicker';
-import EmojiPickerHelper from './EmojiPickerHelper';
-import GridEditModal from './GridEditModal';
-// TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-// import geu from './GridEditorUtil';
-import mdu from './MarkdownDrawioUtil';
-import markdownLinkUtil from './MarkdownLinkUtil';
-import markdownListUtil from './MarkdownListUtil';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
-import mtu from './MarkdownTableUtil';
-import pasteHelper from './PasteHelper';
-import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
-import SimpleCheatsheet from './SimpleCheatsheet';
-
-import styles from './CodeMirrorEditor.module.scss';
-
-require('codemirror/addon/hint/show-hint.css'); // Import from CodeMirrorEditor.module.scss not working
-require('codemirror/addon/display/placeholder');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/matchtags');
-require('codemirror/addon/edit/closetag');
-require('codemirror/addon/edit/continuelist');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/search/searchcursor');
-require('codemirror/addon/search/match-highlighter');
-require('codemirror/addon/selection/active-line');
-require('codemirror/addon/scroll/annotatescrollbar');
-require('codemirror/addon/scroll/scrollpastend');
-require('codemirror/addon/fold/foldcode');
-require('codemirror/addon/fold/foldgutter');
-require('codemirror/addon/fold/markdown-fold');
-require('codemirror/addon/fold/brace-fold');
-require('codemirror/addon/display/placeholder');
-require('~/client/util/codemirror/autorefresh.ext');
-require('~/client/util/codemirror/drawio-fold.ext');
-require('~/client/util/codemirror/gfm-growi.mode');
-// import modes to highlight
-require('codemirror/mode/clike/clike');
-require('codemirror/mode/css/css');
-require('codemirror/mode/django/django');
-require('codemirror/mode/erlang/erlang');
-require('codemirror/mode/gfm/gfm');
-require('codemirror/mode/go/go');
-require('codemirror/mode/javascript/javascript');
-require('codemirror/mode/jsx/jsx');
-require('codemirror/mode/mathematica/mathematica');
-require('codemirror/mode/nginx/nginx');
-require('codemirror/mode/perl/perl');
-require('codemirror/mode/php/php');
-require('codemirror/mode/python/python');
-require('codemirror/mode/r/r');
-require('codemirror/mode/ruby/ruby');
-require('codemirror/mode/rust/rust');
-require('codemirror/mode/sass/sass');
-require('codemirror/mode/shell/shell');
-require('codemirror/mode/sql/sql');
-require('codemirror/mode/stex/stex');
-require('codemirror/mode/stylus/stylus');
-require('codemirror/mode/swift/swift');
-require('codemirror/mode/toml/toml');
-require('codemirror/mode/vb/vb');
-require('codemirror/mode/vue/vue');
-require('codemirror/mode/xml/xml');
-require('codemirror/mode/yaml/yaml');
-
-
-const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
-const MARKDOWN_LINK_ACTIVATED_CLASS = 'markdown-link-activated';
-
-class CodeMirrorEditor extends AbstractEditor {
-
-  constructor(props) {
-    super(props);
-    this.logger = loggerFactory('growi:PageEditor:CodeMirrorEditor');
-
-    this.state = {
-      isGfmMode: this.props.isGfmMode,
-      isLoadingKeymap: false,
-      isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value?.length === 0,
-      isCheatsheetModalShown: false,
-      additionalClassSet: new Set(),
-      isEmojiPickerShown: false,
-      emojiSearchText: '',
-      startPosWithEmojiPickerModeTurnedOn: null,
-      isEmojiPickerMode: false,
-      isTemplateModalOpened: false,
-    };
-
-    this.cm = React.createRef();
-    this.gridEditModal = React.createRef();
-    this.linkEditModal = React.createRef();
-    this.drawioModal = React.createRef();
-
-    this.init();
-
-    this.getCodeMirror = this.getCodeMirror.bind(this);
-
-    this.getBol = this.getBol.bind(this);
-    this.getEol = this.getEol.bind(this);
-
-    this.loadTheme = this.loadTheme.bind(this);
-    this.loadKeymapMode = this.loadKeymapMode.bind(this);
-    this.setKeymapMode = this.setKeymapMode.bind(this);
-    this.handleEnterKey = this.handleEnterKey.bind(this);
-    this.handleCtrlEnterKey = this.handleCtrlEnterKey.bind(this);
-
-    this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
-    this.scrollCursorIntoViewHandlerThrottled = throttle(500, this.scrollCursorIntoViewHandler);
-    this.pasteHandler = this.pasteHandler.bind(this);
-    this.cursorHandler = this.cursorHandler.bind(this);
-    this.cursorHandlerDebounced = debounce(200, throttle(500, this.cursorHandler));
-    this.changeHandler = this.changeHandler.bind(this);
-    this.turnOnEmojiPickerMode = this.turnOnEmojiPickerMode.bind(this);
-    this.turnOffEmojiPickerMode = this.turnOffEmojiPickerMode.bind(this);
-    this.windowClickHandler = this.windowClickHandler.bind(this);
-    this.keyDownHandler = this.keyDownHandler.bind(this);
-    this.keyDownHandlerForEmojiPicker = this.keyDownHandlerForEmojiPicker.bind(this);
-    this.keyDownHandlerForEmojiPickerThrottled = throttle(400, this.keyDownHandlerForEmojiPicker);
-    this.showEmojiPicker = this.showEmojiPicker.bind(this);
-    this.keyPressHandlerForEmojiPicker = this.keyPressHandlerForEmojiPicker.bind(this);
-    this.keyPressHandlerForEmojiPickerThrottled = debounce(50, throttle(200, this.keyPressHandlerForEmojiPicker));
-    this.keyPressHandler = this.keyPressHandler.bind(this);
-
-    this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
-
-    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
-    this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
-
-    this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
-    // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-    // this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
-
-    this.foldDrawioSection = this.foldDrawioSection.bind(this);
-    this.clickDrawioIconHandler = this.clickDrawioIconHandler.bind(this);
-    this.clickTableIconHandler = this.clickTableIconHandler.bind(this);
-
-    this.showTemplateModal = this.showTemplateModal.bind(this);
-    this.showLinkEditModal = this.showLinkEditModal.bind(this);
-
-  }
-
-  init() {
-    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
-    this.cmNoCdnScriptRoot = '/static/js/cdn';
-    this.cmNoCdnStyleRoot = '/static/styles/cdn';
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new PreventMarkdownListInterceptor(),
-      new MarkdownTableInterceptor(),
-    ]);
-
-    this.loadedThemeSet = new Set(['eclipse', 'elegant']); // themes imported in _vendor.scss
-    this.loadedKeymapSet = new Set();
-  }
-
-  componentDidMount() {
-    // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
-    this.getCodeMirror().codeMirrorEditor = this;
-
-    // mark clean
-    this.getCodeMirror().getDoc().markClean();
-
-    // fold drawio section
-    this.foldDrawioSection();
-
-    // initialize commentMentionHelper if comment editor is opened
-    if (this.props.isComment) {
-      this.commentMentionHelper = new CommentMentionHelper(this.getCodeMirror());
-    }
-    this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror());
-
-    // HACKME: Find a better way to handle onClick for Editor
-    document.addEventListener('click', this.windowClickHandler);
-  }
-
-  componentWillUnmount() {
-    // HACKME: Find a better way to handle onClick for Editor
-    document.removeEventListener('click', this.windowClickHandler);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.initializeEditorSettings(nextProps.editorSettings);
-
-    // fold drawio section
-    this.foldDrawioSection();
-  }
-
-  initializeEditorSettings(editorSettings) {
-    if (editorSettings == null) {
-      return;
-    }
-
-    // load theme
-    const theme = editorSettings.theme;
-    if (theme != null) {
-      this.loadTheme(theme);
-    }
-
-    // set keymap
-    const keymapMode = editorSettings.keymapMode;
-    if (keymapMode != null) {
-      this.setKeymapMode(keymapMode);
-    }
-  }
-
-  getCodeMirror() {
-    return this.cm.current?.editor;
-  }
-
-  /**
-   * @inheritDoc
-   */
-  forceToFocus() {
-    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
-    const intervalId = setInterval(() => {
-      const editor = this.getCodeMirror();
-      editor.focus();
-      if (editor.hasFocus()) {
-        clearInterval(intervalId);
-        // refresh
-        editor.refresh();
-      }
-    }, 100);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setValue(newValue) {
-    this.getCodeMirror().getDoc().setValue(newValue);
-
-    // mark clean
-    this.getCodeMirror().getDoc().markClean();
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setGfmMode(bool) {
-    // update state
-    this.setState({
-      isGfmMode: bool,
-    });
-
-    this.updateCheatsheetStates(bool, null);
-
-    // update CodeMirror option
-    const mode = bool ? 'gfm' : undefined;
-    this.getCodeMirror().setOption('mode', mode);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setCaretLine(line) {
-    if (Number.isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    const linePosition = Math.max(0, line - 1);
-
-    editor.setCursor({ line: linePosition }); // leave 'ch' field as null/undefined to indicate the end of line
-
-    setTimeout(() => {
-      this.setScrollTopByLine(linePosition);
-    }, 100);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setScrollTopByLine(line) {
-    if (Number.isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    // get top position of the line
-    const top = editor.charCoords({ line: line - 1, ch: 0 }, 'local').top;
-    editor.scrollTo(null, top);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBol(), curPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrToEol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(curPos, this.getEol());
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBolToSelectedUpperPos() {
-    const editor = this.getCodeMirror();
-    const pos = this.selectUpperPos(editor.getCursor('from'), editor.getCursor('to'));
-    return editor.getDoc().getRange(this.getBol(), pos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceBolToCurrentPos(text) {
-    const editor = this.getCodeMirror();
-    const pos = this.selectLowerPos(editor.getCursor('from'), editor.getCursor('to'));
-    editor.getDoc().replaceRange(text, this.getBol(), pos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceLine(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceRange(text, this.getBol(), this.getEol());
-  }
-
-  /**
-   * @inheritDoc
-   */
-  insertText(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceSelection(text);
-  }
-
-  /**
-   * return the postion of the BOL(beginning of line)
-   */
-  getBol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return { line: curPos.line, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOL(end of line)
-   */
-  getEol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    const lineLength = editor.getDoc().getLine(curPos.line).length;
-    return { line: curPos.line, ch: lineLength };
-  }
-
-  /**
-   * select the upper position of pos1 and pos2
-   * @param {{line: number, ch: number}} pos1
-   * @param {{line: number, ch: number}} pos2
-   */
-  selectUpperPos(pos1, pos2) {
-    // if both is in same line
-    if (pos1.line === pos2.line) {
-      return (pos1.ch < pos2.ch) ? pos1 : pos2;
-    }
-    return (pos1.line < pos2.line) ? pos1 : pos2;
-  }
-
-  /**
-   * select the lower position of pos1 and pos2
-   * @param {{line: number, ch: number}} pos1
-   * @param {{line: number, ch: number}} pos2
-   */
-  selectLowerPos(pos1, pos2) {
-    // if both is in same line
-    if (pos1.line === pos2.line) {
-      return (pos1.ch < pos2.ch) ? pos2 : pos1;
-    }
-    return (pos1.line < pos2.line) ? pos2 : pos1;
-  }
-
-  loadCss(source) {
-    return new Promise((resolve) => {
-      loadCssSync(source);
-      resolve();
-    });
-  }
-
-  /**
-   * load Theme
-   * @see https://codemirror.net/doc/manual.html#config
-   *
-   * @param {string} theme
-   */
-  loadTheme(theme) {
-    if (!this.loadedThemeSet.has(theme)) {
-      const url = this.props.noCdn
-        ? urljoin(this.cmNoCdnStyleRoot, `codemirror-theme-${theme}.css`)
-        : urljoin(this.cmCdnRoot, `theme/${theme}.min.css`);
-
-      this.loadCss(url);
-
-      // update Set
-      this.loadedThemeSet.add(theme);
-    }
-  }
-
-  /**
-   * load assets for Key Maps
-   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  loadKeymapMode(keymapMode) {
-    const loadCss = this.loadCss;
-    const scriptList = [];
-    const cssList = [];
-
-    // add dependencies
-    if (this.loadedKeymapSet.size === 0) {
-      const dialogScriptUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnScriptRoot, 'codemirror-dialog.js')
-        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js');
-      const dialogStyleUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnStyleRoot, 'codemirror-dialog.css')
-        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css');
-
-      scriptList.push(loadScript(dialogScriptUrl));
-      cssList.push(loadCss(dialogStyleUrl));
-    }
-    // load keymap
-    if (!this.loadedKeymapSet.has(keymapMode)) {
-      const keymapScriptUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnScriptRoot, `codemirror-keymap-${keymapMode}.js`)
-        : urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`);
-      scriptList.push(loadScript(keymapScriptUrl));
-      // update Set
-      this.loadedKeymapSet.add(keymapMode);
-    }
-
-    // set loading state
-    this.setState({ isLoadingKeymap: true });
-
-    return Promise.all(scriptList.concat(cssList))
-      .then(() => {
-        this.setState({ isLoadingKeymap: false });
-      });
-  }
-
-  /**
-   * set Key Maps
-   * @see https://codemirror.net/doc/manual.html#keymaps
-   *
-   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  setKeymapMode(keymapMode) {
-    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
-      // reset
-      this.getCodeMirror().setOption('keyMap', 'default');
-      return;
-    }
-
-    this.loadKeymapMode(keymapMode)
-      .then(() => {
-        let errorCount = 0;
-        const timer = setInterval(() => {
-          if (errorCount > 10) { // cancel over 3000ms
-            this.logger.error(`Timeout to load keyMap '${keymapMode}'`);
-            clearInterval(timer);
-          }
-
-          try {
-            this.getCodeMirror().setOption('keyMap', keymapMode);
-            clearInterval(timer);
-          }
-          catch (e) {
-            this.logger.info(`keyMap '${keymapMode}' has not been initialized. retry..`);
-
-            // continue if error occured
-            errorCount++;
-          }
-        }, 300);
-      });
-  }
-
-  /**
-   * handle ENTER key
-   */
-  handleEnterKey() {
-    if (!this.state.isGfmMode) {
-      commands.newlineAndIndent(this.getCodeMirror());
-      return;
-    }
-
-    const context = {
-      handlers: [], // list of handlers which process enter key
-      editor: this,
-      autoFormatMarkdownTable: this.props.editorSettings.autoFormatMarkdownTable,
-    };
-
-    const interceptorManager = this.interceptorManager;
-    interceptorManager.process('preHandleEnter', context)
-      .then(() => {
-        if (context.handlers.length === 0) {
-          markdownListUtil.newlineAndIndentContinueMarkdownList(this);
-        }
-      });
-  }
-
-  /**
-   * handle Ctrl+ENTER key
-   */
-  handleCtrlEnterKey() {
-    if (this.props.onCtrlEnter != null) {
-      this.props.onCtrlEnter();
-    }
-  }
-
-  scrollCursorIntoViewHandler(editor, event) {
-    if (this.props.onScrollCursorIntoView != null) {
-      const line = editor.getCursor().line;
-      this.props.onScrollCursorIntoView(line);
-    }
-  }
-
-  cursorHandler(editor, event) {
-    const { additionalClassSet } = this.state;
-    const hasCustomClass = additionalClassSet.has(MARKDOWN_TABLE_ACTIVATED_CLASS);
-    const hasLinkClass = additionalClassSet.has(MARKDOWN_LINK_ACTIVATED_CLASS);
-
-    const isInTable = mtu.isInTable(editor);
-    const isInLink = markdownLinkUtil.isInLink(editor);
-
-    if (!hasCustomClass && isInTable) {
-      additionalClassSet.add(MARKDOWN_TABLE_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (hasCustomClass && !isInTable) {
-      additionalClassSet.delete(MARKDOWN_TABLE_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (!hasLinkClass && isInLink) {
-      additionalClassSet.add(MARKDOWN_LINK_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (hasLinkClass && !isInLink) {
-      additionalClassSet.delete(MARKDOWN_LINK_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-  }
-
-  changeHandler(editor, data, value) {
-    if (this.props.onChange != null) {
-      const isClean = data.origin == null || editor.isClean() || value === this.props.value;
-      this.props.onChange(value, isClean);
-    }
-
-    this.updateCheatsheetStates(null, value);
-
-    // Show username hint on comment editor
-    if (this.props.isComment) {
-      this.commentMentionHelper.showUsernameHint();
-    }
-
-  }
-
-  turnOnEmojiPickerMode(pos) {
-    this.setState({
-      isEmojiPickerMode: true,
-      startPosWithEmojiPickerModeTurnedOn: pos,
-    });
-  }
-
-  turnOffEmojiPickerMode() {
-    this.setState({
-      isEmojiPickerMode: false,
-    });
-  }
-
-  showEmojiPicker(initialSearchingText) {
-    // show emoji picker with a stored word
-    this.setState({
-      isEmojiPickerShown: true,
-      emojiSearchText: initialSearchingText ?? '',
-    });
-
-    const resetStartPos = initialSearchingText == null;
-    if (resetStartPos) {
-      this.setState({ startPosWithEmojiPickerModeTurnedOn: null });
-    }
-
-    this.turnOffEmojiPickerMode();
-  }
-
-  keyPressHandlerForEmojiPicker(editor, event) {
-    const char = event.key;
-    const isEmojiPickerMode = this.state.isEmojiPickerMode;
-
-    // evaluate whether emoji picker mode to be turned on
-    if (!isEmojiPickerMode) {
-      const startPos = this.emojiPickerHelper.shouldModeTurnOn(char);
-      if (startPos == null) {
-        return;
-      }
-
-      this.turnOnEmojiPickerMode(startPos);
-      return;
-    }
-
-    // evaluate whether EmojiPicker to be opened
-    const startPos = this.state.startPosWithEmojiPickerModeTurnedOn;
-    if (this.emojiPickerHelper.shouldOpen(startPos)) {
-      const initialSearchingText = this.emojiPickerHelper.getInitialSearchingText(startPos);
-      this.showEmojiPicker(initialSearchingText);
-      return;
-    }
-
-    this.turnOffEmojiPickerMode();
-  }
-
-  keyPressHandler(editor, event) {
-    this.keyPressHandlerForEmojiPickerThrottled(editor, event);
-  }
-
-  keyDownHandlerForEmojiPicker(editor, event) {
-    const key = event.key;
-
-    if (!this.state.isEmojiPickerMode) {
-      return;
-    }
-
-    if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'BackSpace'].includes(key)) {
-      this.turnOffEmojiPickerMode();
-    }
-  }
-
-  keyDownHandler(editor, event) {
-    this.keyDownHandlerForEmojiPickerThrottled(editor, event);
-  }
-
-  windowClickHandler() {
-    this.turnOffEmojiPickerMode();
-  }
-
-  /**
-   * CodeMirror paste event handler
-   * see: https://codemirror.net/doc/manual.html#events
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   */
-  pasteHandler(editor, event) {
-    const types = event.clipboardData.types;
-
-    // files
-    if (types.includes('Files')) {
-      event.preventDefault();
-      this.dispatchPasteFiles(event);
-    }
-    // text
-    else if (types.includes('text/plain')) {
-      pasteHelper.pasteText(this, event);
-    }
-
-  }
-
-  /**
-   * update states which related to cheatsheet
-   * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
-   * @param {string} valueTmp (get value from codemirror if null is set)
-   */
-  updateCheatsheetStates(isGfmModeTmp, valueTmp) {
-    const isGfmMode = isGfmModeTmp || this.state.isGfmMode;
-    const value = valueTmp || this.getCodeMirror().getDoc().getValue();
-
-    // update isSimpleCheatsheetShown
-    const isSimpleCheatsheetShown = isGfmMode && value.length === 0;
-    this.setState({ isSimpleCheatsheetShown });
-  }
-
-  markdownHelpButtonClickedHandler() {
-    if (this.props.onMarkdownHelpButtonClicked != null) {
-      this.props.onMarkdownHelpButtonClicked();
-    }
-  }
-
-  renderLoadingKeymapOverlay() {
-    // centering
-    const style = {
-      top: 0,
-      right: 0,
-      bottom: 0,
-      left: 0,
-    };
-
-    return this.state.isLoadingKeymap
-      ? (
-        <div className="overlay overlay-loading-keymap">
-          <span style={style} className="overlay-content">
-            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
-          </span>
-        </div>
-      )
-      : '';
-  }
-
-  renderCheatsheetModalButton() {
-    return (
-      <button type="button" className="btn-link gfm-cheatsheet-modal-link small" onClick={() => { this.markdownHelpButtonClickedHandler() }}>
-        <span className="material-symbols-outlined">help</span> Markdown
-      </button>
-    );
-  }
-
-  renderCheatsheetOverlay() {
-    const cheatsheetModalButton = this.renderCheatsheetModalButton();
-
-    return (
-      <div className="overlay overlay-gfm-cheatsheet mt-1 p-3">
-        { this.state.isSimpleCheatsheetShown
-          ? (
-            <div className="text-end">
-              {cheatsheetModalButton}
-              <div className="mb-2 d-none d-md-block">
-                <SimpleCheatsheet />
-              </div>
-            </div>
-          )
-          : (
-            <div className="me-4 mb-2">
-              {cheatsheetModalButton}
-            </div>
-          )
-        }
-      </div>
-    );
-  }
-
-  renderEmojiPicker() {
-    const { emojiSearchText } = this.state;
-    return this.state.isEmojiPickerShown
-      ? (
-        <div className="text-start">
-          <div className="mb-2 d-none d-md-block">
-            <EmojiPicker
-              onClose={() => this.setState({ isEmojiPickerShown: false })}
-              onSelected={emoji => this.emojiPickerHelper.addEmoji(emoji, this.state.startPosWithEmojiPickerModeTurnedOn)}
-              emojiSearchText={emojiSearchText}
-              emojiPickerHelper={this.emojiPickerHelper}
-              isOpen={this.state.isEmojiPickerShown}
-            />
-          </div>
-        </div>
-      )
-      : '';
-  }
-
-  /**
-   * return a function to replace a selected range with prefix + selection + suffix
-   *
-   * The cursor after replacing is inserted between the selection and the suffix.
-   */
-  createReplaceSelectionHandler(prefix, suffix) {
-    return () => {
-      const cm = this.getCodeMirror();
-      const selection = cm.getDoc().getSelection();
-      const curStartPos = cm.getCursor('from');
-      const curEndPos = cm.getCursor('to');
-
-      const curPosAfterReplacing = {};
-      curPosAfterReplacing.line = curEndPos.line;
-      if (curStartPos.line === curEndPos.line) {
-        curPosAfterReplacing.ch = curEndPos.ch + prefix.length;
-      }
-      else {
-        curPosAfterReplacing.ch = curEndPos.ch;
-      }
-
-      cm.getDoc().replaceSelection(prefix + selection + suffix);
-      cm.setCursor(curPosAfterReplacing);
-      cm.focus();
-    };
-  }
-
-  /**
-   * return a function to add prefix to selected each lines
-   *
-   * The cursor after editing is inserted between the end of the selection.
-   */
-  createAddPrefixToEachLinesHandler(prefix) {
-    return () => {
-      const cm = this.getCodeMirror();
-      const startLineNum = cm.getCursor('from').line;
-      const endLineNum = cm.getCursor('to').line;
-
-      const lines = [];
-      for (let i = startLineNum; i <= endLineNum; i++) {
-        lines.push(prefix + cm.getDoc().getLine(i));
-      }
-      const replacement = `${lines.join('\n')}\n`;
-      cm.getDoc().replaceRange(replacement, { line: startLineNum, ch: 0 }, { line: endLineNum + 1, ch: 0 });
-
-      cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length);
-      cm.focus();
-    };
-  }
-
-  /**
-   * make a selected line a header
-   *
-   * The cursor after editing is inserted between the end of the line.
-   */
-  makeHeaderHandler() {
-    const cm = this.getCodeMirror();
-    const lineNum = cm.getCursor('from').line;
-    const line = cm.getDoc().getLine(lineNum);
-    let prefix = '#';
-    if (!line.startsWith('#')) {
-      prefix += ' ';
-    }
-    cm.getDoc().replaceRange(prefix, { line: lineNum, ch: 0 }, { line: lineNum, ch: 0 });
-    cm.focus();
-  }
-
-  // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-  // showGridEditorHandler() {
-  //   this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
-  // }
-
-  showTemplateModal() {
-    const onSubmit = templateText => this.insertText(templateText);
-    this.props.onClickTemplateBtn({ onSubmit });
-  }
-
-  showLinkEditModal() {
-    const onSubmit = (linkText) => {
-      return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText);
-    };
-
-    const defaultMarkdownLink = markdownLinkUtil.getMarkdownLink(this.getCodeMirror());
-
-    this.props.onClickLinkEditBtn(defaultMarkdownLink, onSubmit);
-  }
-
-  // fold draw.io section (``` drawio ~ ```)
-  foldDrawioSection() {
-    const editor = this.getCodeMirror();
-    const lineNumbers = mdu.findAllDrawioSection(editor);
-    lineNumbers.forEach((lineNumber) => {
-      editor.foldCode({ line: lineNumber, ch: 0 }, { scanUp: false }, 'fold');
-    });
-  }
-
-  clickDrawioIconHandler() {
-    const drawioMxFile = mdu.getMarkdownDrawioMxfile(this.getCodeMirror());
-
-    this.props.onClickDrawioBtn(
-      drawioMxFile,
-      // onSave
-      (drawioMxFile) => {
-        mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioMxFile);
-        // Fold the section after the drawio section (```drawio) has been updated.
-        this.foldDrawioSection();
-      },
-    );
-  }
-
-  clickTableIconHandler() {
-    const markdownTable = mtu.getMarkdownTable(this.getCodeMirror());
-
-    this.props.onClickTableBtn(
-      markdownTable,
-      this.getCodeMirror(),
-      this.props.editorSettings.autoFormatMarkdownTable,
-    );
-  }
-
-  getNavbarItems() {
-    return [
-      <Button
-        key="nav-item-bold"
-        color={null}
-        size="sm"
-        title="Bold"
-        onClick={this.createReplaceSelectionHandler('**', '**')}
-      >
-        <EditorIcon icon="Bold" />
-      </Button>,
-      <Button
-        key="nav-item-italic"
-        color={null}
-        size="sm"
-        title="Italic"
-        onClick={this.createReplaceSelectionHandler('*', '*')}
-      >
-        <EditorIcon icon="Italic" />
-      </Button>,
-      <Button
-        key="nav-item-strikethrough"
-        color={null}
-        size="sm"
-        title="Strikethrough"
-        onClick={this.createReplaceSelectionHandler('~~', '~~')}
-      >
-        <EditorIcon icon="Strikethrough" />
-      </Button>,
-      <Button
-        key="nav-item-header"
-        color={null}
-        size="sm"
-        title="Heading"
-        onClick={this.makeHeaderHandler}
-      >
-        <EditorIcon icon="Heading" />
-      </Button>,
-      <Button
-        key="nav-item-code"
-        color={null}
-        size="sm"
-        title="Inline Code"
-        onClick={this.createReplaceSelectionHandler('`', '`')}
-      >
-        <EditorIcon icon="InlineCode" />
-      </Button>,
-      <Button
-        key="nav-item-quote"
-        color={null}
-        size="sm"
-        title="Quote"
-        onClick={this.createAddPrefixToEachLinesHandler('> ')}
-      >
-        <EditorIcon icon="Quote" />
-      </Button>,
-      <Button
-        key="nav-item-ul"
-        color={null}
-        size="sm"
-        title="List"
-        onClick={this.createAddPrefixToEachLinesHandler('- ')}
-      >
-        <EditorIcon icon="List" />
-      </Button>,
-      <Button
-        key="nav-item-ol"
-        color={null}
-        size="sm"
-        title="Numbered List"
-        onClick={this.createAddPrefixToEachLinesHandler('1. ')}
-      >
-        <EditorIcon icon="NumberedList" />
-      </Button>,
-      <Button
-        key="nav-item-checkbox"
-        color={null}
-        size="sm"
-        title="Check List"
-        onClick={this.createAddPrefixToEachLinesHandler('- [ ] ')}
-      >
-        <EditorIcon icon="CheckList" />
-      </Button>,
-      <Button
-        key="nav-item-attachment"
-        color={null}
-        size="sm"
-        title="Attachment"
-        onClick={this.props.onAddAttachmentButtonClicked}
-      >
-        <EditorIcon icon="Attachment" />
-      </Button>,
-      <Button
-        key="nav-item-link"
-        color={null}
-        size="sm"
-        title="Link"
-        onClick={this.showLinkEditModal}
-      >
-        <EditorIcon icon="Link" />
-      </Button>,
-      <Button
-        key="nav-item-image"
-        color={null}
-        size="sm"
-        title="Image"
-        onClick={this.createReplaceSelectionHandler('![', ']()')}
-      >
-        <EditorIcon icon="Image" />
-      </Button>,
-      // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-      // <Button
-      //   key="nav-item-grid"
-      //   color={null}
-      //   size="sm"
-      //   title="Grid"
-      //   onClick={this.showGridEditorHandler}
-      // >
-      //   <EditorIcon icon="Grid" />
-      // </Button>,
-      <Button
-        key="nav-item-table"
-        color={null}
-        size="sm"
-        title="Table"
-        onClick={this.clickTableIconHandler}
-      >
-        <EditorIcon icon="Table" />
-      </Button>,
-      <Button
-        key="nav-item-drawio"
-        color={null}
-        bssize="small"
-        title="draw.io"
-        onClick={this.clickDrawioIconHandler}
-      >
-        <EditorIcon icon="Drawio" />
-      </Button>,
-      <Button
-        key="nav-item-emoji"
-        color={null}
-        bssize="small"
-        title="Emoji"
-        onClick={() => this.showEmojiPicker()}
-      >
-        <EditorIcon icon="Emoji" />
-      </Button>,
-      <Button
-        key="nav-item-template"
-        color={null}
-        bssize="small"
-        title="Template"
-        onClick={() => this.showTemplateModal()}
-      >
-        <EditorIcon icon="Template" />
-      </Button>,
-    ];
-  }
-
-
-  render() {
-    const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
-    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
-
-    const gutters = [];
-    if (this.props.lineNumbers != null) {
-      gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter');
-    }
-
-    return (
-      <div className={`grw-codemirror-editor ${styles['grw-codemirror-editor']}`}>
-
-        <UncontrolledCodeMirror
-          ref={this.cm}
-          className={additionalClasses}
-          placeholder="search"
-          value={this.props.value}
-          options={{
-            indentUnit: this.props.indentSize,
-            theme: this.props.editorSettings.theme ?? 'elegant',
-            styleActiveLine: this.props.editorSettings.styleActiveLine,
-            lineWrapping: true,
-            scrollPastEnd: true,
-            autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-            autoCloseTags: true,
-            placeholder,
-            matchBrackets: true,
-            emoji: true,
-            matchTags: { bothTags: true },
-            // folding
-            foldGutter: this.props.lineNumbers,
-            gutters,
-            // match-highlighter, matchesonscrollbar, annotatescrollbar options
-            highlightSelectionMatches: { annotateScrollbar: true },
-            // continuelist, indentlist
-            extraKeys: {
-              Enter: this.handleEnterKey,
-              'Ctrl-Enter': this.handleCtrlEnterKey,
-              'Cmd-Enter': this.handleCtrlEnterKey,
-              Tab: 'indentMore',
-              'Shift-Tab': 'indentLess',
-              'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
-            },
-          }}
-          onCursor={this.cursorHandlerDebounced}
-          onScroll={(editor, data) => {
-            if (this.props.onScroll != null) {
-            // add line data
-              const line = editor.lineAtHeight(data.top, 'local');
-              data.line = line;
-              this.props.onScroll(data);
-            }
-          }}
-          onChange={this.changeHandler}
-          onDragEnter={(editor, event) => {
-            if (this.props.onDragEnter != null) {
-              this.props.onDragEnter(event);
-            }
-          }}
-          onKeyPress={this.keyPressHandler}
-          onKeyDown={this.keyDownHandler}
-          onPasteFiles={this.pasteHandler}
-          onScrollCursorIntoView={this.scrollCursorIntoViewHandlerThrottled}
-        />
-
-        { this.renderLoadingKeymapOverlay() }
-
-        { this.renderCheatsheetOverlay() }
-        { this.renderEmojiPicker() }
-
-        {/*
-        // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-        <GridEditModal
-          ref={this.gridEditModal}
-          onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
-        />
-         */}
-      </div>
-    );
-  }
-
-}
-
-CodeMirrorEditor.propTypes = Object.assign({
-  lineNumbers: PropTypes.bool,
-  editorSettings: PropTypes.object.isRequired,
-  onMarkdownHelpButtonClicked: PropTypes.func,
-  onAddAttachmentButtonClicked: PropTypes.func,
-}, AbstractEditor.propTypes);
-
-CodeMirrorEditor.defaultProps = {
-  lineNumbers: true,
-};
-
-const CodeMirrorEditorMemoized = memo(CodeMirrorEditor);
-
-
-const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
-  const { open: openDrawioModal } = useDrawioModal();
-  const { open: openHandsontableModal } = useHandsontableModal();
-  const { open: openTemplateModal } = useTemplateModal();
-  const { open: openLinkEditModal } = useLinkEditModal();
-
-  const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
-    openDrawioModal(drawioMxFile, onSave);
-  }, [openDrawioModal]);
-
-  const openTableModalHandler = useCallback((markdownTable, editor, autoFormatMarkdownTable) => {
-    openHandsontableModal(markdownTable, editor, autoFormatMarkdownTable);
-  }, [openHandsontableModal]);
-
-  const openTemplateModalHandler = useCallback((onSubmit) => {
-    openTemplateModal(onSubmit);
-  }, [openTemplateModal]);
-
-  const openLinkEditModalHandler = useCallback((defaultMarkdownLink, onSubmit) => {
-    openLinkEditModal(defaultMarkdownLink, onSubmit);
-  }, [openLinkEditModal]);
-
-  return (
-    <CodeMirrorEditorMemoized
-      ref={ref}
-      onClickDrawioBtn={openDrawioModalHandler}
-      onClickTableBtn={openTableModalHandler}
-      onClickTemplateBtn={openTemplateModalHandler}
-      onClickLinkEditBtn={openLinkEditModalHandler}
-      {...props}
-    />
-  );
-});
-
-CodeMirrorEditorFc.displayName = 'CodeMirrorEditorFc';
-
-export default memo(CodeMirrorEditorFc);

+ 0 - 126
apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -1,126 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-.grw-codemirror-editor :global {
-  @import '~codemirror/lib/codemirror';
-
-  // addons
-  @import '~codemirror/addon/fold/foldgutter';
-  @import '~codemirror/addon/lint/lint';
-
-  // themes
-  @import '~codemirror/theme/elegant';
-  @import '~codemirror/theme/eclipse';
-
-  .CodeMirror {
-    font-family: var(--font-family-monospace);
-    font-size: 15px;
-
-    pre.CodeMirror-line.grw-cm-header-line {
-      padding-top: 0.16em;
-      padding-bottom: 0.08em;
-      font-family: var(--font-family-monospace);
-
-      // '#'
-      .cm-formatting-header {
-        font-style: italic;
-        font-weight: bold;
-        opacity: 0.5;
-      }
-
-      .cm-header-1 {
-        font-size: 1.9em;
-      }
-      .cm-header-2 {
-        font-size: 1.6em;
-      }
-      .cm-header-3 {
-        font-size: 1.4em;
-      }
-      .cm-header-4 {
-        font-size: 1.35em;
-      }
-      .cm-header-5 {
-        font-size: 1.25em;
-      }
-      .cm-header-6 {
-        font-size: 1.2em;
-      }
-    }
-
-    .cm-matchhighlight {
-      color: bs.$gray-900 !important;
-      background-color: cyan;
-    }
-
-    .CodeMirror-selection-highlight-scrollbar {
-      background-color: darkcyan;
-    }
-
-    // overwrite .CodeMirror-placeholder
-    pre.CodeMirror-line-like.CodeMirror-placeholder {
-      color: bs.$text-muted;
-    }
-
-    // overwrite .CodeMirror-scroll
-    .CodeMirror-scroll {
-      box-sizing: border-box;
-    }
-  }
-
-  // patch to fix https://github.com/codemirror/CodeMirror/issues/4089
-  // see also https://github.com/codemirror/CodeMirror/commit/51a1e7da60a99e019f026a118dc7c98c2b1f9d62
-  .CodeMirror-wrap > div > textarea {
-    font-size: #{bs.$line-height-base}rem;
-  }
-
-  // overwrite .CodeMirror-hints
-  .CodeMirror-hints {
-    max-height: 30em !important;
-
-    // active line
-    .CodeMirror-hint-active.crowi-emoji-autocomplete {
-      .img-container {
-        padding-top: 0.3em;
-        padding-bottom: 0.3em;
-        font-size: 15px; // adjust to .wiki
-      }
-    }
-  }
-
-  // cheat sheat
-  .overlay.overlay-gfm-cheatsheet {
-    align-items: flex-end;
-    justify-content: flex-end;
-
-    pointer-events: none;
-
-    .card.gfm-cheatsheet {
-      box-shadow: unset;
-      opacity: 0.6;
-      .card-body {
-        min-width: 30em;
-        padding-bottom: 0;
-        font-family: var(--font-family-monospace);
-        color: bs.$text-muted;
-      }
-      ul > li {
-        list-style: none;
-      }
-    }
-
-    .gfm-cheatsheet-modal-link {
-      color: bs.$text-muted;
-      pointer-events: all;
-      cursor: pointer;
-      background-color: transparent;
-      border: none;
-
-      opacity: 0.6;
-
-      &:hover,
-      &:focus {
-        opacity: 1;
-      }
-    }
-  }
-}

+ 0 - 65
apps/app/_obsolete/src/components/PageEditor/CommentMentionHelper.ts

@@ -1,65 +0,0 @@
-import { Editor } from 'codemirror';
-import { i18n } from 'next-i18next';
-import { debounce } from 'throttle-debounce';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-type UsersListForHints = {
-  text: string
-  displayText: string
-}
-export default class CommentMentionHelper {
-
-  editor: Editor;
-
-  constructor(editor: Editor) {
-    this.editor = editor;
-  }
-
-  getUsenameHint = (): void => {
-    // Get word that contains `@` character at the begining
-    const currentPos = this.editor.getCursor();
-    const wordStart = this.editor.findWordAt(currentPos).anchor.ch - 1;
-    const wordEnd = this.editor.findWordAt(currentPos).head.ch;
-
-    const searchFrom = { line: currentPos.line, ch: wordStart };
-    const searchTo = { line: currentPos.line, ch: wordEnd };
-
-    const searchMention = this.editor.getRange(searchFrom, searchTo);
-    const isMentioning = searchMention.charAt(0) === '@';
-
-    // Return nothing if not mentioning
-    if (!isMentioning) {
-      return;
-    }
-
-    // Get username after `@` character and search username
-    const mention = searchMention.slice(1);
-    this.editor.showHint({
-      completeSingle: false,
-      hint: async() => {
-        if (mention.length > 0) {
-          const users = await this.getUsersList(mention);
-          return {
-            // Returns default value if i18n is null because it cannot do early return.
-            list: users.length > 0 ? users : [{ text: '', displayText: i18n != null ? i18n.t('page_comment.no_user_found') : 'No user found' }],
-            from: searchFrom,
-            to: searchTo,
-          };
-        }
-      },
-    });
-  };
-
-  getUsersList = async(q: string): Promise<UsersListForHints[]> => {
-    const limit = 20;
-    const { data } = await apiv3Get('/users/usernames', { q, limit });
-    return data.activeUser.usernames.map((username: string) => ({
-      text: `@${username} `,
-      displayText: username,
-    }));
-  };
-
-  showUsernameHint = debounce(800, () => this.getUsenameHint());
-
-}

+ 0 - 344
apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx

@@ -1,344 +0,0 @@
-import React, {
-  useState, useEffect, useRef, useMemo, useCallback,
-} from 'react';
-
-import type { IRevisionOnConflict } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
-import CodeMirror from 'codemirror/lib/codemirror';
-import { format, parseISO } from 'date-fns';
-import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentPathname, useCurrentUser } from '~/stores/context';
-import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
-import {
-  useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
-} from '~/stores/remote-latest-page';
-
-import ExpandOrContractButton from '../ExpandOrContractButton';
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
-
-require('codemirror/lib/codemirror.css');
-require('codemirror/addon/merge/merge');
-require('codemirror/addon/merge/merge.css');
-const DMP = require('diff_match_patch');
-
-Object.keys(DMP).forEach((key) => { window[key] = DMP[key] });
-
-type ConflictDiffModalProps = {
-  isOpen?: boolean;
-  onClose?: (() => void);
-  markdownOnEdit: string;
-  optionsToSave: OptionsToSave | undefined;
-  afterResolvedHandler: () => void,
-};
-
-type ConflictDiffModalCoreProps = {
-  isOpen?: boolean;
-  onClose?: (() => void);
-  optionsToSave: OptionsToSave | undefined;
-  request: IRevisionOnConflictWithStringDate,
-  origin: IRevisionOnConflictWithStringDate,
-  latest: IRevisionOnConflictWithStringDate,
-  afterResolvedHandler: () => void,
-};
-
-type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
-  createdAt: string
-}
-
-const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
-  const {
-    onClose, request, origin, latest, optionsToSave, afterResolvedHandler,
-  } = props;
-
-  const { t } = useTranslation('');
-  const [resolvedRevision, setResolvedRevision] = useState<string>('');
-  const [isRevisionselected, setIsRevisionSelected] = useState<boolean>(false);
-  const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
-  const [codeMirrorRef, setCodeMirrorRef] = useState<HTMLDivElement | null>(null);
-
-  const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
-  const { data: pageId } = useCurrentPageId();
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: currentPathname } = useCurrentPathname();
-
-  const saveOrUpdate = useSaveOrUpdate();
-
-  const uncontrolledRef = useRef<CodeMirror>(null);
-
-  useEffect(() => {
-    if (codeMirrorRef != null) {
-      CodeMirror.MergeView(codeMirrorRef, {
-        value: origin.revisionBody,
-        origLeft: request.revisionBody,
-        origRight: latest.revisionBody,
-        lineNumbers: true,
-        collapseIdentical: true,
-        showDifferences: true,
-        highlightDifferences: true,
-        connect: 'connect',
-        readOnly: true,
-        revertButtons: false,
-      });
-    }
-  }, [codeMirrorRef, origin.revisionBody, request.revisionBody, latest.revisionBody]);
-
-  const close = useCallback(() => {
-    if (onClose != null) {
-      onClose();
-    }
-  }, [onClose]);
-
-  const onResolveConflict = useCallback(async() => {
-    if (currentPathname == null) { return }
-    // disable button after clicked
-    setIsRevisionSelected(false);
-
-    const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
-
-    try {
-      const { page } = await saveOrUpdate(
-        codeMirrorVal,
-        { pageId, path: currentPagePath || currentPathname, revisionId: remoteRevisionId },
-        optionsToSave,
-      );
-      const remotePageData = {
-        remoteRevisionId: page.revision._id,
-        remoteRevisionBody: page.revision.body,
-        remoteRevisionLastUpdateUser: page.lastUpdateUser,
-        remoteRevisionLastUpdatedAt: page.updatedAt,
-        revisionIdHackmdSynced: page.revisionIdHackmdSynced,
-        hasDraftOnHackmd: page.hasDraftOnHackmd,
-      };
-      setRemoteLatestPageData(remotePageData);
-      afterResolvedHandler();
-
-      close();
-
-      toastSuccess('Saved successfully');
-    }
-    catch (error) {
-      toastError(`Error occured: ${error.message}`);
-    }
-
-  }, [afterResolvedHandler, close, currentPagePath, currentPathname, optionsToSave, pageId, remoteRevisionId, saveOrUpdate, setRemoteLatestPageData]);
-
-  // TODO: No longer support custom close icon in bootstrap v5
-  // const resizeAndCloseButtons = useMemo(() => (
-  //   <div className="d-flex flex-nowrap">
-  //     <ExpandOrContractButton
-  //       isWindowExpanded={isModalExpanded}
-  //       expandWindow={() => setIsModalExpanded(true)}
-  //       contractWindow={() => setIsModalExpanded(false)}
-  //     />
-  //     <button type="button" className="close text-white" onClick={close} aria-label="Close">
-  //       <span aria-hidden="true">&times;</span>
-  //     </button>
-  //   </div>
-  // ), [isModalExpanded, close]);
-
-  const isOpen = props.isOpen ?? false;
-
-  return (
-    <Modal
-      isOpen={isOpen}
-      toggle={close}
-      backdrop="static"
-      className={`${isModalExpanded ? ' grw-modal-expanded' : ''}`}
-      size="xl"
-    >
-      {/* <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3" close={resizeAndCloseButtons}> */}
-      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3">
-        <span className="material-symbols-outlined">error</span>{t('modal_resolve_conflict.resolve_conflict')}
-      </ModalHeader>
-      <ModalBody className="mx-4 my-1">
-        { isOpen
-        && (
-          <div className="row">
-            <div className="col-12 text-center mt-2 mb-4">
-              <h2 className="fw-bold">{t('modal_resolve_conflict.resolve_conflict_message')}</h2>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.requested_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={request.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {request.user.username}</p>
-                  <p className="my-0">{request.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.origin_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={origin.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {origin.user.username}</p>
-                  <p className="my-0">{origin.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.latest_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={latest.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {latest.user.username}</p>
-                  <p className="my-0">{latest.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-12" ref={(el) => { setCodeMirrorRef(el) }}></div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(request.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'mine' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(origin.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'origin' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(latest.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'theirs' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-12">
-              <div className="border border-dark">
-                <h3 className="fw-bold my-2 mx-2">{t('modal_resolve_conflict.selected_editable_revision')}</h3>
-                <UncontrolledCodeMirror
-                  ref={uncontrolledRef}
-                  value={resolvedRevision}
-                  options={{
-                    placeholder: t('modal_resolve_conflict.resolve_conflict_message'),
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        )}
-      </ModalBody>
-      <ModalFooter>
-        <button
-          type="button"
-          className="btn btn-outline-secondary"
-          onClick={onClose}
-        >
-          {t('Cancel')}
-        </button>
-        <button
-          type="button"
-          className="btn btn-primary ms-3"
-          onClick={onResolveConflict}
-          disabled={!isRevisionselected}
-        >
-          {t('modal_resolve_conflict.resolve_and_save')}
-        </button>
-      </ModalFooter>
-    </Modal>
-  );
-};
-
-
-export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
-  const {
-    isOpen, onClose, optionsToSave, afterResolvedHandler,
-  } = props;
-  const { data: currentUser } = useCurrentUser();
-
-  // state for current page
-  const { data: currentPage } = useSWRxCurrentPage();
-
-  // state for latest page
-  const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { data: remoteRevisionBody } = useRemoteRevisionBody();
-  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
-  const { data: remoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
-
-  const currentTime: Date = new Date();
-
-  const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
-
-  if (!isOpen || currentUser == null || currentPage == null || isRemotePageDataInappropriate) {
-    return <></>;
-  }
-
-  const currentPageCreatedAtFixed = typeof currentPage.updatedAt === 'string'
-    ? parseISO(currentPage.updatedAt)
-    : currentPage.updatedAt;
-
-  const request: IRevisionOnConflictWithStringDate = {
-    revisionId: '',
-    revisionBody: props.markdownOnEdit,
-    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentUser,
-  };
-  const origin: IRevisionOnConflictWithStringDate = {
-    revisionId: currentPage?.revision._id,
-    revisionBody: currentPage?.revision.body,
-    createdAt: format(currentPageCreatedAtFixed, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentPage?.lastUpdateUser,
-  };
-  const latest: IRevisionOnConflictWithStringDate = {
-    revisionId: remoteRevisionId,
-    revisionBody: remoteRevisionBody,
-    createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
-    user: remoteRevisionLastUpdateUser,
-  };
-
-  const propsForCore = {
-    isOpen,
-    onClose,
-    optionsToSave,
-    request,
-    origin,
-    latest,
-    afterResolvedHandler,
-  };
-
-  return <ConflictDiffModalCore {...propsForCore} />;
-};

+ 0 - 174
apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx

@@ -1,174 +0,0 @@
-import React, {
-  FC, useCallback, useRef,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { useRipple } from 'react-use-ripple';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useUserUISettings } from '~/client/services/user-ui-settings';
-import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
-import { Themes, useNextThemes } from '~/stores/use-next-themes';
-
-import SidebarDockIcon from '../Icons/SidebarDockIcon';
-import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
-
-type AppearanceModeDropdownProps = {
-  isAuthenticated: boolean,
-}
-export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: AppearanceModeDropdownProps) => {
-
-  const { t } = useTranslation('commons');
-
-  const { isAuthenticated } = props;
-
-  const {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
-  } = useNextThemes();
-  const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
-  const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
-  const { scheduleToPut } = useUserUISettings();
-
-  // ripple
-  const buttonRef = useRef(null);
-  useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
-
-  const preferDrawerModeSwitchModifiedHandler = useCallback((preferDrawerMode: boolean, isEditMode: boolean) => {
-    if (isEditMode) {
-      mutatePreferDrawerModeOnEdit(preferDrawerMode);
-      scheduleToPut({ preferDrawerModeOnEditByUser: preferDrawerMode });
-    }
-    else {
-      updatePreferDrawerMode(preferDrawerMode);
-    }
-  }, [updatePreferDrawerMode, mutatePreferDrawerModeOnEdit, scheduleToPut]);
-
-  const followOsCheckboxModifiedHandler = useCallback((isChecked: boolean) => {
-    if (isChecked) {
-      setTheme(Themes.SYSTEM);
-    }
-    else {
-      setTheme(resolvedTheme ?? Themes.LIGHT);
-    }
-  }, [resolvedTheme, setTheme]);
-
-  const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
-    setTheme(isDarkMode ? Themes.DARK : Themes.LIGHT);
-  }, [setTheme]);
-
-  /* eslint-disable react/prop-types */
-  const IconWithTooltip = ({
-    id, label, children, additionalClasses,
-  }) => (
-    <>
-      <div id={id} className={`px-2 grw-icon-container ${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
-      <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
-    </>
-  );
-
-  const dropdownDivider = <div className="dropdown-divider"></div>;
-
-  const renderSidebarModeSwitch = useCallback((isEditMode: boolean) => {
-    return (
-      <>
-        <h6 className="dropdown-header">{t(isEditMode ? 'personal_dropdown.sidebar_mode_editor' : 'personal_dropdown.sidebar_mode')}</h6>
-        <form className="px-4">
-          <div className="justify-content-center">
-            <div className="col-auto mb-0 d-flex align-items-center">
-              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-drawer' : 'iwt-sidebar-drawer'} label="Drawer" additionalClasses="grw-sidebar-mode-icon">
-                <SidebarDrawerIcon />
-              </IconWithTooltip>
-              <div className="form-check form-switch form-check-secondary ms-2">
-                <input
-                  id={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}
-                  className="form-check-input"
-                  type="checkbox"
-                  checked={isEditMode ? !isPreferDrawerModeOnEdit : !isPreferDrawerMode}
-                  onChange={e => preferDrawerModeSwitchModifiedHandler(!e.target.checked, isEditMode)}
-                />
-                <label className="form-label form-check-label" htmlFor={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}></label>
-              </div>
-              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-dock' : 'iwt-sidebar-dock'} label="Dock" additionalClasses="grw-sidebar-mode-icon">
-                <SidebarDockIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-        </form>
-      </>
-    );
-  }, [isPreferDrawerMode, isPreferDrawerModeOnEdit, preferDrawerModeSwitchModifiedHandler, t]);
-
-  return (
-    <div className="dropend">
-      {/* setting button */}
-      {/* remove .dropdown-toggle for hide caret */}
-      {/* See https://stackoverflow.com/a/44577512/13183572 */}
-      <button className="btn btn-primary" type="button" data-bs-toggle="dropdown" ref={buttonRef} aria-haspopup="true">
-        <span className="material-symbols-outlined">settings</span>
-      </button>
-
-      {/* dropdown */}
-      <div className="dropdown-menu">
-
-        {/* sidebar mode */}
-        {renderSidebarModeSwitch(false)}
-
-        {/* side bar mode on editor */}
-        {isAuthenticated && (
-          <>
-            {dropdownDivider}
-            {renderSidebarModeSwitch(true)}
-          </>
-        )}
-
-        {/* color mode */}
-        { !isForcedByGrowiTheme && (
-          <>
-            {dropdownDivider}
-            <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-            <form className="px-4">
-              <div className="justify-content-center">
-                <div className="col-auto d-flex align-items-center">
-                  <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                  <span className="material-symbols-outlined">light_mode</span>
-                  </IconWithTooltip>
-                  <div className="form-check form-switch form-check-secondary ms-2">
-                    <input
-                      id="swUserPreference"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={isDarkMode}
-                      disabled={useOsSettings}
-                      onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                    />
-                    <label className="form-label form-check-label" htmlFor="swUserPreference"></label>
-                  </div>
-                  <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                  <span className="material-symbols-outlined">dark_mode</span>
-                  </IconWithTooltip>
-                </div>
-              </div>
-              <div>
-                <div className="col-auto">
-                  <div className="form-check">
-                    <input
-                      id="cbFollowOs"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={useOsSettings}
-                      onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                    />
-                    <label className="form-label form-check-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
-                  </div>
-                </div>
-              </div>
-            </form>
-          </>
-        ) }
-
-      </div>
-
-    </div>
-  );
-
-};

+ 0 - 672
apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss

@@ -1,672 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as *;
-
-@use '../variables' as var;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-@use './mixins/count-badge';
-@use './mixins/hsl-button';
-@use './hsl-functions' as hsl;
-
-// determine optional variables
-:root[data-bs-theme='dark'] {
-  $color-list: var(--color-list,var(--color-global));
-  $bgcolor-list: var(--bgcolor-list,var(--bgcolor-global));
-  $color-list-hover: var(--color-list-hover,var(--color-global));
-  $color-list-active: var(--color-list-active,var(--color-reversal));
-  $bgcolor-list-hover: var(--bgcolor-list-hover,var(--bgcolor-global));
-  $bgcolor-list-active: var(--bgcolor-list-active,var(--primary));
-  $color-table: var(--color-table,white);
-  $bgcolor-table: var(--bgcolor-table,#343a40);
-  $border-color-table: var(--border-color-table,lighten(#343a40, 7.5%));
-  $color-table-hover: var(--color-table-hover,rgba(white, 0.075));
-  $bgcolor-table-hover: var(--bgcolor-table-hover,lighten(#343a40, 7.5%));
-  $bgcolor-sidebar-list-group: var(--bgcolor-sidebar-list-group,var(--bgcolor-list));
-  $color-tags: var(--color-tags,#949494);
-  $bgcolor-tags: var(--bgcolor-tags,var(--dark));
-  $border-color-global: var(--border-color-global,#{$gray-500});
-  $border-color-toc: var(--border-color-toc,#{$border-color-global});
-  $color-dropdown: var(--color-dropdown,var(--color-global));
-  $bgcolor-dropdown: var(--bgcolor-dropdown,var(--bgcolor-global));
-  $color-dropdown-link: var(--color-dropdown-link,var(--color-global));
-  $color-dropdown-link-hover: var(--color-dropdown-link-hover,var(--light));
-  $bgcolor-dropdown-link-hover: var(--bgcolor-dropdown-link-hover,hsl.lighten(var(--bgcolor-global), 15%));
-  $color-dropdown-link-active: var(--color-dropdown-link-active,var(--light));
-  $bgcolor-dropdown-link-active: var(--bgcolor-dropdown-link-active,var(--primary));
-  $body-bg: var(--bgcolor-global);
-  $body-color: var(--color-global);
-
-  // override bootstrap variables
-  // $text-muted: $gray-550;
-  $table-dark-color: $color-table;
-  $table-dark-bg: $bgcolor-table;
-  $table-dark-border-color: $border-color-table;
-  $table-dark-hover-color: $color-table-hover;
-  $table-dark-hover-bg: $bgcolor-table-hover;
-  $border-color: $border-color-global;
-  $dropdown-color: $color-dropdown;
-  $dropdown-bg: $bgcolor-dropdown;
-  $dropdown-link-color: $color-dropdown-link;
-  $dropdown-link-hover-color: $color-dropdown-link-hover;
-  $dropdown-link-hover-bg: $bgcolor-dropdown-link-hover;
-  $dropdown-link-active-color: $color-dropdown-link-active;
-  $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
-
-  @import './mixins/list-group';
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-  // @import './reboot-bootstrap-text';
-  // @import './reboot-bootstrap-border-colors';
-  // @import './reboot-bootstrap-tables';
-  // @import './reboot-bootstrap-theme-colors';
-  // @import 'hsl-reboot-bootstrap-theme-colors';
-  // @import './reboot-bootstrap-dropdown';
-
-
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-
-  //   // List Group
-  //   @include override-list-group-item(
-  //     $color-list,
-  //     $bgcolor-sidebar-list-group,
-  //     $color-list-hover,
-  //     $bgcolor-list-hover,
-  //     $color-list-active,
-  //     $bgcolor-list-active
-  //   );
-  //   /*
-  //     * Form
-  //     */
-  //   input.form-control,
-  //   select.form-control,
-  //   select.form-select,
-  //   textarea.form-control {
-  //     color: var(--color-global);
-  //     background-color: hsl.darken(var(--bgcolor-global), 5%);
-  //     border-color: $border-color-global;
-  //     &:focus {
-  //       background-color: var(--bgcolor-global);
-  //     }
-  //     // FIXME: accent color
-  //     // border: 1px solid darken($border, 30%);
-  //   }
-
-  //   .form-control[disabled],
-  //   .form-control[readonly] {
-  //     color: hsl.lighten(var(--color-global),10%);
-  //     background-color: hsl.lighten(var(--bgcolor-global),5%);
-  //   }
-
-  // TODO: theme-color() dropped in bootstrap v5
-  // TODO: .input-group-prepend dropped in bootstrap v5
-  // https://redmine.weseek.co.jp/issues/128307
-  //   .input-group > .input-group-prepend > .input-group-text {
-  //     color: theme-color('light');
-  //     background-color: theme-color('secondary');
-  //     border: 1px solid theme-color('secondary');
-  //     border-right: none;
-  //     &.text-muted {
-  //       color: theme-color('light') !important;
-  //     }
-  //   }
-
-  //   .input-group input {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   label.form-check-label::before {
-  //     background-color: hsl.darken(var(--bgcolor-global),5%);
-  //   }
-
-  //   .rbt-input-multi .rbt-input-main {
-  //     color: black;
-  //   }
-  //   /*
-  //   * Table
-  //   */
-  //   .table {
-  //     @extend .table-dark !optional;
-  //     thead th {
-  //       vertical-align: bottom;
-  //       border-bottom: 2px solid #d6dadf;
-  //     }
-  //   }
-
-  //   /*
-  //   * Card
-  //   */
-  //   .card:not([class*='bg-']):not(.custom-card):not(.card-disabled) {
-  //     @extend .bg-dark;
-  //   }
-
-  //   .card.custom-card {
-  //     border-color: var(--secondary);
-  //   }
-
-  //   .card.card-disabled {
-  //     background-color: lighten($dark, 10%);
-  //     border-color: var(--secondary);
-  //   }
-
-  //   /*
-  //   * Pagination
-  //   */
-  //   ul.pagination {
-  //     li.page-item.disabled {
-  //       button.page-link {
-  //         color: $gray-400;
-  //       }
-  //     }
-  //     li.page-item.active {
-  //       button.page-link {
-  //         color: hsl.contrast(var(--primary));
-  //         background-color: var(--primary);
-  //         &:hover,
-  //         &:focus {
-  //           color: hsl.contrast(var(--primary));
-  //           background-color: var(--primary);
-  //         }
-  //       }
-  //     }
-  //     li.page-item {
-  //       button.page-link {
-  //         @extend .btn-dark;
-  //         color: var(--primary);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Login form
-  //   */
-  //   .nologin {
-  //     // background color
-  //     $color-gradient: #3c465c;
-  //     background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-  //       linear-gradient(135deg, darken(var.$growi-green, 30%) 10%, hsla(225, 95%, 50%, 0) 70%),
-  //       linear-gradient(225deg, darken(var.$growi-blue, 20%) 10%, hsla(140, 90%, 50%, 0) 80%),
-  //       linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
-  //     .nologin-header {
-  //       background-color: rgba(black, 0.5);
-
-  //       .logo {
-  //         background-color: rgba(white, 0);
-  //         fill: rgba(white, 0.5);
-  //       }
-
-  //       h1 {
-  //         color: rgba(white, 0.5);
-  //       }
-  //     }
-
-  //     .nologin-dialog {
-  //       background-color: rgba(black, 0.5);
-  //       .link-switch {
-  //         color: #7b9bd5;
-  //         &:hover {
-  //           color: lighten(#7b9bd5,10%);
-  //         }
-  //       }
-  //     }
-
-  //     .input-group {
-  //       .input-group-text {
-  //         color: darken(white, 30%);
-  //         background-color: rgba($gray-700, 0.7);
-  //       }
-
-  //       .form-control {
-  //         color: white;
-  //         background-color: rgba(#505050, 0.7);
-  //         box-shadow: unset;
-
-  //         &::placeholder {
-  //           color: darken(white, 30%);
-  //         }
-  //       }
-  //     }
-
-  //     .btn-fill {
-  //       .btn-label {
-  //         color: $gray-300;
-  //       }
-  //       .btn-label-text {
-  //         color: $gray-400;
-  //       }
-  //     }
-
-  //     .grw-external-auth-form {
-  //       border-color: gray !important;
-  //     }
-
-  //     .btn-external-auth-tab {
-  //       @extend .btn-dark;
-  //     }
-
-  //     // footer link text
-  //     .link-growi-org {
-  //       color: rgba(white, 0.4);
-
-  //       &:hover,
-  //       &.focus {
-  //         color: rgba(white, 0.7);
-
-  //         .growi {
-  //           color: darken(var.$growi-green, 5%);
-  //         }
-
-  //         .org {
-  //           color: darken(var.$growi-blue, 5%);
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI subnavigation
-  //   */
-  //   .grw-drawer-toggler {
-  //     @include button-variant($dark, $dark);
-  //     @include mixins-buttons.button-svg-icon-variant($dark, $dark);
-  //     color: $gray-400;
-  //     box-shadow: none !important;
-  //   }
-
-  //   /**
-  //    * GROWI PagePathHierarchicalLink
-  //    */
-  //   .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
-  //     color: $gray-400;
-  //   }
-
-  //   /*
-  //   * GROWI page list
-  //   */
-  //   .page-list {
-  //     .page-list-ul {
-  //       > li {
-  //         > span.page-list-meta {
-  //           color: hsl.darken(var(--color-global),10%);
-  //         }
-  //       }
-  //     }
-
-  //     // List group
-  //     .list-group-item {
-  //       &.active {
-  //         background-color: hsl.lighten(var(--bgcolor-global),10%) !important;
-  //       }
-  //       &.list-group-item-action:hover {
-  //         background-color: hsl.lighten(var(--bgcolor-global),10%) !important;
-  //       }
-  //       .page-list-snippet {
-  //         color: hsl.darken(var(--color-global),10%);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI ToC
-  //   */
-  //   .revision-toc-content {
-  //     ::marker {
-  //       color: hsl.lighten(var(--color-global),30%);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI subnavigation
-  //   */
-  //   .grw-subnav {
-  //     background-color: var(--bgcolor-subnav);
-  //   }
-
-  //   .grw-subnav-fixed-container .grw-subnav {
-  //     background-color: hsl.alpha(var(--bgcolor-subnav),85%);
-  //   }
-
-  //   .grw-page-editor-mode-manager {
-  //     .btn-outline-primary {
-  //       &:hover {
-  //         color: var(--primary);
-  //         background-color: $gray-700;
-  //       }
-  //     }
-  //   }
-
-  //   // Search drop down
-  //   #search-typeahead-asynctypeahead {
-  //     background-color: var(--bgcolor-global);
-  //     .table {
-  //       background-color: transparent;
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Sidebar
-  //   */
-  //   .grw-sidebar {
-  //     --gray-500: hsl(var(--gray-500-hs),var(--gray-500-l));
-  //     --gray-500-hs: 210,13%;
-  //     --gray-500-l: 61%;
-  //     // List
-  //     @include override-list-group-item(
-  //       $color-list,
-  //       $bgcolor-sidebar-list-group,
-  //       $color-list-hover,
-  //       $bgcolor-list-hover,
-  //       $color-list-active,
-  //       $bgcolor-list-active
-  //     );
-  //     // Pagetree
-  //     .grw-pagetree, .grw-foldertree {
-  //       @include override-list-group-item-for-pagetree(
-  //         var(--color-sidebar-context),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),8%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),15%),
-  //         hsl.darken(var(--color-sidebar-context),15%),
-  //         hsl.darken(var(--color-sidebar-context),10%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),18%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),24%)
-  //       );
-  //       .grw-pagetree-triangle-btn, .grw-foldertree-triangle-btn {
-  //         @include mixins-buttons.button-outline-svg-icon-variant(var(--secondary), $gray-200);
-  //       }
-  //       .btn-page-item-control {
-  //         @include hsl-button.button-outline-variant(var(--gray-500), var(--gray-500), var(--secondary), transparent);
-  //         &:hover {
-  //           background-color: hsl.lighten(var(--bgcolor-sidebar-context),20%);
-  //         }
-  //         &:not(:disabled):not(.disabled):active,
-  //         &:not(:disabled):not(.disabled).active {
-  //           background-color: hsl.lighten(var(--bgcolor-sidebar-context),34%);
-  //         }
-  //         box-shadow: none !important;
-  //       }
-  //     }
-
-  //     // bookmarks
-  //     .grw-folder-tree-container {
-  //       .grw-drop-item-area , .grw-foldertree-item-container {
-  //         .grw-accept-drop-item {
-  //           border-color: hsl.lighten(var(--bgcolor-sidebar-context), 30%) !important;
-  //         }
-  //       }
-  //     }
-  //     .private-legacy-pages-link {
-  //       &:hover {
-  //         background: var(--bgcolor-list-hover);
-  //       }
-  //     }
-  //   }
-
-  //   .btn.btn-page-item-control {
-  //     @include hsl-button.button-outline-variant(var(--gray-500), var(--gray-500), var(--secondary), transparent);
-  //     &:hover {
-  //       background-color: $gray-700;
-  //     }
-  //     &:not(:disabled):not(.disabled):active,
-  //     &:not(:disabled):not(.disabled).active {
-  //       color: $gray-200;
-  //       background-color: $gray-600;
-  //     }
-  //     box-shadow: none !important;
-  //   }
-
-  //   // Bookmark item on user page
-  //   .grw-user-page-list-m {
-  //     @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
-  //     .grw-foldertree {
-  //       @include override-list-group-item-for-pagetree(
-  //         $body-color,
-  //         hsl.lighten($body-bg, 8%),
-  //         hsl.lighten($body-bg, 15%),
-  //         hsl.darken($body-color, 15%),
-  //         hsl.darken($body-color, 10%),
-  //         hsl.lighten($body-bg, 18%),
-  //         hsl.lighten($body-bg, 24%)
-  //       );
-  //       .grw-foldertree-triangle-btn {
-  //         @include mixins-buttons.button-outline-svg-icon-variant($secondary, $gray-200);
-  //       }
-  //     }
-  //     .grw-folder-tree-container {
-  //       .grw-drop-item-area , .grw-foldertree-item-container {
-  //         .grw-accept-drop-item {
-  //           border-color: hsl.lighten(var($body-bg), 30%) !important;
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   // Bookmark dropdown menu
-  //   .grw-bookmark-folder-dropdown  {
-  //     .grw-bookmark-folder-menu {
-  //       .form-control{
-  //         &:focus {
-  //           color: $body-color
-  //         }
-  //       }
-  //       .grw-bookmark-folder-menu-item  {
-  //         @include mixins-buttons.button-outline-svg-icon-variant($secondary, $gray-200);
-  //         .grw-bookmark-folder-menu-item-title {
-  //           color: $body-color
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * Popover
-  //   */
-  //   .popover {
-  //     background-color: var(--bgcolor-global);
-  //     border-color: var(--secondary);
-  //     .popover-header {
-  //       color: white;
-  //       background-color: var(--secondary);
-  //       border-color: var(--secondary);
-  //     }
-  //     .popover-body {
-  //       color: inherit;
-  //     }
-
-  // TODO: Check renamed .arrow to .popover-arrow
-  // see: https://getbootstrap.com/docs/5.2/migration/#popovers
-
-  //     &.bs-popover-top .arrow {
-  //       &::before {
-  //         border-top-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-top-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-bottom .arrow {
-  //       &::before {
-  //         border-bottom-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-bottom-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-right .arrow {
-  //       &::before {
-  //         border-right-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-right-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-left .arrow {
-  //       &::before {
-  //         border-left-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-left-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Grid Edit Modal
-  //   */
-  //   .grw-grid-edit-preview {
-  //     background: $gray-900;
-  //   }
-
-  //   /*
-  //   * Slack
-  //   */
-  //   .grw-slack-notification {
-  //     background-color: transparent;
-  //     $color-slack: #4b144c;
-
-  //     .form-control {
-  //       background: var(--bgcolor-global);
-  //     }
-
-  //     .form-check-label {
-  //       &::before {
-  //         background-color: var(--secondary);
-  //         border-color: transparent;
-  //       }
-  //       &::after {
-  //         background-color: darken($color-slack, 5%);
-  //         background-image: url(/images/icons/slack/slack-logo-dark-off.svg);
-  //       }
-  //     }
-
-  //     .form-check-input:checked ~ .form-check-label {
-  //       &::before {
-  //         background-color: lighten($color-slack, 10%);
-  //       }
-  //       &::after {
-  //         background-color: darken($color-slack, 5%);
-  //         background-image: url(/images/icons/slack/slack-logo-dark-on.svg);
-  //       }
-  //     }
-  //     .grw-slack-logo svg {
-  //       fill: #dd80de;
-  //     }
-
-  //     .grw-btn-slack {
-  //       background-color: black;
-  //       &:focus,
-  //       &:hover {
-  //         background-color: black;
-  //       }
-  //     }
-
-  //     .grw-btn-slack-triangle {
-  //       color: var(--secondary);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI HandsontableModal
-  //   */
-
-  //   .handsontable td {
-  //     color: black;
-  //   }
-
-  //   .grw-hot-modal-navbar {
-  //     background-color: var(--dark);
-  //   }
-
-  //   .wiki {
-  //     h1 {
-  //       border-color: hsl.lighten(var(--border-color-theme),10%);
-  //     }
-  //     h2 {
-  //       border-color: var(--border-color-theme);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI comment form
-  //   */
-  //   .comment-form {
-  //     #slack-mark-black {
-  //       display: none;
-  //     }
-  //   }
-
-  //   .page-comment-form .comment-form-main {
-  //     &:before {
-  //       border-right-color: var(--bgcolor-global);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI tags
-  //   */
-  //   .grw-tag-labels {
-  //     .grw-tag-label {
-  //       color: $color-tags;
-  //       background-color: $bgcolor-tags;
-  //     }
-  //   }
-
-  //   mark.rbt-highlight-text {
-  //     color: var(--color-global);
-  //   }
-
-  //   /*
-  //   * GROWI popular tags
-  //   */
-  //   .grw-popular-tag-labels {
-  //     .grw-tag-label {
-  //       color: $color-tags;
-  //       background-color: $bgcolor-tags;
-  //     }
-  //   }
-
-  //   /*
-  //   * admin settings
-  //   */
-  //   .admin-setting-header {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * grw-side-contents
-  //   */
-  //   .grw-side-contents-sticky-container {
-  //     .grw-count-badge {
-  //       @include count-badge.count-badge($gray-400, $gray-700);
-  //     }
-
-  //     .grw-border-vr {
-  //       border-color: $border-color-toc;
-  //     }
-
-  //     .revision-toc {
-  //       border-color: $border-color-toc;
-  //     }
-  //   }
-
-  //   /*
-  //   * drawio
-  //   */
-  //   .drawio-viewer {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * modal
-  //   */
-  //   .grw-modal-head {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * skeleton
-  //   */
-  //   .grw-skeleton {
-  //     background-color: hsl.lighten(var(--bgcolor-subnav),10%);
-  //   }
-}

+ 0 - 534
apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss

@@ -1,534 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as *;
-
-@use '../variables' as var;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-@use './mixins/count-badge';
-@use './mixins/hsl-button';
-@use './hsl-functions' as hsl;
-
-// determine optional variables
-:root[data-bs-theme='light'] {
-  $color-list: var(--color-list,var(--color-global));
-  $bgcolor-list: var(--bgcolor-list,var(--bgcolor-global));
-  $color-list-hover: var(--color-list-hover,var(--color-global));
-  $bgcolor-list-hover: var(--bgcolor-list-hover, var(--bgcolor-global));
-  $bgcolor-list-active: var(--bgcolor-list-active, hsl.lighten(var(--primary),65%));
-  $color-list-active: var(--color-list-active,hsl(var(--primary-hs), clamp(10%, (100% - var(--primary-l)  - 65% - 51%) * 1000, 95%)));
-  $color-table: var(--color-table,var(--color-global));
-  $bgcolor-table: var(--bgcolor-table,null);
-  $border-color-table: var(--border-color-table,#{$gray-200});
-  $color-table-hover: var(--color-table-hover,var(--color-table));
-  $bgcolor-table-hover: var(--bgcolor-table-hover,rgba(black, 0.075));
-  $bgcolor-sidebar-list-group: var(--bgcolor-sidebar-list-group,var(--bgcolor-list));
-  $color-tags: var(--color-tags,var(--secondary));
-  $bgcolor-tags: var(--bgcolor-tags,#{$gray-200});
-  $border-color-global: var(--border-color-global,#{$gray-300});
-  $border-color-toc: var(--border-color-toc,#{$gray-300});
-  $color-dropdown: var(--color-dropdown,var(--color-global));
-  $bgcolor-dropdown: var(--color-dropdown,var(--bgcolor-global));
-  $color-dropdown-link: var(--color-dropdown-link,var(--color-global));
-  $color-dropdown-link-hover: var(--color-dropdown-link-hover,var(--color-global));
-  $color-dropdown-link-active: var(--color-dropdown-link-active,var(--color-reversal));
-  $bgcolor-dropdown-link-hover: hsl.darken(var(--bgcolor-global),15%);
-  $bgcolor-dropdown-link-active: var(--bgcolor-dropdown-link-active,var(--primary));
-  $body-bg: var(--bgcolor-global);
-  $body-color: var(--color-global);
-
-  // override bootstrap variables
-  $text-muted: $gray-500;
-  $table-color: $color-table;
-  $table-bg: $bgcolor-table;
-  $table-border-color: $border-color-table;
-  $table-hover-color: $color-table-hover;
-  $table-hover-bg: $bgcolor-table-hover;
-  $border-color: $border-color-global;
-  $dropdown-color: $color-dropdown;
-  $dropdown-link-color: $color-dropdown-link;
-  $dropdown-link-hover-color: $color-dropdown-link-hover;
-  $dropdown-link-hover-bg: $bgcolor-dropdown-link-hover;
-  $dropdown-link-active-color: $color-dropdown-link-active;
-  $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
-
-  @import './mixins/list-group';
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-  // @import './reboot-bootstrap-text';
-  // @import './reboot-bootstrap-border-colors';
-  // @import './reboot-bootstrap-tables';
-  // @import './reboot-bootstrap-theme-colors';
-  // @import 'hsl-reboot-bootstrap-theme-colors';
-  // @import './reboot-bootstrap-dropdown';
-
-  // List Group
-  @include override-list-group-item(
-    $color-list,
-    $bgcolor-sidebar-list-group,
-    $color-list-hover,
-    $bgcolor-list-hover,
-    $color-list-active,
-    $bgcolor-list-active
-  );
-  /*
-  * Form
-  */
-  .form-control {
-    background-color: var(--bgcolor-global);
-  }
-
-  .form-control::placeholder {
-    color: hsl.darken(var(--bgcolor-global), 20%);
-  }
-
-  .form-control[disabled],
-  .form-control[readonly] {
-    color: hsl.lighten(var(--color-global),10%);
-    background-color: hsl.darken(var(--bgcolor-global),5%);
-  }
-
-  /*
-  * card
-  */
-  .card.card-disabled {
-    background-color: var(--bgcolor-global);
-    border-color: $gray-200;
-  }
-
-  /*
-  * GROWI Login form
-  */
-  .nologin {
-    // background color
-    $color-gradient: #3c465c;
-    background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-      linear-gradient(135deg, var.$growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, var.$growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
-      linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
-    .nologin-header {
-      background-color: rgba(white, 0.5);
-
-      .logo {
-        background-color: rgba(black, 0);
-        fill: rgba(black, 0.5);
-      }
-
-      h1 {
-        color: rgba(black, 0.5);
-      }
-    }
-
-    .nologin-dialog {
-      background-color: rgba(white, 0.5);
-      .link-switch {
-        color: #1939b8;
-        &:hover {
-          color: lighten(#1939b8,20%);
-        }
-      }
-    }
-
-    .dropdown-with-icon {
-      .dropdown-toggle {
-        color: white;
-        background-color: rgba($gray-600, 0.7);
-        box-shadow: unset;
-        &:focus {
-          color: white;
-          background-color: rgba($gray-600, 0.7);
-        }
-      }
-      i {
-        color: darken(white, 30%);
-        background-color: rgba($gray-700, 0.7);
-      }
-    }
-
-    .input-group {
-      .input-group-text {
-        color: darken(white, 30%);
-        background-color: rgba($gray-700, 0.7);
-      }
-
-      .form-control {
-        color: white;
-        background-color: rgba($gray-600, 0.7);
-        box-shadow: unset;
-
-        &::placeholder {
-          color: darken(white, 30%);
-        }
-      }
-    }
-
-    // footer link text
-    .link-growi-org {
-      color: rgba(black, 0.4);
-
-      &:hover,
-      &.focus {
-        color: black;
-
-        .growi {
-          color: darken(var.$growi-green, 20%);
-        }
-
-        .org {
-          color: darken(var.$growi-blue, 15%);
-        }
-      }
-    }
-  }
-
-  /*
-  * GROWI subnavigation
-  */
-  // .grw-subnav {
-  //   background-color: var(--bgcolor-subnav);
-  // }
-
-  .grw-subnav-fixed-container .grw-subnav {
-    background-color: hsl.alpha(var(--bgcolor-subnav),85%);
-  }
-
-  .grw-page-editor-mode-manager {
-    .btn-outline-primary {
-      &:hover {
-        color: var(--primary);
-        background-color: $gray-200;
-      }
-    }
-  }
-
-  .grw-drawer-toggler {
-    @include button-variant($light, $light);
-    @include mixins-buttons.button-svg-icon-variant($light, $light);
-    color: $gray-500;
-    box-shadow: none !important;
-  }
-
-  /**
-   * GROWI PagePathHierarchicalLink
-   */
-  // .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
-  //   color: $gray-600;
-  // }
-  /*
-  * GROWI Sidebar
-  */
-  .grw-sidebar {
-    // List
-    @include override-list-group-item(
-      $color-list,
-      $bgcolor-sidebar-list-group,
-      $color-list-hover,
-      $bgcolor-list-hover,
-      $color-list-active,
-      $bgcolor-list-active
-    );
-    // sidebar-centent-bg
-    .grw-navigation-wrap {
-      // Drop a shadow on the light theme. The dark theme makes '$ bgcolor-sidebar-context' brighter than the body.
-      box-shadow: 0px 0px 3px rgba(black, 0.24);
-    }
-    // Pagetree
-    .grw-pagetree, .grw-foldertree {
-      @include override-list-group-item-for-pagetree(
-        var(--color-sidebar-context),
-        hsl.darken(var(--bgcolor-sidebar-context),5%),
-        hsl.darken(var(--bgcolor-sidebar-context),12%),
-        hsl.lighten(var(--color-sidebar-context),10%),
-        hsl.lighten(var(--color-sidebar-context),8%),
-        hsl.darken(var(--bgcolor-sidebar-context),15%),
-        hsl.darken(var(--bgcolor-sidebar-context),24%)
-      );
-
-      .grw-pagetree-triangle-btn, .grw-foldertree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant($gray-400, var(--primary));
-      }
-    }
-
-    // bookmark
-    .grw-folder-tree-container {
-      .grw-drop-item-area, .grw-foldertree-item-container  {
-        .grw-accept-drop-item {
-          border-color: hsl.darken(var(--bgcolor-sidebar-context), 30%) !important;
-        }
-      }
-    }
-
-    .private-legacy-pages-link {
-      &:hover {
-        background: $bgcolor-list-hover;
-      }
-    }
-  }
-
-  .btn.btn-page-item-control {
-    --gray-500: hsl(var(--gray-500-hs),var(--gray-500-l));
-    --gray-500-hs: 210,13%;
-    --gray-500-l: 61%;
-    @include hsl-button.button-outline-variant(var(--gray-500), var(--primary), #{hsl.lighten(var(--primary), 52%)}, transparent);
-    &:hover {
-      background-color: hsl.lighten(var(--primary), 58%);
-    }
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: var(--primary);
-    }
-    box-shadow: none !important;
-  }
-
-
-  // Bookmark item on user page
-  .grw-user-page-list-m {
-    @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
-    .grw-foldertree {
-      @include override-list-group-item-for-pagetree(
-        $body-color,
-        hsl.darken($body-bg, 5%),
-        hsl.darken($body-bg, 12%),
-        hsl.lighten($body-color, 10%),
-        hsl.lighten($body-color, 8%),
-        hsl.darken($body-bg, 15%),
-        hsl.darken($body-bg, 24%)
-      );
-      .grw-foldertree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant($gray-400, $primary);
-      }
-    }
-    .grw-folder-tree-container {
-      .grw-drop-item-area, .grw-foldertree-item-container  {
-        .grw-accept-drop-item {
-          border-color: hsl.darken(var($body-bg), 30%) !important;
-        }
-      }
-    }
-  }
-
-  // Bookmark dropdown menu
-  .grw-bookmark-folder-menu {
-    .form-control{
-      &:focus {
-        color: $body-color
-      }
-    }
-    .grw-bookmark-folder-menu-item {
-      @include mixins-buttons.button-outline-svg-icon-variant($gray-400, $primary);
-      .grw-bookmark-folder-menu-item-title {
-        color: $body-color
-      }
-    }
-  }
-
-  /*
-  * GROWI page list
-  */
-  .page-list {
-    .page-list-ul {
-      > li {
-        > span.page-list-meta {
-          color: hsl.lighten(var(--color-global),10%);
-        }
-      }
-    }
-    // List group
-    .list-group-item {
-      &.active {
-        background-color: hsl.lighten(var(--primary),77%) !important;
-      }
-      &.list-group-item-action:hover {
-        background-color: hsl.lighten(var(--primary),72%) !important;
-      }
-      .page-list-snippet {
-        color: hsl.lighten(var(--color-global),10%);
-      }
-    }
-  }
-
-  /*
-  * GROWI ToC
-  */
-  .revision-toc-content {
-    ::marker {
-      color: hsl.darken(var(--bgcolor-global),20%);
-    }
-  }
-
-  /*
-  * GROWI Editor
-  */
-  .grw-editor-navbar-bottom {
-    background-color: $gray-100;
-
-    #slack-mark-white {
-      display: none;
-    }
-
-    .input-group-text {
-      margin-right: 1px;
-      color: var(--secondary);
-      border-color: var(--light);
-    }
-
-    .btn.btn-outline-secondary {
-      border-color: $border-color;
-    }
-  }
-
-  /*
-  * GROWI Link Edit Modal
-  */
-  .link-edit-modal {
-    span i {
-      color: $gray-400;
-    }
-  }
-
-  /*
-  * GROWI Grid Edit Modal
-  */
-
-  .grw-grid-edit-preview {
-    background: $gray-100;
-  }
-
-  /*
-  * Slack
-  */
-  .grw-slack-notification {
-    background-color: white;
-    $color-slack: #4b144c;
-
-    .form-control {
-      background: white;
-    }
-
-    .form-check-label {
-      &::before {
-        background-color: $gray-200;
-        border-color: transparent;
-      }
-      &::after {
-        background-color: white;
-        background-image: url(/images/icons/slack/slack-logo-off.svg);
-      }
-    }
-    .form-check-input:checked ~ .form-check-label {
-      &::before {
-        background-color: lighten($color-slack, 60%);
-      }
-      &::after {
-        background-image: url(/images/icons/slack/slack-logo-on.svg);
-      }
-    }
-    .grw-slack-logo svg {
-      fill: #af30b0;
-    }
-
-    .grw-btn-slack {
-      background-color: white;
-
-      &:hover,
-      &:focus {
-        background-color: white;
-      }
-    }
-
-    .grw-btn-slack-triangle {
-      color: var(--secondary);
-    }
-  }
-
-  /*
-  * GROWI HandsontableModal
-  */
-  .grw-hot-modal-navbar {
-    background-color: var(--light);
-  }
-
-  .wiki {
-    h1 {
-      border-color: var(--border-color-theme);
-    }
-    h2 {
-      border-color: var(--border-color-theme);
-    }
-  }
-
-  /*
-  * GROWI comment form
-  */
-  .comment-form {
-    #slack-mark-white {
-      display: none;
-    }
-  }
-
-  .page-comment-form .comment-form-main {
-    &:before {
-      border-right-color: var(--bgcolor-global);
-    }
-  }
-
-  /*
-  * GROWI tags
-  */
-  .grw-tag-labels {
-    .grw-tag-label {
-      color: $color-tags;
-      background-color: $bgcolor-tags;
-    }
-  }
-
-  /*
-  * GROWI popular tags
-  */
-  .grw-popular-tag-labels {
-    .grw-tag-label {
-      color: $color-tags;
-      background-color: $bgcolor-tags;
-    }
-  }
-
-  /*
-  * grw-side-contents
-  */
-  .grw-side-contents-sticky-container {
-    .grw-count-badge {
-      @include count-badge.count-badge($gray-600, $gray-200);
-    }
-
-    .grw-border-vr {
-      border-color: $border-color-toc;
-    }
-    .revision-toc {
-      border-color: $border-color-toc;
-    }
-  }
-
-  /*
-  * drawio
-  */
-  .drawio-viewer {
-    border-color: $border-color-global;
-  }
-
-  /*
-  * admin settings
-  */
-  .admin-setting-header {
-    border-color: $border-color;
-  }
-
-  /*
-  * modal
-  */
-  .grw-modal-head {
-    border-color: $border-color-global;
-  }
-
-  /*
-  * skeleton
-  */
-  .grw-skeleton {
-    background-color: hsl.darken(var(--bgcolor-subnav),10%);
-  }
-}

+ 0 - 22
apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,22 +0,0 @@
-.toast-success {
-  background-color: var(--success) !important;
-}
-
-.toast-error {
-  background-color: var(--danger) !important;
-}
-
-.toast-info {
-  background-color: var(--info) !important;
-}
-
-.toast-warning {
-  background-color: var(--warning) !important;
-}
-
-:root {
-  --toastify-color-info: var(--info);
-  --toastify-color-success: var(--success);
-  --toastify-color-warning: var(--warning);
-  --toastify-color-error: var(--danger);
-}

+ 0 - 11
apps/app/_obsolete/src/styles/theme/apply-colors.scss

@@ -561,17 +561,6 @@ ul.pagination {
   background: var.$growi-green;
   background: var.$growi-green;
 }
 }
 
 
-/*
- * GROWI comment
- */
-.page-comment-meta .page-comment-revision svg {
-  fill: var(--color-link);
-
-  &:hover {
-    fill: var(--color-link-hover);
-  }
-}
-
 /*
 /*
  * GROWI comment form
  * GROWI comment form
  */
  */

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
+* [`7.0.1`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.1/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 0 - 1
apps/app/docker/codebuild/buildspec.yml

@@ -20,7 +20,6 @@ phases:
   build:
   build:
     commands:
     commands:
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .
-      - docker tag ${IMAGE_TAG}
 
 
   post_build:
   post_build:
     commands:
     commands:

+ 2 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.0.1-RC.0",
+  "version": "7.0.2-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -244,7 +244,7 @@
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "^5.3.1",
     "bootstrap": "^5.3.1",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "diff2html": "^3.4.35",
+    "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-cypress": "^2.12.1",

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

@@ -328,6 +328,8 @@
     "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
     "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "Commments",
+    "comment": "Commment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "No user found"
     "no_user_found": "No user found"
   },
   },

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

@@ -361,6 +361,8 @@
     "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
     "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "コメント",
+    "comment": "コメント",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
     "no_user_found": "ユーザー名が見つかりません"
     "no_user_found": "ユーザー名が見つかりません"
   },
   },

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

@@ -318,6 +318,8 @@
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "评论",
+    "comment": "评论",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "未找到用户名"
     "no_user_found": "未找到用户名"
   },
   },

+ 7 - 0
apps/app/src/client/services/create-page/create-page.ts

@@ -0,0 +1,7 @@
+import { apiv3Post } from '~/client/util/apiv3-client';
+import type { IApiv3PageCreateParams, IApiv3PageCreateResponse } from '~/interfaces/apiv3';
+
+export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
+  const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
+  return res.data;
+};

+ 1 - 0
apps/app/src/client/services/create-page/index.ts

@@ -1,2 +1,3 @@
+export * from './create-page';
 export * from './use-create-page-and-transit';
 export * from './use-create-page-and-transit';
 export * from './use-create-template-page';
 export * from './use-create-template-page';

+ 3 - 1
apps/app/src/client/services/create-page/use-create-page-and-transit.tsx

@@ -2,12 +2,14 @@ import { useCallback, useState } from 'react';
 
 
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
-import { createPage, exist } from '~/client/services/page-operation';
+import { exist } from '~/client/services/page-operation';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { createPage } from './create-page';
+
 const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
 const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
 
 
 /**
 /**

+ 0 - 8
apps/app/src/client/services/page-operation.ts

@@ -4,9 +4,6 @@ import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import type {
-  IApiv3PageCreateParams, IApiv3PageCreateResponse, IApiv3PageUpdateParams, IApiv3PageUpdateResponse,
-} from '~/interfaces/apiv3';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
@@ -91,11 +88,6 @@ export const resumeRenameOperation = async(pageId: string): Promise<void> => {
   await apiv3Post('/pages/resume-rename', { pageId });
   await apiv3Post('/pages/resume-rename', { pageId });
 };
 };
 
 
-export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
-  const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
-  return res.data;
-};
-
 export type UpdateStateAfterSaveOption = {
 export type UpdateStateAfterSaveOption = {
   supressEditingMarkdownMutation: boolean,
   supressEditingMarkdownMutation: boolean,
 }
 }

+ 1 - 0
apps/app/src/client/services/upload-attachments/index.ts

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

+ 39 - 0
apps/app/src/client/services/upload-attachments/upload-attachments.ts

@@ -0,0 +1,39 @@
+import type { IAttachment } from '@growi/core';
+
+import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import type { IApiv3GetAttachmentLimitParams, IApiv3GetAttachmentLimitResponse, IApiv3PostAttachmentResponse } from '~/interfaces/apiv3/attachment';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:client:services:upload-attachment');
+
+
+type UploadOpts = {
+  onUploaded?: (attachment: IAttachment) => void,
+  onError?: (error: Error, file: File) => void,
+}
+
+export const uploadAttachments = async(pageId: string, files: File[], opts?: UploadOpts): Promise<void> => {
+  files.forEach(async(file) => {
+    try {
+      const params: IApiv3GetAttachmentLimitParams = { fileSize: file.size };
+      const { data: resLimit } = await apiv3Get<IApiv3GetAttachmentLimitResponse>('/attachment/limit', params);
+
+      if (!resLimit.isUploadable) {
+        throw new Error(resLimit.errorMessage);
+      }
+
+      const formData = new FormData();
+      formData.append('file', file);
+      formData.append('page_id', pageId);
+
+      const { data: resAdd } = await apiv3PostForm<IApiv3PostAttachmentResponse>('/attachment', formData);
+
+      opts?.onUploaded?.(resAdd.attachment);
+    }
+    catch (e) {
+      logger.error('failed to upload', e);
+      opts?.onError?.(e, file);
+    }
+  });
+};

+ 1 - 1
apps/app/src/client/util/apiv3-client.ts

@@ -1,5 +1,5 @@
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports
-import { AxiosResponse } from 'axios';
+import type { AxiosResponse } from 'axios';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports

+ 2 - 1
apps/app/src/client/util/toastr.ts

@@ -1,4 +1,5 @@
-import { toast, ToastContent, ToastOptions } from 'react-toastify';
+import type { ToastContent, ToastOptions } from 'react-toastify';
+import { toast } from 'react-toastify';
 
 
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 

+ 4 - 4
apps/app/src/components/Admin/UserManagement.tsx

@@ -90,7 +90,7 @@ const UserManagement = (props: UserManagementProps) => {
           onChange={() => clickHandler(status)}
           onChange={() => clickHandler(status)}
         />
         />
         <label className="form-label form-check-label" htmlFor={`c_${status}`}>
         <label className="form-label form-check-label" htmlFor={`c_${status}`}>
-          <span className={`badge rounded-pill bg-${statusColor} d-inline-block vt mt-1`}>
+          <span className={`badge text-bg-${statusColor} d-inline-block vt mt-1`}>
             {statusLabel}
             {statusLabel}
           </span>
           </span>
         </label>
         </label>
@@ -167,11 +167,11 @@ const UserManagement = (props: UserManagementProps) => {
 
 
           <div className="offset-md-1 col-md-6 my-2">
           <div className="offset-md-1 col-md-6 my-2">
             <div>
             <div>
-              {renderCheckbox('all', 'All', 'secondary')}
+              {renderCheckbox('all', 'All', 'primary')}
               {renderCheckbox('registered', 'Approval Pending', 'info')}
               {renderCheckbox('registered', 'Approval Pending', 'info')}
               {renderCheckbox('active', 'Active', 'success')}
               {renderCheckbox('active', 'Active', 'success')}
-              {renderCheckbox('suspended', 'Suspended', 'warning text-dark')}
-              {renderCheckbox('invited', 'Invited', 'pink')}
+              {renderCheckbox('suspended', 'Suspended', 'warning')}
+              {renderCheckbox('invited', 'Invited', 'secondary')}
             </div>
             </div>
             <div>
             <div>
               { isNotifyCommentShow && <span className="text-warning">{t('admin:user_management.click_twice_same_checkbox')}</span> }
               { isNotifyCommentShow && <span className="text-warning">{t('admin:user_management.click_twice_same_checkbox')}</span> }

+ 9 - 9
apps/app/src/components/Admin/Users/UserTable.tsx

@@ -22,34 +22,34 @@ const UserTable = (props: UserTableProps) => {
   const { adminUsersContainer } = props;
   const { adminUsersContainer } = props;
 
 
   const getUserStatusLabel = (userStatus: number) => {
   const getUserStatusLabel = (userStatus: number) => {
-    let additionalClassName = 'bg-info';
+    let additionalClassName = 'text-bg-info';
     let text = 'Approval Pending';
     let text = 'Approval Pending';
 
 
     switch (userStatus) {
     switch (userStatus) {
       case 1:
       case 1:
-        additionalClassName = 'bg-info';
+        additionalClassName = 'text-bg-info';
         text = 'Approval Pending';
         text = 'Approval Pending';
         break;
         break;
       case 2:
       case 2:
-        additionalClassName = 'bg-success';
+        additionalClassName = 'text-bg-success';
         text = 'Active';
         text = 'Active';
         break;
         break;
       case 3:
       case 3:
-        additionalClassName = 'bg-warning text-dark';
+        additionalClassName = 'text-bg-warning';
         text = 'Suspended';
         text = 'Suspended';
         break;
         break;
       case 4:
       case 4:
-        additionalClassName = 'bg-danger';
+        additionalClassName = 'text-bg-danger';
         text = 'Deleted';
         text = 'Deleted';
         break;
         break;
       case 5:
       case 5:
-        additionalClassName = 'bg-pink';
+        additionalClassName = 'text-bg-secondary';
         text = 'Invited';
         text = 'Invited';
         break;
         break;
     }
     }
 
 
     return (
     return (
-      <span className={`badge rounded-pill ${additionalClassName}`}>
+      <span className={`badge ${additionalClassName}`}>
         {text}
         {text}
       </span>
       </span>
     );
     );
@@ -153,12 +153,12 @@ const UserTable = (props: UserTableProps) => {
                 <td>
                 <td>
                   {getUserStatusLabel(user.status)}
                   {getUserStatusLabel(user.status)}
                   {(user.admin) && (
                   {(user.admin) && (
-                    <span className="badge bg-indigo rounded-pill ms-2">
+                    <span className="badge text-bg-secondary ms-2">
                       {t('admin:user_management.user_table.administrator')}
                       {t('admin:user_management.user_table.administrator')}
                     </span>
                     </span>
                   )}
                   )}
                   {(user.readOnly) && (
                   {(user.readOnly) && (
-                    <span className="badge bg-light text-dark rounded-pill ms-2">
+                    <span className="badge text-bg-light ms-2">
                       {t('admin:user_management.user_table.read_only')}
                       {t('admin:user_management.user_table.read_only')}
                     </span>
                     </span>
                   )}
                   )}

+ 5 - 2
apps/app/src/components/Comments.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef } from 'react';
 
 
 import type { IRevisionHasId } from '@growi/core';
 import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
@@ -13,7 +14,6 @@ import { useCurrentUser } from '../stores/context';
 
 
 import type { CommentEditorProps } from './PageComment/CommentEditor';
 import type { CommentEditorProps } from './PageComment/CommentEditor';
 
 
-
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 
 
 
 
@@ -33,6 +33,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     pageId, pagePath, revision, onLoaded,
     pageId, pagePath, revision, onLoaded,
   } = props;
   } = props;
 
 
+  const { t } = useTranslation('');
+
   const { mutate } = useSWRxPageComment(pageId);
   const { mutate } = useSWRxPageComment(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: isDeleted } = useIsTrashPage();
@@ -69,7 +71,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   };
   };
 
 
   return (
   return (
-    <div className="page-comments-row mt-5 py-4 border-top border-3 d-edit-none d-print-none">
+    <div className="page-comments-row mt-5 py-4 border-top d-edit-none d-print-none">
+      <h4 className="mb-3">{t('page_comment.comments')}</h4>
       <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
       <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
         <PageComment
         <PageComment
           pageId={pageId}
           pageId={pageId}

+ 17 - 0
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss

@@ -36,3 +36,20 @@
     @include btn-muted.colorize(bs.$orange);
     @include btn-muted.colorize(bs.$orange);
   }
   }
 }
 }
+
+// == Colors
+.grw-former-link :global {
+  .separator {
+    opacity: 0.75;
+  }
+}
+
+.grw-former-link a {
+  --bs-link-opacity: 0.75;
+
+  &:global {
+    &:hover {
+      --bs-link-opacity: 1;
+    }
+  }
+}

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

@@ -29,8 +29,8 @@ type Props = {
 
 
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 
 
-const Separator = (): JSX.Element => {
-  return <span className={styles['grw-mx-02em']}>/</span>;
+const Separator = ({ className }: {className?: string}): JSX.Element => {
+  return <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>;
 };
 };
 
 
 export const PagePathNav: FC<Props> = (props: Props) => {
 export const PagePathNav: FC<Props> = (props: Props) => {
@@ -69,10 +69,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     formerLink = (
     formerLink = (
-      <div className="fs-5">
+      <>
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <Separator />
         <Separator />
-      </div>
+      </>
     );
     );
     latterLink = (
     latterLink = (
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
@@ -83,7 +83,7 @@ export const PagePathNav: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <div>
     <div>
-      <span className={formerLinkClassName}>{formerLink}</span>
+      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
       <div className="d-flex align-items-center">
       <div className="d-flex align-items-center">
         <h1 className={`m-0 ${latterLinkClassName}`}>
         <h1 className={`m-0 ${latterLinkClassName}`}>
           {latterLink}
           {latterLink}

+ 2 - 1
apps/app/src/components/CompleteUserRegistration.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 

+ 1 - 1
apps/app/src/components/DescendantsPageListModal.tsx

@@ -75,7 +75,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
         expandWindow={() => setIsWindowExpanded(true)}
         expandWindow={() => setIsWindowExpanded(true)}
         contractWindow={() => setIsWindowExpanded(false)}
         contractWindow={() => setIsWindowExpanded(false)}
       />
       />
-      <button type="button" className="btn btn-close" onClick={close} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
     </span>
     </span>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 

+ 8 - 0
apps/app/src/components/ExpandOrContractButton.module.scss

@@ -0,0 +1,8 @@
+.btn-expand-or-contract {
+  padding: 3px;
+  opacity: .5;
+
+  &:hover {
+    opacity: .75;
+  }
+}

+ 9 - 2
apps/app/src/components/ExpandOrContractButton.tsx

@@ -1,12 +1,17 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React from 'react';
 import React from 'react';
 
 
+import styles from './ExpandOrContractButton.module.scss';
+
 type Props = {
 type Props = {
   isWindowExpanded: boolean,
   isWindowExpanded: boolean,
   contractWindow?: () => void,
   contractWindow?: () => void,
   expandWindow?: () => void,
   expandWindow?: () => void,
 };
 };
 
 
+const moduleClass = styles['btn-expand-or-contract'] ?? '';
+
+
 const ExpandOrContractButton: FC<Props> = (props: Props) => {
 const ExpandOrContractButton: FC<Props> = (props: Props) => {
   const { isWindowExpanded, contractWindow, expandWindow } = props;
   const { isWindowExpanded, contractWindow, expandWindow } = props;
 
 
@@ -25,10 +30,12 @@ const ExpandOrContractButton: FC<Props> = (props: Props) => {
   return (
   return (
     <button
     <button
       type="button"
       type="button"
-      className="btn material-symbols-outlined"
+      className={`btn ${moduleClass}`}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
     >
     >
-      {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
+      <span className="material-symbols-outlined fw-bold">
+        {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
+      </span>
     </button>
     </button>
   );
   );
 };
 };

+ 3 - 1
apps/app/src/components/FontFamily/use-growi-custom-icons.tsx

@@ -1,9 +1,11 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const growiCustomIconFont = localFont({
 const growiCustomIconFont = localFont({
   src: '../../../../../packages/custom-icons/dist/growi-custom-icons.woff2',
   src: '../../../../../packages/custom-icons/dist/growi-custom-icons.woff2',
+  adjustFontFallback: false,
+  display: 'block',
 });
 });
 
 
 export const useGrowiCustomIcon: DefineStyle = () => (
 export const useGrowiCustomIcon: DefineStyle = () => (

+ 1 - 1
apps/app/src/components/FontFamily/use-lato.tsx

@@ -1,6 +1,6 @@
 import { Lato } from 'next/font/google';
 import { Lato } from 'next/font/google';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const lato = Lato({
 const lato = Lato({
   weight: ['400', '700'],
   weight: ['400', '700'],

+ 2 - 1
apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx

@@ -1,10 +1,11 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const materialSymbolsOutlined = localFont({
 const materialSymbolsOutlined = localFont({
   src: '../../../resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2',
   src: '../../../resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2',
   adjustFontFallback: false,
   adjustFontFallback: false,
+  display: 'block',
 });
 });
 
 
 export const useMaterialSymbolsOutlined: DefineStyle = () => (
 export const useMaterialSymbolsOutlined: DefineStyle = () => (

+ 1 - 1
apps/app/src/components/FontFamily/use-source-han-code-jp.tsx

@@ -1,6 +1,6 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const sourceHanCodeJPSubsetMain = localFont({
 const sourceHanCodeJPSubsetMain = localFont({
   src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',
   src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',

+ 14 - 0
apps/app/src/components/Layout/RawLayout.module.scss

@@ -0,0 +1,14 @@
+.grw-toast-container {
+  --toastify-color-info: var(--bs-info);
+  --toastify-color-success: var(--bs-success);
+  --toastify-color-warning: var(--bs-warning);
+  --toastify-color-error: var(--bs-danger);
+  --toastify-icon-color-info: var(--toastify-color-success);
+  --toastify-icon-color-success: var(--toastify-color-success);
+  --toastify-icon-color-warning: var(--toastify-color-warning);
+  --toastify-icon-color-error: var(--toastify-color-error);
+  --toastify-color-progress-info: var(--toastify-color-success);
+  --toastify-color-progress-success: var(--toastify-color-success);
+  --toastify-color-progress-warning: var(--toastify-color-warning);
+  --toastify-color-progress-error: var(--toastify-color-error);
+}

+ 8 - 3
apps/app/src/components/Layout/RawLayout.tsx

@@ -1,6 +1,7 @@
-import React, { ReactNode, useState } from 'react';
+import type { ReactNode } from 'react';
+import React, { useState } from 'react';
 
 
-import { ColorScheme } from '@growi/core';
+import type { ColorScheme } from '@growi/core';
 import Head from 'next/head';
 import Head from 'next/head';
 import { ToastContainer } from 'react-toastify';
 import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
@@ -9,6 +10,10 @@ import { useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
+import styles from './RawLayout.module.scss';
+
+const toastContainerClass = styles['grw-toast-container'] ?? '';
+
 const logger = loggerFactory('growi:cli:RawLayout');
 const logger = loggerFactory('growi:cli:RawLayout');
 
 
 
 
@@ -41,7 +46,7 @@ export const RawLayout = ({ children, className }: Props): JSX.Element => {
       <NextThemesProvider>
       <NextThemesProvider>
         <div className={classNames.join(' ')}>
         <div className={classNames.join(' ')}>
           {children}
           {children}
-          <ToastContainer theme={colorScheme} />
+          <ToastContainer className={toastContainerClass} theme={colorScheme} />
         </div>
         </div>
       </NextThemesProvider>
       </NextThemesProvider>
     </>
     </>

+ 44 - 0
apps/app/src/components/LoginForm/ExternalAuthButton.tsx

@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+
+import { IExternalAuthProviderType } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+const authIcon = {
+  [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
+  [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
+  [IExternalAuthProviderType.facebook]: <span className="growi-custom-icons align-bottom">facebook</span>,
+  [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
+  [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
+};
+
+const authLabel = {
+  [IExternalAuthProviderType.google]: 'Google',
+  [IExternalAuthProviderType.github]: 'GitHub',
+  [IExternalAuthProviderType.facebook]: 'Facebook',
+  [IExternalAuthProviderType.oidc]: 'OIDC',
+  [IExternalAuthProviderType.saml]: 'SAML',
+};
+
+
+export const ExternalAuthButton = ({ authType }: {authType: IExternalAuthProviderType}): JSX.Element => {
+  const { t } = useTranslation();
+
+  const key = `btn-auth-${authType.toString()}`;
+  const btnClass = `btn-auth-${authType.toString()}`;
+
+  const handleLoginWithExternalAuth = useCallback(() => {
+    window.location.href = `/passport/${authType.toString()}`;
+  }, [authType]);
+
+  return (
+    <button
+      key={key}
+      type="button"
+      className={`btn btn-secondary ${btnClass} my-2 col-10 col-sm-7 mx-auto d-flex`}
+      onClick={handleLoginWithExternalAuth}
+    >
+      <span>{authIcon[authType]}</span>
+      <span className="flex-grow-1">{t('Sign in with External auth', { signin: authLabel[authType] })}</span>
+    </button>
+  );
+};

+ 9 - 6
apps/app/src/components/LoginForm.module.scss → apps/app/src/components/LoginForm/LoginForm.module.scss

@@ -3,12 +3,15 @@
 @use '~/styles/atoms/placeholders/buttons';
 @use '~/styles/atoms/placeholders/buttons';
 
 
 .login-form :global {
 .login-form :global {
-  // To adjust the behavior, this problem is not solved.
-  // See https://github.com/AaronCCWong/react-card-flip/issues/56
-  .react-card-front,
-  .react-card-back {
-    height: 0% !important;
-  }
+  //
+  // deactivated in order to fix https://redmine.weseek.co.jp/issues/143531 -- 2024.04.02 Yuki Takei
+  //
+  // // To adjust the behavior, this problem is not solved.
+  // // See https://github.com/AaronCCWong/react-card-flip/issues/56
+  // .react-card-front,
+  // .react-card-back {
+  //   height: 0% !important;
+  // }
 
 
   .collapse-external-auth {
   .collapse-external-auth {
     overflow: hidden;
     overflow: hidden;

+ 15 - 48
apps/app/src/components/LoginForm.tsx → apps/app/src/components/LoginForm/LoginForm.tsx

@@ -2,6 +2,7 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
+import type { IExternalAuthProviderType } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -14,12 +15,15 @@ import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
-import { CompleteUserRegistration } from './CompleteUserRegistration';
+import { CompleteUserRegistration } from '../CompleteUserRegistration';
+
+import { ExternalAuthButton } from './ExternalAuthButton';
 
 
 import styles from './LoginForm.module.scss';
 import styles from './LoginForm.module.scss';
 
 
 const moduleClass = styles['login-form'];
 const moduleClass = styles['login-form'];
 
 
+
 type LoginFormProps = {
 type LoginFormProps = {
   username?: string,
   username?: string,
   name?: string,
   name?: string,
@@ -31,7 +35,7 @@ type LoginFormProps = {
   isLocalStrategySetup: boolean,
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
   isLdapSetupFailed: boolean,
-  objOfIsExternalAuthEnableds?: any,
+  enabledExternalAuthType?: IExternalAuthProviderType[],
   isMailerSetup?: boolean,
   isMailerSetup?: boolean,
   externalAccountLoginError?: IExternalAccountLoginError,
   externalAccountLoginError?: IExternalAccountLoginError,
 }
 }
@@ -42,10 +46,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
   const {
   const {
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, objOfIsExternalAuthEnableds,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType,
   } = props;
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-  const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+  const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
 
 
   // states
   // states
   const [isRegistering, setIsRegistering] = useState(false);
   const [isRegistering, setIsRegistering] = useState(false);
@@ -81,12 +85,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return t(key);
     return t(key);
   }, [t]);
   }, [t]);
 
 
-  const handleLoginWithExternalAuth = useCallback((e) => {
-    const auth = e.currentTarget.id;
-
-    window.location.href = `/passport/${auth}`;
-  }, []);
-
   const resetLoginErrors = useCallback(() => {
   const resetLoginErrors = useCallback(() => {
     if (loginErrors.length === 0) return;
     if (loginErrors.length === 0) return;
     setLoginErrors([]);
     setLoginErrors([]);
@@ -265,38 +263,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   ]);
   ]);
 
 
 
 
-  const renderExternalAuthInput = useCallback((auth) => {
-    const authIcon = {
-      google: <span className="growi-custom-icons align-bottom">google</span>,
-      github: <span className="growi-custom-icons align-bottom">github</span>,
-      facebook: <span className="growi-custom-icons align-bottom">facebook</span>,
-      oidc: <span className="growi-custom-icons align-bottom">openid</span>,
-      saml: <span className="material-symbols-outlined align-bottom">key</span>,
-    };
-    const authBtn = `btn-auth-${auth}`;
-    const signin = {
-      google: 'Google',
-      github: 'GitHub',
-      facebook: 'Facebook',
-      oidc: 'OIDC',
-      saml: 'SAML',
-    };
-
-    return (
-      <button
-        key={`btn-auth-${auth}`}
-        type="button"
-        className={`btn btn-secondary ${authBtn} my-2 col-10 col-sm-7 mx-auto d-flex`}
-        onClick={handleLoginWithExternalAuth}
-      >
-        <span>{authIcon[auth]}</span>
-        <span className="flex-grow-1">{t('Sign in with External auth', { signin: signin[auth] })}</span>
-      </button>
-    );
-  }, [handleLoginWithExternalAuth, t]);
-
   const renderExternalAuthLoginForm = useCallback(() => {
   const renderExternalAuthLoginForm = useCallback(() => {
-    const { objOfIsExternalAuthEnableds } = props;
+    const { enabledExternalAuthType } = props;
+
+    if (enabledExternalAuthType == null) {
+      return <></>;
+    }
 
 
     return (
     return (
       <>
       <>
@@ -304,16 +276,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           <p className="text-white mb-0">{t('or')}</p>
           <p className="text-white mb-0">{t('or')}</p>
         </div>
         </div>
         <div className="mt-2">
         <div className="mt-2">
-          {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
-            if (!objOfIsExternalAuthEnableds[auth]) {
-              return;
-            }
-            return renderExternalAuthInput(auth);
-          })}
+          { enabledExternalAuthType.map(authType => <ExternalAuthButton authType={authType} />) }
         </div>
         </div>
       </>
       </>
     );
     );
-  }, [props, t, renderExternalAuthInput]);
+  }, [props, t]);
 
 
   const resetRegisterErrors = useCallback(() => {
   const resetRegisterErrors = useCallback(() => {
     if (registerErrors.length === 0) return;
     if (registerErrors.length === 0) return;

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

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

+ 1 - 1
apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -75,7 +75,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
         expandWindow={() => setIsWindowExpanded(true)}
         expandWindow={() => setIsWindowExpanded(true)}
         contractWindow={() => setIsWindowExpanded(false)}
         contractWindow={() => setIsWindowExpanded(false)}
       />
       />
-      <button type="button" className="btn btn-close" onClick={close} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
     </span>
     </span>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 

+ 29 - 2
apps/app/src/components/PageComment.module.scss

@@ -9,8 +9,7 @@
 
 
   // reply button
   // reply button
   .btn-comment-reply {
   .btn-comment-reply {
-    margin-top: 0.5em;
-    border: none;
+    backdrop-filter: blur(10px);
   }
   }
 
 
   // TODO: Refacotr Soft-coding
   // TODO: Refacotr Soft-coding
@@ -21,3 +20,31 @@
     border: none;
     border: none;
   }
   }
 }
 }
+
+// // Light mode color
+@include bs.color-mode(light) {
+  .page-comment-styles :global {
+    .btn-comment-reply {
+      --bs-btn-color: #{( bs.$gray-600 )};
+      --bs-btn-hover-color:  #{( bs.$gray-700 )};
+      --bs-btn-bg: #{rgba( bs.$gray-200, 0.3 )};
+      --bs-btn-hover-bg: #{( bs.$gray-200 )};
+      --bs-btn-border-color: #{( bs.$gray-200 )};
+      --bs-btn-hover-border-color: #{( bs.$gray-300 )};
+    }
+  }
+}
+
+// dark mode color
+@include bs.color-mode(dark) {
+  .page-comment-styles :global {
+    .btn-comment-reply {
+      --bs-btn-color: #{( bs.$gray-500 )};
+      --bs-btn-hover-color:  #{( bs.$gray-400 )};
+      --bs-btn-bg: #{rgba( bs.$gray-800, 0.3 )};
+      --bs-btn-hover-bg: #{( bs.$gray-800 )};
+      --bs-btn-border-color: #{( bs.$gray-700 )};
+      --bs-btn-hover-border-color: #{( bs.$gray-600 )};
+    }
+  }
+}

+ 14 - 14
apps/app/src/components/PageComment.tsx

@@ -1,17 +1,18 @@
+import type { FC } from 'react';
 import React, {
 import React, {
-  FC, useState, useMemo, memo, useCallback,
+  useState, useMemo, memo, useCallback,
 } from 'react';
 } from 'react';
 
 
 import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
 import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
-import { Button } from 'reactstrap';
+import { UserPicture } from '@growi/ui/dist/components';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { RendererOptions } from '~/interfaces/renderer-options';
+import type { RendererOptions } from '~/interfaces/renderer-options';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 
 
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+import type { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { NotAvailableForGuest } from './NotAvailableForGuest';
@@ -153,10 +154,10 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   return (
   return (
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
       <div className="page-comments">
       <div className="page-comments">
-        <div className="page-comments-list" id="page-comments-list">
+        <div className="page-comments-list mb-3" id="page-comments-list">
           {commentsExceptReply.map((comment) => {
           {commentsExceptReply.map((comment) => {
 
 
-            const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+            const defaultCommentThreadClasses = 'page-comment-thread mb-2';
             const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
             const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
 
 
             let commentThreadClasses = '';
             let commentThreadClasses = '';
@@ -170,16 +171,15 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                   <div className="d-flex flex-row-reverse">
                   <div className="d-flex flex-row-reverse">
                     <NotAvailableForGuest>
                     <NotAvailableForGuest>
                       <NotAvailableForReadOnlyUser>
                       <NotAvailableForReadOnlyUser>
-                        <Button
-                          data-testid="comment-reply-button"
-                          outline
-                          color="secondary"
-                          size="sm"
-                          className="btn-comment-reply"
+                        <button
+                          type="button"
+                          id="comment-reply-button"
+                          className="btn btn-secondary btn-comment-reply text-start w-100 ms-5"
                           onClick={() => onReplyButtonClickHandler(comment._id)}
                           onClick={() => onReplyButtonClickHandler(comment._id)}
                         >
                         >
-                          <span className="material-symbols-outlined">replay</span> Reply
-                        </Button>
+                          <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-2" />
+                          <span className="material-symbols-outlined me-1 fs-5 pb-1">reply</span><small>Reply...</small>
+                        </button>
                       </NotAvailableForReadOnlyUser>
                       </NotAvailableForReadOnlyUser>
                     </NotAvailableForGuest>
                     </NotAvailableForGuest>
                   </div>
                   </div>

+ 32 - 23
apps/app/src/components/PageComment/Comment.module.scss

@@ -4,11 +4,6 @@
 @use './_comment-inheritance';
 @use './_comment-inheritance';
 
 
 .comment-styles :global {
 .comment-styles :global {
-  .page-comment-writer {
-    @include bs.media-breakpoint-down(xs) {
-      height: 3.5em;
-    }
-  }
 
 
   .page-comment {
   .page-comment {
     position: relative;
     position: relative;
@@ -16,11 +11,9 @@
 
 
     scroll-margin-top: var.$grw-scroll-margin-top-in-view;
     scroll-margin-top: var.$grw-scroll-margin-top-in-view;
 
 
-    // user name
-    .page-comment-creator {
-      margin-top: -0.5em;
-      margin-bottom: 0.5em;
-      font-weight: bold;
+    // background
+    .bg-comment {
+      @extend %bg-comment;
     }
     }
 
 
     // user icon
     // user icon
@@ -31,14 +24,6 @@
     // comment section
     // comment section
     .page-comment-main {
     .page-comment-main {
       @extend %comment-section;
       @extend %comment-section;
-      @include bs.media-breakpoint-up(sm) {
-        margin-left: 4.5em;
-      }
-      @include bs.media-breakpoint-down(xs) {
-        &:before {
-          content: none;
-        }
-      }
 
 
       pointer-events: auto;
       pointer-events: auto;
 
 
@@ -57,8 +42,10 @@
 
 
     // comment body
     // comment body
     .page-comment-body {
     .page-comment-body {
-      margin-bottom: 0.5em;
       word-wrap: break-word;
       word-wrap: break-word;
+      .wiki p {
+        margin: 8px 0;
+      }
     }
     }
 
 
     // older comments
     // older comments
@@ -73,6 +60,14 @@
       }
       }
     }
     }
 
 
+    .page-comment-revision {
+      .material-symbols-outlined {
+        font-size: 16px;
+        vertical-align: middle;
+      }
+    }
+
+
     .page-comment-meta {
     .page-comment-meta {
       display: flex;
       display: flex;
       justify-content: flex-end;
       justify-content: flex-end;
@@ -81,10 +76,6 @@
       color: bs.$gray-400;
       color: bs.$gray-400;
     }
     }
 
 
-    .page-comment-revision svg {
-      width: 16px;
-      height: 16px;
-    }
   }
   }
 
 
   // TODO: Refacotr Soft-coding
   // TODO: Refacotr Soft-coding
@@ -98,3 +89,21 @@
     }
     }
   }
   }
 }
 }
+
+// // Light mode color
+@include bs.color-mode(light) {
+  .comment-styles :global {
+    .page-comment-revision {
+      color: bs.$gray-500;
+    }
+  }
+}
+
+// // Dark mode color
+@include bs.color-mode(dark) {
+  .comment-styles :global {
+    .page-comment-revision {
+      color: bs.$gray-600;
+    }
+  }
+}

+ 16 - 20
apps/app/src/components/PageComment/Comment.tsx

@@ -111,10 +111,6 @@ export const Comment = (props: CommentProps): JSX.Element => {
     deleteBtnClicked(comment);
     deleteBtnClicked(comment);
   };
   };
 
 
-  const renderText = (comment: string) => {
-    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
-  };
-
   const commentBody = useMemo(() => {
   const commentBody = useMemo(() => {
     if (rendererOptions == null) {
     if (rendererOptions == null) {
       return <></>;
       return <></>;
@@ -151,24 +147,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
         />
         />
       ) : (
       ) : (
         <div id={commentId} className={rootClassName}>
         <div id={commentId} className={rootClassName}>
-          <div className="page-comment-writer">
-            <UserPicture user={creator} />
-          </div>
-          <div className="page-comment-main">
-            <div className="page-comment-creator">
-              <Username user={creator} />
-            </div>
-            <div className="page-comment-body">{commentBody}</div>
-            <div className="page-comment-meta">
-              <Link href={`#${commentId}`} prefetch={false}>
+          <div className="page-comment-main bg-comment rounded mb-2">
+            <div className="d-flex align-items-center">
+              <UserPicture user={creator} additionalClassName="me-2" />
+              <div className="small fw-bold me-3">
+                <Username user={creator} />
+              </div>
+              <Link href={`#${commentId}`} prefetch={false} className="small page-comment-revision">
                 <FormattedDistanceDate id={commentId} date={comment.createdAt} />
                 <FormattedDistanceDate id={commentId} date={comment.createdAt} />
               </Link>
               </Link>
-              { isEdited && (
-                <>
-                  <span id={editedDateId}>&nbsp;(edited)</span>
-                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
-                </>
-              ) }
               <span className="ms-2">
               <span className="ms-2">
                 <Link
                 <Link
                   id={`page-comment-revision-${commentId}`}
                   id={`page-comment-revision-${commentId}`}
@@ -183,6 +170,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
                 </UncontrolledTooltip>
                 </UncontrolledTooltip>
               </span>
               </span>
             </div>
             </div>
+            <div className="page-comment-body">{commentBody}</div>
+            <div className="page-comment-meta">
+              { isEdited && (
+                <>
+                  <span id={editedDateId}>&nbsp;(edited)</span>
+                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
+                </>
+              ) }
+            </div>
             { (isCurrentUserEqualsToAuthor() && !isReadOnly) && (
             { (isCurrentUserEqualsToAuthor() && !isReadOnly) && (
               <CommentControl
               <CommentControl
                 onClickDeleteBtn={deleteBtnClickedHandler}
                 onClickDeleteBtn={deleteBtnClickedHandler}

+ 2 - 2
apps/app/src/components/PageComment/CommentControl.tsx

@@ -13,13 +13,13 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
   return (
   return (
     // The page-comment-control class is imported from Comment.module.scss
     // The page-comment-control class is imported from Comment.module.scss
     <div className="page-comment-control">
     <div className="page-comment-control">
-      <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
+      <button type="button" className="btn btn-link p-2 opacity-50" onClick={onClickEditBtn}>
         <span className="material-symbols-outlined">edit</span>
         <span className="material-symbols-outlined">edit</span>
       </button>
       </button>
       <button
       <button
         data-testid="comment-delete-button"
         data-testid="comment-delete-button"
         type="button"
         type="button"
-        className="btn btn-link p-2 me-2"
+        className="btn btn-link p-2 me-2 opacity-50"
         onClick={onClickDeleteBtn}
         onClick={onClickDeleteBtn}
       >
       >
         <span className="material-symbols-outlined">close</span>
         <span className="material-symbols-outlined">close</span>

+ 21 - 20
apps/app/src/components/PageComment/CommentEditor.module.scss

@@ -4,34 +4,35 @@
 
 
 // display cheatsheet for comment form only
 // display cheatsheet for comment form only
 .comment-editor-styles :global {
 .comment-editor-styles :global {
-  .cm-editor {
-    height: 300px !important;
-  }
-
   .comment-form {
   .comment-form {
     position: relative;
     position: relative;
-    margin-top: 1em;
+
+    // background
+    .bg-comment {
+      @extend %bg-comment
+    }
 
 
     // user icon
     // user icon
     .picture {
     .picture {
       @extend %picture;
       @extend %picture;
     }
     }
 
 
-    // seciton
-    .comment-form-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      @include bs.media-breakpoint-down(xs) {
-        margin-left: 3.5em;
-      }
-    }
+  }
+}
 
 
-    // textarea
-    .comment-write {
-      margin-bottom: 0.5em;
-    }
-    .comment-form-preview {
-      padding-top: 0.5em;
-    }
+
+// adjust height
+.comment-editor-styles :global {
+  .cm-editor {
+    min-height: comment-inheritance.$codemirror-default-height !important;
+    max-height: #{2 * comment-inheritance.$codemirror-default-height};
+  }
+  .cm-gutters {
+    min-height: comment-inheritance.$codemirror-default-height !important;
+  }
+  .comment-preview-container {
+    min-height: page-editor-inheritance.$navbar-editor-height
+      + comment-inheritance.$codemirror-default-height;
+    padding-top: 0.5em;
   }
   }
 }
 }

+ 50 - 89
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -6,13 +6,14 @@ import {
   CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
   CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import {
 import {
-  Button, TabContent, TabPane,
+  TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
@@ -26,11 +27,12 @@ import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 
 import { CommentPreview } from './CommentPreview';
 import { CommentPreview } from './CommentPreview';
+import { SwitchingButtonGroup } from './SwitchingButtonGroup';
+
 
 
 import '@growi/editor/dist/style.css';
 import '@growi/editor/dist/style.css';
 import styles from './CommentEditor.module.scss';
 import styles from './CommentEditor.module.scss';
@@ -41,18 +43,6 @@ const logger = loggerFactory('growi:components:CommentEditor');
 
 
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 
 
-
-const navTabMapping = {
-  comment_editor: {
-    Icon: () => <span className="material-symbols-outlined">edit_square</span>,
-    i18n: 'Write',
-  },
-  comment_preview: {
-    Icon: () => <span className="material-symbols-outlined">play_arrow</span>,
-    i18n: 'Preview',
-  },
-};
-
 export type CommentEditorProps = {
 export type CommentEditorProps = {
   pageId: string,
   pageId: string,
   isForNewComment?: boolean,
   isForNewComment?: boolean,
@@ -92,11 +82,13 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');
   const [comment, setComment] = useState(commentBody ?? '');
-  const [activeTab, setActiveTab] = useState('comment_editor');
+  const [showPreview, setShowPreview] = useState(false);
   const [error, setError] = useState();
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [incremented, setIncremented] = useState(false);
   const [incremented, setIncremented] = useState(false);
 
 
+  const { t } = useTranslation('');
+
   const editorRef = useRef<IEditorMethods>(null);
   const editorRef = useRef<IEditorMethods>(null);
 
 
   const router = useRouter();
   const router = useRouter();
@@ -113,8 +105,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     };
     };
   }, [onRouterChangeComplete, router.events]);
   }, [onRouterChangeComplete, router.events]);
 
 
-  const handleSelect = useCallback((activeTab: string) => {
-    setActiveTab(activeTab);
+  const handleSelect = useCallback((showPreview: boolean) => {
+    setShowPreview(showPreview);
   }, []);
   }, []);
 
 
   // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
   // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
@@ -140,7 +132,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
     const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
 
 
     setComment('');
     setComment('');
-    setActiveTab('comment_editor');
+    setShowPreview(false);
     setError(undefined);
     setError(undefined);
     initializeSlackEnabled();
     initializeSlackEnabled();
     // reset value
     // reset value
@@ -209,40 +201,21 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   // the upload event handler
   // the upload event handler
   const uploadHandler = useCallback((files: File[]) => {
   const uploadHandler = useCallback((files: File[]) => {
-    files.forEach(async(file) => {
-      try {
-        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
-
-        if (!resLimit.isUploadable) {
-          throw new Error(resLimit.errorMessage);
-        }
-
-        const formData = new FormData();
-        formData.append('file', file);
-        if (pageId != null) {
-          formData.append('page_id', pageId);
-        }
-
-        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
-
-        const attachment = resAdd.attachment;
+    uploadAttachments(pageId, files, {
+      onUploaded: (attachment) => {
         const fileName = attachment.originalName;
         const fileName = attachment.originalName;
 
 
-        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
+        const prefix = attachment.fileFormat.startsWith('image/')
+          ? '!' // use "![fileName](url)" syntax when image
+          : '';
+        const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`;
 
 
         codeMirrorEditor?.insertText(insertText);
         codeMirrorEditor?.insertText(insertText);
-      }
-      catch (e) {
-        logger.error('failed to upload', e);
-        toastError(e);
-      }
+      },
+      onError: (error) => {
+        toastError(error);
+      },
     });
     });
-
   }, [codeMirrorEditor, pageId]);
   }, [codeMirrorEditor, pageId]);
 
 
   const getCommentHtml = useCallback(() => {
   const getCommentHtml = useCallback(() => {
@@ -255,22 +228,24 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const renderBeforeReady = useCallback((): JSX.Element => {
   const renderBeforeReady = useCallback((): JSX.Element => {
     return (
     return (
-      <div className="text-center">
+      <div>
         <NotAvailableForGuest>
         <NotAvailableForGuest>
           <NotAvailableForReadOnlyUser>
           <NotAvailableForReadOnlyUser>
             <button
             <button
               type="button"
               type="button"
-              className="btn btn-lg btn-link"
+              className="btn btn-outline-primary w-100 text-start py-3"
               onClick={() => setIsReadyToUse(true)}
               onClick={() => setIsReadyToUse(true)}
               data-testid="open-comment-editor-button"
               data-testid="open-comment-editor-button"
             >
             >
-              <span className="material-symbols-outlined">comment</span> Add Comment
+              <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-3" />
+              <span className="material-symbols-outlined me-1 fs-5">add_comment</span>
+              <small>Add Comment in markdown...</small>
             </button>
             </button>
           </NotAvailableForReadOnlyUser>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
         </NotAvailableForGuest>
       </div>
       </div>
     );
     );
-  }, []);
+  }, [currentUser]);
 
 
   // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
   // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
   //   setComment(newValue);
   //   setComment(newValue);
@@ -304,33 +279,36 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
     const errorMessage = <span className="text-danger text-end me-2">{error}</span>;
     const errorMessage = <span className="text-danger text-end me-2">{error}</span>;
     const cancelButton = (
     const cancelButton = (
-      <Button
-        outline
-        color="danger"
-        size="xs"
-        className="btn btn-outline-danger rounded-pill"
+      <button
+        type="button"
+        className="btn btn-outline-neutral-secondary"
         onClick={cancelButtonClickedHandler}
         onClick={cancelButtonClickedHandler}
       >
       >
-        Cancel
-      </Button>
+        {t('Cancel')}
+      </button>
     );
     );
     const submitButton = (
     const submitButton = (
-      <Button
+      <button
+        type="button"
         data-testid="comment-submit-button"
         data-testid="comment-submit-button"
-        outline
-        color="primary"
-        className="btn btn-outline-primary rounded-pill"
+        className="btn btn-primary"
         onClick={postCommentHandler}
         onClick={postCommentHandler}
       >
       >
-        Comment
-      </Button>
+        {t('page_comment.comment')}
+      </button>
     );
     );
 
 
     return (
     return (
       <>
       <>
-        <div className="comment-write">
-          <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
-          <TabContent activeTab={activeTab}>
+        <div className="px-4 pt-3 pb-1">
+          <div className="d-flex justify-content-between align-items-center mb-2">
+            <div className="d-flex">
+              <UserPicture user={currentUser} noLink noTooltip />
+              <p className="ms-2 mb-0">Add a comment</p>
+            </div>
+            <SwitchingButtonGroup showPreview={showPreview} onSelected={handleSelect} />
+          </div>
+          <TabContent activeTab={showPreview ? 'comment_preview' : 'comment_editor'}>
             <TabPane tabId="comment_editor">
             <TabPane tabId="comment_editor">
               <CodeMirrorEditorComment
               <CodeMirrorEditorComment
                 acceptedUploadFileType={acceptedUploadFileType}
                 acceptedUploadFileType={acceptedUploadFileType}
@@ -339,37 +317,23 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onUpload={uploadHandler}
                 onUpload={uploadHandler}
                 editorSettings={editorSettings}
                 editorSettings={editorSettings}
               />
               />
-              {/* <Editor
-                ref={editorRef}
-                value={commentBody ?? ''} // DO NOT use state
-                isUploadable={isUploadable}
-                isUploadAllFileAllowed={isUploadAllFileAllowed}
-                onChange={onChangeHandler}
-                onUpload={uploadHandler}
-                onCtrlEnter={ctrlEnterHandler}
-                isComment
-              /> */}
-              {/*
-                Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
-                See a review comment in https://github.com/weseek/growi/pull/3473
-              */}
             </TabPane>
             </TabPane>
             <TabPane tabId="comment_preview">
             <TabPane tabId="comment_preview">
-              <div className="comment-form-preview">
+              <div className="comment-preview-container">
                 {commentPreview}
                 {commentPreview}
               </div>
               </div>
             </TabPane>
             </TabPane>
           </TabContent>
           </TabContent>
         </div>
         </div>
 
 
-        <div className="comment-submit">
+        <div className="comment-submit px-4 pb-3 mb-2">
           <div className="d-flex">
           <div className="d-flex">
             <span className="flex-grow-1" />
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
             <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
 
 
             {isSlackConfigured && isSlackEnabled != null
             {isSlackConfigured && isSlackEnabled != null
               && (
               && (
-                <div className="align-self-center me-md-2">
+                <div className="align-self-center me-md-3">
                   <SlackNotification
                   <SlackNotification
                     isSlackEnabled={isSlackEnabled}
                     isSlackEnabled={isSlackEnabled}
                     slackChannels={slackChannels}
                     slackChannels={slackChannels}
@@ -398,10 +362,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   return (
   return (
     <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
     <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
       <div className="comment-form">
       <div className="comment-form">
-        <div className="comment-form-user">
-          <UserPicture user={currentUser} noLink noTooltip />
-        </div>
-        <div className="comment-form-main">
+        <div className="comment-form-main bg-comment rounded">
           {isReadyToUse
           {isReadyToUse
             ? renderReady()
             ? renderReady()
             : renderBeforeReady()
             : renderBeforeReady()

+ 0 - 7
apps/app/src/components/PageComment/CommentPreview.module.scss

@@ -1,9 +1,2 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-@use './comment-inheritance';
-@use '../PageEditor/page-editor-inheritance';
-
 .grw-comment-preview {
 .grw-comment-preview {
-  min-height: page-editor-inheritance.$navbar-editor-height
-    + comment-inheritance.$codemirror-default-height
-    + bs.$line-height-base;
 }
 }

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

@@ -5,6 +5,8 @@ import RevisionRenderer from '../Page/RevisionRenderer';
 
 
 import styles from './CommentPreview.module.scss';
 import styles from './CommentPreview.module.scss';
 
 
+const moduleClass = styles['grw-comment-preview'] ?? '';
+
 
 
 type CommentPreviewPorps = {
 type CommentPreviewPorps = {
   markdown: string,
   markdown: string,
@@ -21,7 +23,7 @@ export const CommentPreview = (props: CommentPreviewPorps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className={`grw-comment-preview ${styles['grw-comment-preview']}`}>
+    <div className={moduleClass}>
       <RevisionRenderer
       <RevisionRenderer
         rendererOptions={rendererOptions}
         rendererOptions={rendererOptions}
         markdown={markdown}
         markdown={markdown}

+ 0 - 6
apps/app/src/components/PageComment/ReplyComments.module.scss

@@ -1,9 +1,3 @@
-/*
-* reply
-*/
-.page-comment-reply :global {
-  margin-top: 1em;
-}
 
 
 // remove margin after hidden replies
 // remove margin after hidden replies
 .page-comments-hidden-replies + .page-comment-reply :global {
 .page-comments-hidden-replies + .page-comment-reply :global {

+ 1 - 1
apps/app/src/components/PageComment/ReplyComments.tsx

@@ -40,7 +40,7 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 
 
   const renderReply = (reply: ICommentHasId) => {
   const renderReply = (reply: ICommentHasId) => {
     return (
     return (
-      <div key={reply._id} className={`${styles['page-comment-reply']} ms-4 ms-sm-5 me-3`}>
+      <div key={reply._id} className={`${styles['page-comment-reply']} mt-2 ms-4 ms-sm-5`}>
         <Comment
         <Comment
           rendererOptions={rendererOptions}
           rendererOptions={rendererOptions}
           comment={reply}
           comment={reply}

+ 46 - 0
apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss

@@ -0,0 +1,46 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+
+.btn-group-switching :global {
+  .btn {
+    --bs-btn-border-width: 2px;
+
+    width: 60px;
+    height: 38px;
+
+    @include bs.media-breakpoint-up(sm) {
+      width: 90px;
+      height: 30px;
+    }
+  }
+}
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .btn-group-switching :global {
+    .btn {
+      $bg: var(--bs-gray-500);
+
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-bg: var(--bs-gray-100);
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-color: white;
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .btn-group-switching :global {
+    .btn {
+      $bg: var(--bs-gray-800);
+
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-bg: #{rgba(bs.$gray-600, 0.1)};
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+    }
+  }
+}

+ 73 - 0
apps/app/src/components/PageComment/SwitchingButtonGroup.tsx

@@ -0,0 +1,73 @@
+import type { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
+import { memo } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import styles from './SwitchingButtonGroup.module.scss';
+
+const moduleClass = styles['btn-group-switching'] ?? '';
+
+
+type SwitchingButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
+    active?: boolean,
+}
+
+const SwitchingButton = memo((props: SwitchingButtonProps) => {
+  const {
+    active, className, children, onClick, ...rest
+  } = props;
+
+  return (
+    <button
+      type="button"
+      className={`btn btn-sm py-1 d-flex align-items-center justify-content-center
+        ${className}
+        ${active ? 'active' : ''}
+      `}
+      onClick={onClick}
+      {...rest}
+    >
+      {children}
+    </button>
+  );
+});
+
+
+type Props = {
+  showPreview: boolean,
+  onSelected?: (showPreview: boolean) => void,
+};
+
+export const SwitchingButtonGroup = (props: Props): JSX.Element => {
+
+  const { t } = useTranslation();
+
+  const {
+    showPreview, onSelected,
+  } = props;
+
+  return (
+    <div
+      className={`btn-group ${moduleClass}`}
+      role="group"
+    >
+      <SwitchingButton
+        active={showPreview}
+        className="ps-2 pe-3"
+        onClick={() => onSelected?.(true)}
+      >
+        <span className="material-symbols-outlined me-0">play_arrow</span>
+        <span className="d-none d-sm-inline">{t('Preview')}</span>
+      </SwitchingButton>
+      <SwitchingButton
+        active={!showPreview}
+        className="px-2"
+        onClick={() => onSelected?.(false)}
+      >
+        <span className="material-symbols-outlined me-1">edit_square</span>
+        <span className="d-none d-sm-inline">{t('Write')}</span>
+      </SwitchingButton>
+    </div>
+  );
+
+};

+ 19 - 8
apps/app/src/components/PageComment/_comment-inheritance.scss

@@ -22,15 +22,26 @@
 }
 }
 
 
 %picture {
 %picture {
-  float: left;
-  width: 3em;
-  height: 3em;
-  margin-top: 0.8em;
+  width: 1.2em;
+  height: 1.2em;
+}
+
+$codemirror-default-height: 300px;
 
 
-  @include bs.media-breakpoint-down(xs) {
-    width: 2em;
-    height: 2em;
+// // Light mode color
+@include bs.color-mode(light) {
+  %bg-comment {
+    background-color: rgba( bs.$gray-200, 0.5 );
+    border: 1px solid bs.$gray-200;
+    backdrop-filter: blur(10px);
   }
   }
 }
 }
 
 
-$codemirror-default-height: 300px;
+// // Dark mode color
+@include bs.color-mode(dark) {
+  %bg-comment {
+    background-color: rgba( bs.$gray-800, 0.3 );
+    border: 1px solid bs.$gray-700;
+    backdrop-filter: blur(10px);
+  }
+}

+ 1 - 1
apps/app/src/components/PageControls/BookmarkButtons.tsx

@@ -81,7 +81,7 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
           </span>
           </span>
         </DropdownToggle>
         </DropdownToggle>
       </BookmarkFolderMenu>
       </BookmarkFolderMenu>
-      <UncontrolledTooltip placement="top" data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
+      <UncontrolledTooltip data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 

+ 1 - 1
apps/app/src/components/PageControls/LikeButtons.tsx

@@ -54,7 +54,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         <span className={`material-symbols-outlined ${isLiked ? 'fill' : ''}`}>favorite</span>
         <span className={`material-symbols-outlined ${isLiked ? 'fill' : ''}`}>favorite</span>
       </button>
       </button>
 
 
-      <UncontrolledTooltip data-testid="like-button-tooltip" placement="top" target="like-button" autohide={false} fade={false}>
+      <UncontrolledTooltip data-testid="like-button-tooltip" target="like-button" autohide={false} fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 

+ 1 - 2
apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -2,7 +2,6 @@ import type { FC } from 'react';
 import React, { useState } from 'react';
 import React, { useState } from 'react';
 
 
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
-import { FootstampIcon } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
 
@@ -40,7 +39,7 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
           </div>
           </div>
         </PopoverBody>
         </PopoverBody>
       </Popover>
       </Popover>
-      <UncontrolledTooltip data-testid="seen-user-info-tooltip" placement="top" target="btn-seen-user" fade={false}>
+      <UncontrolledTooltip data-testid="seen-user-info-tooltip" target="btn-seen-user" fade={false}>
         {t('tooltip.footprints')}
         {t('tooltip.footprints')}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
     </div>
     </div>

+ 3 - 2
apps/app/src/components/PageControls/SubscribeButton.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useCallback } from 'react';
 
 
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -41,7 +42,7 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         </span>
         </span>
       </button>
       </button>
 
 
-      <UncontrolledTooltip data-testid="subscribe-button-tooltip" placement="top" target="subscribe-button" fade={false}>
+      <UncontrolledTooltip data-testid="subscribe-button-tooltip" target="subscribe-button" fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
     </>
     </>

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

@@ -12,7 +12,7 @@ export const EditorNavbar = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
   const { data: editingUsers } = useEditingUsers();
 
 
   return (
   return (
-    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1 ms-3`}>
+    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
       <PageHeader />
       <PageHeader />
       <EditingUserList
       <EditingUserList
         userList={editingUsers?.userList ?? []}
         userList={editingUsers?.userList ?? []}

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

@@ -10,11 +10,11 @@ const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSel
 const EditorNavbarBottom = (): JSX.Element => {
 const EditorNavbarBottom = (): JSX.Element => {
   return (
   return (
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
-      <div className={`flex-expand-horiz align-items-center px-2 py-1 py-md-2 px-md-3 ${moduleClass}`}>
-        <form className="m-2 me-auto">
+      <div className={`flex-expand-horiz align-items-center p-2 ps-md-3 pe-md-4 ${moduleClass}`}>
+        <form className="me-auto">
           <OptionsSelector />
           <OptionsSelector />
         </form>
         </form>
-        <form className="m-2">
+        <form>
           <SavePageControls />
           <SavePageControls />
         </form>
         </form>
       </div>
       </div>

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

@@ -442,7 +442,7 @@ export const HandsontableModal = (): JSX.Element => {
         contractWindow={contractWindow}
         contractWindow={contractWindow}
         expandWindow={expandWindow}
         expandWindow={expandWindow}
       />
       />
-      <button type="button" className="btn btn-close" onClick={cancel} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={cancel} aria-label="Close"></button>
     </span>
     </span>
   );
   );
 
 
@@ -457,14 +457,14 @@ export const HandsontableModal = (): JSX.Element => {
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
       onOpened={handleModalOpen}
       onOpened={handleModalOpen}
     >
     >
-      <ModalHeader tag="h4" toggle={cancel} close={closeButton} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={cancel} close={closeButton}>
         {t('handsontable_modal.title')}
         {t('handsontable_modal.title')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="p-0 d-flex flex-column">
       <ModalBody className="p-0 d-flex flex-column">
-        <div className="grw-hot-modal-navbar px-4 py-3 border-bottom">
+        <div className="grw-hot-modal-navbar p-3">
           <button
           <button
             type="button"
             type="button"
-            className="me-4 data-import-button btn btn-secondary"
+            className="me-4 data-import-button btn btn-outline-neutral-secondary"
             data-bs-toggle="collapse"
             data-bs-toggle="collapse"
             data-bs-target="#collapseDataImport"
             data-bs-target="#collapseDataImport"
             aria-expanded={isDataImportAreaExpanded}
             aria-expanded={isDataImportAreaExpanded}
@@ -474,23 +474,23 @@ export const HandsontableModal = (): JSX.Element => {
             <span className="material-symbols-outlined">{isDataImportAreaExpanded ? 'expand_less' : 'expand_more'}</span>
             <span className="material-symbols-outlined">{isDataImportAreaExpanded ? 'expand_less' : 'expand_more'}</span>
           </button>
           </button>
           <div role="group" className="btn-group">
           <div role="group" className="btn-group">
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('l') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('l') }}>
               <span className="material-symbols-outlined">format_align_left</span>
               <span className="material-symbols-outlined">format_align_left</span>
             </button>
             </button>
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('c') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('c') }}>
               <span className="material-symbols-outlined">format_align_center</span>
               <span className="material-symbols-outlined">format_align_center</span>
             </button>
             </button>
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('r') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('r') }}>
               <span className="material-symbols-outlined">format_align_right</span>
               <span className="material-symbols-outlined">format_align_right</span>
             </button>
             </button>
           </div>
           </div>
           <Collapse isOpen={isDataImportAreaExpanded}>
           <Collapse isOpen={isDataImportAreaExpanded}>
-            <div className="mt-4">
+            <div className="py-3 border-bottom">
               <MarkdownTableDataImportForm onCancel={toggleDataImportArea} onImport={importData} />
               <MarkdownTableDataImportForm onCancel={toggleDataImportArea} onImport={importData} />
             </div>
             </div>
           </Collapse>
           </Collapse>
         </div>
         </div>
-        <div ref={c => setHotTableContainer(c)} className="m-4 hot-table-container">
+        <div ref={c => setHotTableContainer(c)} className="hot-table-container px-3">
           <HotTable
           <HotTable
             ref={c => setHotTable(c)}
             ref={c => setHotTable(c)}
             data={markdownTable.table}
             data={markdownTable.table}
@@ -507,9 +507,9 @@ export const HandsontableModal = (): JSX.Element => {
         </div>
         </div>
       </ModalBody>
       </ModalBody>
       <ModalFooter className="grw-modal-footer">
       <ModalFooter className="grw-modal-footer">
-        <button type="button" className="btn btn-danger" onClick={reset}>{t('commons:Reset')}</button>
+        <button type="button" className="btn btn-outline-danger" onClick={reset}>{t('commons:Reset')}</button>
         <div className="ms-auto">
         <div className="ms-auto">
-          <button type="button" className="me-2 btn btn-secondary" onClick={cancel}>{t('handsontable_modal.cancel')}</button>
+          <button type="button" className="me-2 btn btn-outline-neutral-secondary" onClick={cancel}>{t('handsontable_modal.cancel')}</button>
           <button type="button" className="btn btn-primary" onClick={save}>{t('handsontable_modal.done')}</button>
           <button type="button" className="btn btn-primary" onClick={save}>{t('handsontable_modal.done')}</button>
         </div>
         </div>
       </ModalFooter>
       </ModalFooter>

+ 3 - 3
apps/app/src/components/PageEditor/MarkdownTableDataImportForm.tsx

@@ -66,7 +66,7 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
           <option value="html">HTML</option>
           <option value="html">HTML</option>
         </select>
         </select>
       </div>
       </div>
-      <div>
+      <div className="mt-2">
         <label htmlFor="data-import-form-type-textarea" className="form-label">{t('import_data')}</label>
         <label htmlFor="data-import-form-type-textarea" className="form-label">{t('import_data')}</label>
         <textarea
         <textarea
           id="data-import-form-type-textarea"
           id="data-import-form-type-textarea"
@@ -88,8 +88,8 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
           />
           />
         </div>
         </div>
       </Collapse>
       </Collapse>
-      <div className="d-flex justify-content-end">
-        <Button color="secondary me-2" onClick={onCancel}>{t('cancel')}</Button>
+      <div className="mt-3 d-flex justify-content-end">
+        <Button color="outline-neutral-secondary me-2" onClick={onCancel}>{t('cancel')}</Button>
         <Button color="primary" onClick={importButtonHandler}>{t('import')}</Button>
         <Button color="primary" onClick={importButtonHandler}>{t('import')}</Button>
       </div>
       </div>
     </form>
     </form>

+ 31 - 34
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -21,7 +21,7 @@ import { throttle, debounce } from 'throttle-debounce';
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { updatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/services/update-page';
 import { updatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/services/update-page';
-import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import {
 import {
   useDefaultIndentSize, useCurrentUser,
   useDefaultIndentSize, useCurrentUser,
@@ -237,47 +237,30 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   // the upload event handler
   // the upload event handler
   const uploadHandler = useCallback((files: File[]) => {
   const uploadHandler = useCallback((files: File[]) => {
-    files.forEach(async(file) => {
-      try {
-        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
-
-        if (!resLimit.isUploadable) {
-          throw new Error(resLimit.errorMessage);
-        }
-
-        const formData = new FormData();
-        formData.append('file', file);
-        if (pageId != null) {
-          formData.append('page_id', pageId);
-        }
-
-        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
+    if (pageId == null) {
+      logger.error('pageId is invalid', {
+        pageId,
+      });
+      throw new Error('pageId is invalid');
+    }
 
 
-        const attachment = resAdd.attachment;
+    uploadAttachments(pageId, files, {
+      onUploaded: (attachment) => {
         const fileName = attachment.originalName;
         const fileName = attachment.originalName;
 
 
-        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
+        const prefix = attachment.fileFormat.startsWith('image/')
+          ? '!' // use "![fileName](url)" syntax when image
+          : '';
+        const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`;
 
 
         codeMirrorEditor?.insertText(insertText);
         codeMirrorEditor?.insertText(insertText);
-      }
-      catch (e) {
-        logger.error('failed to upload', e);
-        toastError(e);
-      }
+      },
+      onError: (error) => {
+        toastError(error);
+      },
     });
     });
-
   }, [codeMirrorEditor, pageId]);
   }, [codeMirrorEditor, pageId]);
 
 
-  // initial caret line
-  useEffect(() => {
-    codeMirrorEditor?.setCaretLine();
-  }, [codeMirrorEditor]);
-
   // set handler to save and return to View
   // set handler to save and return to View
   useEffect(() => {
   useEffect(() => {
     globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
     globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
@@ -287,6 +270,20 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     };
     };
   }, [saveAndReturnToViewHandler]);
   }, [saveAndReturnToViewHandler]);
 
 
+
+  // TODO: https://redmine.weseek.co.jp/issues/142729
+  // https://regex101.com/r/Wg2Hh6/1
+  // initial caret line
+  useEffect(() => {
+    const untitledPageRegex = /^Untitled-\d+$/;
+    const isNewlyCreatedPage = (
+      currentPage?.wip && currentPage?.latestRevision == null && untitledPageRegex.test(nodePath.basename(currentPage?.path ?? ''))
+    ) ?? false;
+    if (!isNewlyCreatedPage) {
+      codeMirrorEditor?.setCaretLine();
+    }
+  }, [codeMirrorEditor, currentPage]);
+
   // set handler to focus
   // set handler to focus
   useLayoutEffect(() => {
   useLayoutEffect(() => {
     if (editorMode === EditorMode.Editor) {
     if (editorMode === EditorMode.Editor) {

+ 22 - 8
apps/app/src/components/PageEditor/ScrollSyncHelper.tsx

@@ -19,12 +19,12 @@ const getDataLine = (element: Element | null): number => {
 
 
 const getEditorElements = (editorRootElement: HTMLElement): Array<Element> => {
 const getEditorElements = (editorRootElement: HTMLElement): Array<Element> => {
   return Array.from(editorRootElement.getElementsByClassName('cm-line'))
   return Array.from(editorRootElement.getElementsByClassName('cm-line'))
-    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
+    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line') ?? Number.NaN) });
 };
 };
 
 
 const getPreviewElements = (previewRootElement: HTMLElement): Array<Element> => {
 const getPreviewElements = (previewRootElement: HTMLElement): Array<Element> => {
   return Array.from(previewRootElement.getElementsByClassName('has-data-line'))
   return Array.from(previewRootElement.getElementsByClassName('has-data-line'))
-    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
+    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line') ?? Number.NaN) });
 };
 };
 
 
 // Ref: https://github.com/mikolalysenko/binary-search-bounds/blob/f436a2a8af11bf3208434e18bbac17e18e7a3a30/search-bounds.js
 // Ref: https://github.com/mikolalysenko/binary-search-bounds/blob/f436a2a8af11bf3208434e18bbac17e18e7a3a30/search-bounds.js
@@ -63,14 +63,14 @@ const findElementIndexFromDataLine = (previewElements: Array<Element>, dataline:
 
 
 
 
 type SourceElement = {
 type SourceElement = {
-  start: DOMRect,
-  top: DOMRect,
-  next: DOMRect | undefined,
+  start?: DOMRect,
+  top?: DOMRect,
+  next?: DOMRect,
 }
 }
 
 
 type TargetElement = {
 type TargetElement = {
-  start: DOMRect,
-  next: DOMRect | undefined,
+  start?: DOMRect,
+  next?: DOMRect,
 }
 }
 
 
 const calcScrollElementToTop = (element: Element): number => {
 const calcScrollElementToTop = (element: Element): number => {
@@ -78,7 +78,13 @@ const calcScrollElementToTop = (element: Element): number => {
 };
 };
 
 
 const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: TargetElement): number => {
 const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: TargetElement): number => {
-  if (sourceElement.start === sourceElement.next || sourceElement.next == null || targetElement.next == null) {
+  if (sourceElement.start === sourceElement.next) {
+    return 0;
+  }
+  if (sourceElement.start == null || sourceElement.top == null || sourceElement.next == null) {
+    return 0;
+  }
+  if (targetElement.start == null || targetElement.next == null) {
     return 0;
     return 0;
   }
   }
   const sourceAllHeight = sourceElement.next.top - sourceElement.start.top;
   const sourceAllHeight = sourceElement.next.top - sourceElement.start.top;
@@ -107,6 +113,10 @@ const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLEl
 
 
   let newScrollTop = previewRootElement.scrollTop;
   let newScrollTop = previewRootElement.scrollTop;
 
 
+  if (previewElements[topPreviewElementIndex] == null) {
+    return;
+  }
+
   newScrollTop += calcScrollElementToTop(previewElements[topPreviewElementIndex]);
   newScrollTop += calcScrollElementToTop(previewElements[topPreviewElementIndex]);
   newScrollTop += calcScorllElementByRatio(
   newScrollTop += calcScorllElementByRatio(
     {
     {
@@ -136,6 +146,10 @@ const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLE
   const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
   const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
   const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
   const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
 
 
+  if (editorElements[startEditorElementIndex] == null) {
+    return;
+  }
+
   let newScrollTop = editorRootElement.scrollTop;
   let newScrollTop = editorRootElement.scrollTop;
 
 
   newScrollTop += calcScrollElementToTop(editorElements[startEditorElementIndex]);
   newScrollTop += calcScrollElementToTop(editorElements[startEditorElementIndex]);

+ 1 - 1
apps/app/src/components/PageEditor/_page-editor-inheritance.scss

@@ -1 +1 @@
-$navbar-editor-height: 30px;
+$navbar-editor-height: 32.8px;

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

@@ -21,9 +21,8 @@ export const PageHeader: FC = () => {
       <PagePathHeader
       <PagePathHeader
         currentPage={currentPage}
         currentPage={currentPage}
       />
       />
-      <div className="row mt-1">
+      <div className="mt-1">
         <PageTitleHeader
         <PageTitleHeader
-          className="col"
           currentPage={currentPage}
           currentPage={currentPage}
         />
         />
       </div>
       </div>

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

@@ -3,9 +3,9 @@
   input {
   input {
     min-width: 20px;
     min-width: 20px;
     min-height: unset;
     min-height: unset;
-    padding-top: 0;
-    padding-bottom: 0;
-    line-height: 1em;
+    padding-top: 2px;
+    padding-bottom: 2px;
+    line-height: 1.2em;
   }
   }
 
 
   .page-path-header-buttons {
   .page-path-header-buttons {

+ 6 - 5
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -1,7 +1,7 @@
 import {
 import {
-  useState, useEffect, useCallback, memo, useMemo,
+  useState, useCallback, memo,
 } from 'react';
 } from 'react';
-import type { CSSProperties, FC } from 'react';
+import type { FC } from 'react';
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
@@ -23,12 +23,13 @@ const moduleClass = styles['page-path-header'];
 
 
 
 
 type Props = {
 type Props = {
-  currentPage: IPagePopulatedToShowRevision
+  currentPage: IPagePopulatedToShowRevision,
+  className?: string,
 }
 }
 
 
 export const PagePathHeader: FC<Props> = memo((props: Props) => {
 export const PagePathHeader: FC<Props> = memo((props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { currentPage } = props;
+  const { currentPage, className } = props;
 
 
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const parentPagePath = dPagePath.former;
   const parentPagePath = dPagePath.former;
@@ -102,7 +103,7 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
   return (
   return (
     <div
     <div
       id="page-path-header"
       id="page-path-header"
-      className={`d-flex ${moduleClass} small position-relative`}
+      className={`d-flex ${moduleClass} ${className ?? ''} small position-relative ms-2`}
       onMouseEnter={() => setHover(true)}
       onMouseEnter={() => setHover(true)}
       onMouseLeave={() => setHover(false)}
       onMouseLeave={() => setHover(false)}
     >
     >

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

@@ -5,6 +5,16 @@
     min-height: unset;
     min-height: unset;
     padding: 0 0.5rem;
     padding: 0 0.5rem;
     line-height: 1em;
     line-height: 1em;
-    transform: translateX(-0.55rem) translateY(-0.05rem);
+    transform: translateX(0.05rem) translateY(0.05rem);
+  }
+}
+
+.page-title-header-border-color {
+  --bs-border-color: transparent;
+
+  &:global {
+    &:hover {
+      --bs-border-color: var(--bs-primary-border-subtle);
+    }
   }
   }
 }
 }

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

@@ -1,11 +1,12 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useEffect } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
+import { isMovablePage } from '@growi/core/dist/utils/page-path-utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { ValidationTarget } from '~/client/util/input-validator';
 import { ValidationTarget } from '~/client/util/input-validator';
@@ -14,9 +15,11 @@ import ClosableTextInput from '../Common/ClosableTextInput';
 import { CopyDropdown } from '../Common/CopyDropdown';
 import { CopyDropdown } from '../Common/CopyDropdown';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 
 
+
 import styles from './PageTitleHeader.module.scss';
 import styles from './PageTitleHeader.module.scss';
 
 
-const moduleClass = styles['page-title-header'];
+const moduleClass = styles['page-title-header'] ?? '';
+const borderColorClass = styles['page-title-header-border-color'] ?? '';
 
 
 type Props = {
 type Props = {
   currentPage: IPagePopulatedToShowRevision,
   currentPage: IPagePopulatedToShowRevision,
@@ -29,6 +32,8 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
 
   const currentPagePath = currentPage.path;
   const currentPagePath = currentPage.path;
 
 
+  const isMovable = isMovablePage(currentPagePath);
+
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const pageTitle = dPagePath.latter;
   const pageTitle = dPagePath.latter;
 
 
@@ -39,6 +44,12 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
 
   const editedPageTitle = nodePath.basename(editedPagePath);
   const editedPageTitle = nodePath.basename(editedPagePath);
 
 
+  // TODO: https://redmine.weseek.co.jp/issues/142729
+  // https://regex101.com/r/Wg2Hh6/1
+  const untitledPageRegex = /^Untitled-\d+$/;
+
+  const isNewlyCreatedPage = (currentPage.wip && currentPage.latestRevision == null && untitledPageRegex.test(editedPageTitle)) ?? false;
+
   const onRenameFinish = useCallback(() => {
   const onRenameFinish = useCallback(() => {
     setRenameInputShown(false);
     setRenameInputShown(false);
   }, []);
   }, []);
@@ -48,8 +59,9 @@ export const PageTitleHeader: FC<Props> = (props) => {
   }, []);
   }, []);
 
 
   const onInputChange = useCallback((inputText: string) => {
   const onInputChange = useCallback((inputText: string) => {
+    const newPageTitle = pathUtils.removeHeadingSlash(inputText);
     const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
     const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
-    const newPagePath = nodePath.resolve(parentPagePath, inputText);
+    const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
 
 
     setEditedPagePath(newPagePath);
     setEditedPagePath(newPagePath);
   }, [currentPage?.path, setEditedPagePath]);
   }, [currentPage?.path, setEditedPagePath]);
@@ -64,10 +76,19 @@ export const PageTitleHeader: FC<Props> = (props) => {
   }, [currentPagePath]);
   }, [currentPagePath]);
 
 
   const onClickPageTitle = useCallback(() => {
   const onClickPageTitle = useCallback(() => {
+    if (!isMovable) {
+      return;
+    }
+
     setEditedPagePath(currentPagePath);
     setEditedPagePath(currentPagePath);
     setRenameInputShown(true);
     setRenameInputShown(true);
-  }, [currentPagePath]);
+  }, [currentPagePath, isMovable]);
 
 
+  useEffect(() => {
+    if (isNewlyCreatedPage) {
+      setRenameInputShown(true);
+    }
+  }, [currentPage._id, isNewlyCreatedPage]);
 
 
   return (
   return (
     <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
     <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
@@ -75,18 +96,24 @@ export const PageTitleHeader: FC<Props> = (props) => {
         { isRenameInputShown && (
         { isRenameInputShown && (
           <div className="position-absolute w-100">
           <div className="position-absolute w-100">
             <ClosableTextInput
             <ClosableTextInput
-              value={editedPageTitle}
+              value={isNewlyCreatedPage ? '' : editedPageTitle}
               placeholder={t('Input page name')}
               placeholder={t('Input page name')}
               inputClassName="fs-4"
               inputClassName="fs-4"
               onPressEnter={onPressEnter}
               onPressEnter={onPressEnter}
               onPressEscape={onPressEscape}
               onPressEscape={onPressEscape}
               onChange={onInputChange}
               onChange={onInputChange}
-              onClickOutside={() => setRenameInputShown(false)}
+              onClickOutside={() => { setRenameInputShown(false) }}
               validationTarget={ValidationTarget.PAGE}
               validationTarget={ValidationTarget.PAGE}
             />
             />
           </div>
           </div>
         ) }
         ) }
-        <h1 className={`mb-0 fs-4 ${isRenameInputShown ? 'invisible' : ''} text-truncate`} onClick={onClickPageTitle}>
+        <h1
+          className={`mb-0 px-2 fs-4
+            ${isRenameInputShown ? 'invisible' : ''} text-truncate
+            ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
+          `}
+          onClick={onClickPageTitle}
+        >
           {pageTitle}
           {pageTitle}
         </h1>
         </h1>
       </div>
       </div>
@@ -100,7 +127,7 @@ export const PageTitleHeader: FC<Props> = (props) => {
           pageId={currentPage._id}
           pageId={currentPage._id}
           pagePath={currentPage.path}
           pagePath={currentPage.path}
           dropdownToggleId={`copydropdown-${currentPage._id}`}
           dropdownToggleId={`copydropdown-${currentPage._id}`}
-          dropdownToggleClassName="ms-2 p-1"
+          dropdownToggleClassName="p-1"
         >
         >
           <span className="material-symbols-outlined fs-6">content_paste</span>
           <span className="material-symbols-outlined fs-6">content_paste</span>
         </CopyDropdown>
         </CopyDropdown>

+ 14 - 12
apps/app/src/components/PageHistory/PageRevisionTable.tsx

@@ -56,6 +56,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
 
 
   useEffect(() => {
   useEffect(() => {
     if (revisions != null) {
     if (revisions != null) {
+      // when both source and target are specified
       if (sourceRevisionId != null && targetRevisionId != null) {
       if (sourceRevisionId != null && targetRevisionId != null) {
         const sourceRevision = revisions.filter(revision => revision._id === sourceRevisionId)[0];
         const sourceRevision = revisions.filter(revision => revision._id === sourceRevisionId)[0];
         const targetRevision = revisions.filter(revision => revision._id === targetRevisionId)[0];
         const targetRevision = revisions.filter(revision => revision._id === targetRevisionId)[0];
@@ -63,11 +64,10 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
         setTargetRevision(targetRevision);
         setTargetRevision(targetRevision);
       }
       }
       else {
       else {
-        const latestRevision = revisions != null ? revisions[0] : null;
-        if (latestRevision != null) {
-          setSourceRevision(latestRevision);
-          setTargetRevision(latestRevision);
-        }
+        const latestRevision = revisions != null ? revisions[0] : undefined;
+        const previousRevision = revisions.length >= 2 ? revisions[1] : latestRevision;
+        setTargetRevision(latestRevision);
+        setSourceRevision(previousRevision);
       }
       }
     }
     }
   }, [revisions, sourceRevisionId, targetRevisionId]);
   }, [revisions, sourceRevisionId, targetRevisionId]);
@@ -210,13 +210,15 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
       </table>
       </table>
 
 
       {sourceRevision != null && targetRevision != null && (
       {sourceRevision != null && targetRevision != null && (
-        <RevisionComparer
-          sourceRevision={sourceRevision}
-          targetRevision={targetRevision}
-          currentPageId={currentPageId}
-          currentPagePath={currentPagePath}
-          onClose={onClose}
-        />
+        <div className="mt-5">
+          <RevisionComparer
+            sourceRevision={sourceRevision}
+            targetRevision={targetRevision}
+            currentPageId={currentPageId}
+            currentPagePath={currentPagePath}
+            onClose={onClose}
+          />
+        </div>
       )
       )
       }
       }
     </>
     </>

+ 2 - 21
apps/app/src/components/PageHistory/RevisionDiff.module.scss

@@ -1,28 +1,9 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
 .revision-diff-container :global {
 .revision-diff-container :global {
-  .comparison-header {
-    height: 34px;
-    background-color: #ffffff;
-    border: 1px solid bs.$gray-300;
-    .comparison-source-wrapper {
-      height: 26px;
-      margin-right: 1px;
-      border-right: 1px solid bs.$gray-300;
-      .comparison-source {
-        color: bs.$gray-500;
-      }
-    }
-    .comparison-target-wrapper {
-      height: 26px;
-      .comparison-target {
-        color: bs.$gray-500;
-      }
-    }
+  .link-created-at {
+    text-decoration-line: underline;
   }
   }
 
 
   .revision-history-diff {
   .revision-history-diff {
-    color: bs.$gray-900;
     table-layout: fixed;
     table-layout: fixed;
 
 
     // revision-history
     // revision-history

+ 48 - 26
apps/app/src/components/PageHistory/RevisionDiff.tsx

@@ -1,14 +1,17 @@
-import React from 'react';
+import { useMemo } from 'react';
 
 
 import type { IRevisionHasPageId } from '@growi/core';
 import type { IRevisionHasPageId } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { createPatch } from 'diff';
 import { createPatch } from 'diff';
 import type { Diff2HtmlConfig } from 'diff2html';
 import type { Diff2HtmlConfig } from 'diff2html';
 import { html } from 'diff2html';
 import { html } from 'diff2html';
+import { ColorSchemeType } from 'diff2html/lib/types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
+
 import UserDate from '../User/UserDate';
 import UserDate from '../User/UserDate';
 
 
 import styles from './RevisionDiff.module.scss';
 import styles from './RevisionDiff.module.scss';
@@ -31,6 +34,19 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
     currentRevision, previousRevision, revisionDiffOpened, currentPageId, currentPagePath, onClose,
     currentRevision, previousRevision, revisionDiffOpened, currentPageId, currentPagePath, onClose,
   } = props;
   } = props;
 
 
+  const { theme } = useNextThemes();
+
+  const colorScheme: ColorSchemeType = useMemo(() => {
+    switch (theme) {
+      case Themes.DARK:
+        return ColorSchemeType.DARK;
+      case Themes.LIGHT:
+        return ColorSchemeType.LIGHT;
+      default:
+        return ColorSchemeType.AUTO;
+    }
+  }, [theme]);
+
   const previousText = (currentRevision._id === previousRevision._id) ? '' : previousRevision.body;
   const previousText = (currentRevision._id === previousRevision._id) ? '' : previousRevision.body;
 
 
   const patch = createPatch(
   const patch = createPatch(
@@ -42,6 +58,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
   const option: Diff2HtmlConfig = {
   const option: Diff2HtmlConfig = {
     outputFormat: 'side-by-side',
     outputFormat: 'side-by-side',
     drawFileList: false,
     drawFileList: false,
+    colorScheme,
   };
   };
 
 
   const diffViewHTML = revisionDiffOpened ? html(patch, option) : '';
   const diffViewHTML = revisionDiffOpened ? html(patch, option) : '';
@@ -50,34 +67,39 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
 
 
   return (
   return (
     <div className={`${styles['revision-diff-container']}`}>
     <div className={`${styles['revision-diff-container']}`}>
-      <div className="comparison-header">
-        <div className="container pt-1 pe-0">
-          <div className="row">
-            <div className="col comparison-source-wrapper pt-1 px-0">
-              <span className="comparison-source pe-3">{t('page_history.comparing_source')}</span><UserDate dateTime={previousRevision.createdAt} />
-              <Link
-                href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${previousRevision._id}`)}
-                className="ms-3"
-                onClick={onClose}
-                prefetch={false}
-              >
-                <span className="material-symbols-outlined">login</span>
-              </Link>
-            </div>
-            <div className="col comparison-target-wrapper pt-1">
-              <span className="comparison-target pe-3">{t('page_history.comparing_target')}</span><UserDate dateTime={currentRevision.createdAt} />
-              <Link
-                href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${currentRevision._id}`)}
-                className="ms-3"
-                onClick={onClose}
-                prefetch={false}
-              >
-                <span className="material-symbols-outlined">login</span>
-              </Link>
-            </div>
+      <div className="container">
+        <div className="row mt-2">
+          <div className="col px-0 py-2">
+            <span className="fw-bold">{t('page_history.comparing_source')}</span>
+            <Link
+              href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${previousRevision._id}`)}
+              className="small ms-2
+                link-created-at
+                link-secondary link-opacity-75 link-opacity-100-hover
+                link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover"
+              onClick={onClose}
+              prefetch={false}
+            >
+              <UserDate dateTime={previousRevision.createdAt} />
+            </Link>
+          </div>
+          <div className="col px-0 py-2">
+            <span className="fw-bold">{t('page_history.comparing_target')}</span>
+            <Link
+              href={urljoin(returnPathForURL(currentPagePath, currentPageId), `?revisionId=${currentRevision._id}`)}
+              className="small ms-2
+                link-created-at
+                link-secondary link-opacity-75 link-opacity-100-hover
+                link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover"
+              onClick={onClose}
+              prefetch={false}
+            >
+              <UserDate dateTime={currentRevision.createdAt} />
+            </Link>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
+      {/* eslint-disable-next-line react/no-danger */}
       <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
       <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
     </div>
     </div>
   );
   );

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

@@ -15,11 +15,6 @@
     }
     }
     .grw-card-btn-container {
     .grw-card-btn-container {
       text-align: center;
       text-align: center;
-
-      .btn {
-        // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-        // @include bs.button-size(bs.$btn-padding-y-lg, bs.$btn-padding-x-lg, bs.$btn-font-size-lg, bs.$btn-line-height-lg, bs.$btn-border-radius-lg);
-      }
     }
     }
   }
   }
 
 

+ 22 - 7
apps/app/src/components/ReactMarkdownComponents/LightBox.tsx

@@ -1,19 +1,34 @@
-import React, { useState } from 'react';
+import type { DetailedHTMLProps, ImgHTMLAttributes } from 'react';
+import React, { useMemo, useState } from 'react';
 
 
 import FsLightbox from 'fslightbox-react';
 import FsLightbox from 'fslightbox-react';
+import { createPortal } from 'react-dom';
 
 
-export const LightBox = (props) => {
+type Props = DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>
+
+export const LightBox = (props: Props): JSX.Element => {
   const [toggler, setToggler] = useState(false);
   const [toggler, setToggler] = useState(false);
-  const { node, ...rest } = props;
+  const { alt, ...rest } = props;
 
 
-  return (
-    <>
-      <img {...rest} onClick={() => setToggler(!toggler)} />
+  const lightboxPortal = useMemo(() => {
+    return createPortal(
       <FsLightbox
       <FsLightbox
         toggler={toggler}
         toggler={toggler}
         sources={[props.src]}
         sources={[props.src]}
+        alt={alt}
         type="image"
         type="image"
-      />
+        exitFullscreenOnClose
+      />,
+      document.body,
+    );
+  }, [alt, props.src, toggler]);
+
+  return (
+    <>
+      {/* eslint-disable-next-line @next/next/no-img-element */}
+      <img alt={alt} {...rest} onClick={() => setToggler(!toggler)} />
+
+      {lightboxPortal}
     </>
     </>
   );
   );
 };
 };

+ 8 - 0
apps/app/src/components/RevisionComparer/RevisionComparer.module.scss

@@ -1,3 +1,7 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '@growi/ui/scss/atoms/btn-muted';
+
 .revision-compare :global {
 .revision-compare :global {
   .revision-compare-container {
   .revision-compare-container {
     min-height: 100px;
     min-height: 100px;
@@ -13,6 +17,10 @@
   }
   }
 
 
   .grw-copy-dropdown {
   .grw-copy-dropdown {
+    .btn.btn-copy {
+      @include btn-muted.colorize(bs.$gray-500);
+    }
+
     .dropdown-menu {
     .dropdown-menu {
       min-width: 310px;
       min-width: 310px;
 
 

+ 26 - 26
apps/app/src/components/RevisionComparer/RevisionComparer.tsx

@@ -66,33 +66,33 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
     <div className={`${styles['revision-compare']} revision-compare`}>
     <div className={`${styles['revision-compare']} revision-compare`}>
       <div className="d-flex">
       <div className="d-flex">
         <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
         <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
-        <Dropdown
-          className="grw-copy-dropdown align-self-center ms-auto"
-          isOpen={dropdownOpen}
-          toggle={() => toggleDropdown()}
-        >
-          <DropdownToggle
-            caret
-            className="d-block text-muted bg-transparent btn-copy border-0 py-0"
+
+        { !isNodiff && (
+          <Dropdown
+            className="grw-copy-dropdown align-self-center ms-auto"
+            isOpen={dropdownOpen}
+            toggle={() => toggleDropdown()}
           >
           >
-            <span className="material-symbols-outlined">content_paste</span>
-          </DropdownToggle>
-          <DropdownMenu strategy="fixed" end>
-            {/* Page path URL */}
-            <CopyToClipboard text={generateURL(currentPagePath)}>
-              <DropdownItem className="px-3">
-                <DropdownItemContents title={t('copy_to_clipboard.Page URL', { ns: 'commons' })} contents={generateURL(currentPagePath)} />
-              </DropdownItem>
-            </CopyToClipboard>
-            {/* Permanent Link URL */}
-            <CopyToClipboard text={generateURL(currentPageId)}>
-              <DropdownItem className="px-3">
-                <DropdownItemContents title={t('copy_to_clipboard.Permanent link', { ns: 'commons' })} contents={generateURL(currentPageId)} />
-              </DropdownItem>
-            </CopyToClipboard>
-            <DropdownItem divider className="my-0"></DropdownItem>
-          </DropdownMenu>
-        </Dropdown>
+            <DropdownToggle className="btn-copy">
+              <span className="material-symbols-outlined">content_paste</span>
+            </DropdownToggle>
+            <DropdownMenu strategy="fixed" end>
+              {/* Page path URL */}
+              <CopyToClipboard text={generateURL(currentPagePath)}>
+                <DropdownItem className="px-3">
+                  <DropdownItemContents title={t('copy_to_clipboard.Page URL', { ns: 'commons' })} contents={generateURL(currentPagePath)} />
+                </DropdownItem>
+              </CopyToClipboard>
+              {/* Permanent Link URL */}
+              <CopyToClipboard text={generateURL(currentPageId)}>
+                <DropdownItem className="px-3">
+                  <DropdownItemContents title={t('copy_to_clipboard.Permanent link', { ns: 'commons' })} contents={generateURL(currentPageId)} />
+                </DropdownItem>
+              </CopyToClipboard>
+              <DropdownItem divider className="my-0"></DropdownItem>
+            </DropdownMenu>
+          </Dropdown>
+        ) }
       </div>
       </div>
 
 
       <div className={`revision-compare-container ${isNodiff ? 'nodiff' : ''}`}>
       <div className={`revision-compare-container ${isNodiff ? 'nodiff' : ''}`}>

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

@@ -50,7 +50,7 @@ export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: He
                 checked={isWipPageShown}
                 checked={isWipPageShown}
                 onChange={() => {}}
                 onChange={() => {}}
               />
               />
-              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+              <label className="form-label form-check-label text-muted mb-0" htmlFor="wipPageVisibility">
                 {t('sidebar_header.show_wip_page')}
                 {t('sidebar_header.show_wip_page')}
               </label>
               </label>
             </div>
             </div>

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

@@ -95,14 +95,14 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
   const FormerLink = () => (
   const FormerLink = () => (
-    <div className={`${formerLinkClass} ${isSmall ? 'text-truncate' : ''} small`}>
+    <div className={`${formerLinkClass} ${isSmall ? 'text-truncate small' : ''}`}>
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
     </div>
     </div>
   );
   );
 
 
   let locked;
   let locked;
   if (page.grant !== 1) {
   if (page.grant !== 1) {
-    locked = <span className="material-symbols-outlined ms-2">lock</span>;
+    locked = <span className="material-symbols-outlined ms-2 fs-6">lock</span>;
   }
   }
 
 
   const isTagElementsRendered = !(isSmall || (page.tags.length === 0));
   const isTagElementsRendered = !(isSmall || (page.tags.length === 0));
@@ -114,7 +114,7 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
         <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
         <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
 
 
         <div className="flex-grow-1 ms-2">
         <div className="flex-grow-1 ms-2">
-          <div className={`row ${isSmall ? 'gy-0' : 'gy-2'}`}>
+          <div className={`row ${isSmall ? 'gy-0' : 'gy-1'}`}>
 
 
             <div className="col-12">
             <div className="col-12">
               { !dPagePath.isRoot && <FormerLink /> }
               { !dPagePath.isRoot && <FormerLink /> }

+ 6 - 2
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -130,13 +130,17 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
     mutateCollapsedContentsOpened(false);
     mutateCollapsedContentsOpened(false);
   }, [isCollapsedMode, mutateCollapsedContentsOpened]);
   }, [isCollapsedMode, mutateCollapsedContentsOpened]);
 
 
-  const openClass = `${isCollapsedContentsOpened ? 'open' : ''}`;
+  const closedClass = isCollapsedMode() && !isCollapsedContentsOpened ? 'd-none' : '';
+  const openedClass = isCollapsedMode() && isCollapsedContentsOpened ? 'open' : '';
   const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined;
   const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined;
 
 
   return (
   return (
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
-      <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${openClass}`} style={{ width: collapsibleContentsWidth }}>
+      <div
+        className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
+        style={{ width: collapsibleContentsWidth }}
+      >
         {children}
         {children}
       </div>
       </div>
     </div>
     </div>

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

@@ -12,7 +12,7 @@ export const SidebarBrandLogo = memo((props: SidebarBrandLogoProps) => {
   return isDefaultLogo
   return isDefaultLogo
     ? <GrowiLogo />
     ? <GrowiLogo />
     // eslint-disable-next-line @next/next/no-img-element
     // eslint-disable-next-line @next/next/no-img-element
-    : (<img src="/attachment/brand-logo" alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />);
+    : (<div><img src="/attachment/brand-logo" alt="custom logo" className="picture picture-lg p-2" id="settingBrandLogo" /></div>);
 });
 });
 
 
 SidebarBrandLogo.displayName = 'SidebarBrandLogo';
 SidebarBrandLogo.displayName = 'SidebarBrandLogo';

+ 9 - 0
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss

@@ -1,6 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 
 
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
+@use '~/styles/mixins';
 
 
 @use '../button-styles';
 @use '../button-styles';
 
 
@@ -23,6 +24,14 @@
   }
   }
 }
 }
 
 
+// Hide when editing
+@include mixins.editing() {
+  .btn-toggle-collapse {
+    visibility: hidden;
+  }
+}
+
+
 // == Colors
 // == Colors
 .btn-toggle-collapse {
 .btn-toggle-collapse {
   &:global {
   &:global {

+ 18 - 10
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss

@@ -1,5 +1,23 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 
 
+.btn-personal-dropdown :global {
+  img {
+    border: 2px solid var(--bs-border-color-translucent);
+  }
+}
+
+.personal-dropdown-header :global {
+  .item-text-email {
+    font-size: 10.5px;
+  }
+}
+
+.personal-dropdown-menu :global {
+  --bs-dropdown-font-size: 14px;
+}
+
+
+// == Colors
 @include bs.color-mode(light) {
 @include bs.color-mode(light) {
   .personal-dropdown-header :global {
   .personal-dropdown-header :global {
     color: var(--bs-gray-600);
     color: var(--bs-gray-600);
@@ -21,13 +39,3 @@
     color: 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;
-  }
-}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä