Sfoglia il codice sorgente

Merge pull request #8631 from weseek/support/142908-comment-style

imprv: New Comment style
Yuki Takei 2 anni fa
parent
commit
71a691c287

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

@@ -0,0 +1,710 @@
+@use '@growi/core/scss/bootstrap/init' as *;
+
+@use '../variables' as var;
+@use '../mixins';
+@use '../atoms/mixins/code';
+@use './mixins/hsl-button';
+@use './hsl-functions' as hsl;
+
+@import 'apply-colors-dark';
+@import 'apply-colors-light';
+
+//
+//== Apply to Bootstrap
+//
+
+// determine optional variables
+$bgcolor-search-top-dropdown: var(--bgcolor-search-top-dropdown,var(--secondary));
+$bgcolor-sidebar-nav-item-active: var(--bgcolor-sidebar-nav-item-active,#{hsl.darken(var(--primary),10%)});
+$text-shadow-sidebar-nav-item-active: var(--text-shadow-sidebar-nav-item-active,1px 1px 2px var(--primary));
+$bgcolor-inline-code: var(--bgcolor-inline-code, #{$gray-100});
+$color-inline-code: var(--color-inline-code, #{darken($red, 15%)});
+$bordercolor-inline-code: var(--bordercolor-inline-code, #{$gray-400});
+$bordercolor-nav-tabs: var(--bordercolor-nav-tabs, #{$gray-300});
+$bordercolor-nav-tabs-hover: var(--bordercolor-nav-tabs-hover,#{$gray-200} #{$gray-200} #{$bordercolor-nav-tabs});
+$border-nav-tabs-link-active: var(--border-nav-tabs-link-active, #{$gray-600});
+$bordercolor-nav-tabs-active: var(--bordercolor-nav-tabs-active,$bordercolor-nav-tabs $bordercolor-nav-tabs var(--bgcolor-global));
+$color-btn-reload-in-sidebar: var(--color-btn-reload-in-sidebar,#{$gray-500});
+$bgcolor-keyword-highlighted: var(--bgcolor-keyword-highlighted,#{var.$grw-marker-yellow});
+$color-page-list-group-item-meta: var(--color-page-list-group-item-meta,#{$gray-500});
+$color-search-page-list-title: var(--color-search-page-list-title,var(--color-global));
+
+// override bootstrap variables
+$body-bg: var(--bgcolor-global);
+$body-color: var(--color-global);
+$link-color: var(--color-link);
+$link-hover-color: var(--color-link-hover);
+$input-focus-color: var(--color-global);
+$nav-tabs-border-color: $bordercolor-nav-tabs;
+$nav-tabs-link-hover-border-color: $bordercolor-nav-tabs-hover;
+$nav-tabs-link-active-color: var(--color-nav-tabs-link-active);
+$nav-tabs-link-active-bg: var(--bgcolor-global);
+$nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
+$theme-colors: map-merge($theme-colors, ( primary: $primary ));
+
+// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
+// @import 'reboot-bootstrap-buttons';
+// @import 'reboot-bootstrap-colors';
+// @import 'reboot-bootstrap-theme-colors';
+// @import 'hsl-reboot-bootstrap-theme-colors';
+// @import 'reboot-bootstrap-nav';
+// @import 'reboot-toastr-colors';
+
+// determine variables with bootstrap function (These variables can be used after importing bootstrap above)
+$color-modal-header: var(--color-modal-header,#{hsl.contrast(var(--primary))});
+
+code:not([class^='language-']) {
+  @include code.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
+}
+
+.code-highlighted {
+  border-color: $bordercolor-inline-code;
+}
+
+//
+//== Apply to Bootstrap Elements
+//
+
+// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
+// theme-color-level() dropped in bootstrap v5
+// Alert link
+// @each $color, $value in $theme-colors {
+//   .alert.alert-#{$color} {
+//     a,
+//     a:hover {
+//       color: theme-color-level($color, $alert-color-level - 2);
+//     }
+//   }
+// }
+
+// Dropdown
+.grw-apperance-mode-dropdown {
+  .grw-sidebar-mode-icon svg {
+    fill: var(--secondary);
+  }
+  .grw-color-mode-icon svg {
+    fill: var(--color-global);
+  }
+  .grw-color-mode-icon-muted svg {
+    fill: var(--secondary);
+  }
+}
+
+// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
+// form-control-focus() dropped in bootstrap v5
+// Form
+// .form-control {
+//   @include form-control-focus();
+// }
+
+// Tabs
+.nav.nav-tabs .nav-link.active {
+  color: var(--color-link);
+  background: transparent;
+
+  &:hover,
+  &:focus {
+    color: var(--color-link-hover);
+  }
+}
+
+// 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 {
+      color: var(--primary);
+      border-color: var(--secondary) !important;
+      &:hover,
+      &:active,
+      &:focus {
+        color: var(--primary);
+      }
+    }
+  }
+}
+
+//
+//== Apply to Handsontable
+//
+.handsontable {
+  color: initial;
+}
+
+//
+//== Apply to GROWI Elements
+//
+
+.grw-logo {
+  // set transition for fill
+  svg, svg * {
+    transition: fill 0.8s ease-out;
+  }
+
+  svg {
+    fill: var(--fillcolor-logo-mark);
+  }
+
+  &:hover {
+    svg {
+      .group1 {
+        fill: var.$growi-green;
+      }
+
+      .group2 {
+        fill: var.$growi-blue;
+      }
+    }
+  }
+}
+
+.grw-navbar {
+  background: var(--bgcolor-navbar);
+  .nav-item .nav-link {
+    color: var(--color-link-nabvar);
+  }
+
+  border-image: var(--border-image-navbar) !important;
+  border-image-slice: 1 !important;
+
+  .grw-app-title {
+    color: var(--fillcolor-logo-mark);
+  }
+}
+
+.grw-global-search {
+  .btn-secondary.dropdown-toggle {
+    @include hsl-button.button-variant(var(--bgcolor-search-top-dropdown), var(--bgcolor-search-top-dropdown));
+  }
+
+  // for https://youtrack.weseek.co.jp/issue/GW-2603
+  .search-typeahead {
+    background-color: hsl.alpha(var(--bgcolor-global),10%);
+  }
+  input.form-control {
+    border: none;
+  }
+}
+
+.grw-sidebar {
+  $color-resize-button: var(--color-resize-button,var(--color-global));
+  $bgcolor-resize-button: var(--bgcolor-resize-button,white);
+  $color-resize-button-hover: var(--color-resize-button-hover,var(--color-reversal));
+  $bgcolor-resize-button-hover: var(--bgcolor-resize-button-hover,#{hsl.lighten(var(--bgcolor-resize-button), 5%)});
+  // .grw-navigation-resize-button {
+  //   .hexagon-container svg {
+  //     .background {
+  //       fill: var(--bgcolor-resize-button);
+  //     }
+  //     .icon {
+  //       fill: var(--color-resize-button);
+  //     }
+  //   }
+  //   &:hover .hexagon-container svg {
+  //     .background {
+  //       fill: var(--bgcolor-resize-button-hover);
+  //     }
+  //     .icon {
+  //       fill: var(--color-resize-button-hover);
+  //     }
+  //   }
+  // }
+  div.grw-contextual-navigation {
+    > div {
+      color: var(--color-sidebar-context);
+      background-color: var(--bgcolor-sidebar-context);
+    }
+  }
+
+  .grw-sidebar-nav {
+    .btn {
+      @include hsl-button.button-variant(
+        var(--bgcolor-sidebar),
+        var(--bgcolor-sidebar),
+      );
+    }
+  }
+  .grw-sidebar-nav-primary-container {
+    .btn.active {
+      i {
+        text-shadow: $text-shadow-sidebar-nav-item-active;
+      }
+      // fukidashi
+      &:after {
+        border-right-color: var(--bgcolor-sidebar-context) !important;
+      }
+    }
+  }
+
+  .grw-sidebar-content-header {
+    .grw-btn-reload {
+      color: $color-btn-reload-in-sidebar;
+    }
+
+    .grw-recent-changes-resize-button {
+      .form-check-label::before {
+        background-color: var(--primary);
+      }
+
+      .form-check-label::after {
+        background-color: var(--bgcolor-global);
+      }
+
+      .form-check-input:not(:checked) + .form-check-label::before {
+        color: var(--bgcolor-global);
+      }
+
+      .form-check-input:checked + .form-check-label::before {
+        color: var(--bgcolor-global);
+        background-color: var(--primary);
+        border-color: var(--primary);
+      }
+      .form-check-input:checked + .form-check-label::after {
+        color: var(--bgcolor-global);
+      }
+    }
+  }
+
+  .grw-pagetree, .grw-foldertree {
+    .list-group-item {
+      .grw-pagetree-title-anchor, .grw-foldertree-title-anchor {
+        color: inherit;
+      }
+    }
+  }
+
+  .grw-pagetree-footer {
+    .h5.grw-private-legacy-pages-anchor {
+      color: inherit;
+    }
+  }
+
+  .grw-recent-changes {
+    .list-group {
+      .list-group-item {
+        background-color: transparent !important;
+
+        .icon-lock {
+          color: var(--color-link);
+        }
+
+        .grw-recent-changes-item-lower {
+          color: $gray-500;
+
+          svg {
+            fill: $gray-500;
+          }
+        }
+      }
+    }
+  }
+
+}
+
+/*
+ * Icon
+ */
+.editor-container .navbar-editor svg {
+  fill: var(--color-editor-icons);
+}
+
+// page preview button in link form
+.btn-page-preview svg {
+  fill: white;
+}
+
+/*
+ * Modal
+ */
+.modal {
+  .modal-header {
+    border-bottom-color: var(--border-color-theme);
+    .modal-title {
+      color: $color-modal-header;
+    }
+    .btn-close {
+      color: $color-modal-header;
+      opacity: 0.5;
+
+      &:hover {
+        opacity: 0.9;
+      }
+    }
+  }
+
+  .modal-content {
+    background-color: var(--bgcolor-global);
+  }
+
+  .modal-footer {
+    border-top-color: var(--border-color-theme);
+  }
+}
+
+.grw-page-accessories-modal,.grw-descendants-page-list-modal {
+  .modal-header {
+    .btn-close {
+      color: #{hsl.contrast(var(--bgcolor-global))};
+    }
+  }
+}
+
+.grw-custom-nav-tab {
+  .nav-item {
+    &:hover,
+    &:focus {
+      background-color: hsl.alpha(var(--color-link),10%);
+    }
+    .nav-link {
+      -webkit-appearance: none;
+      color: var(--color-link);
+      svg {
+        fill: var(--color-link);
+      }
+
+      // Disabled state lightens text
+      &.disabled {
+        color: $nav-link-disabled-color;
+        svg {
+          fill: $nav-link-disabled-color;
+        }
+      }
+    }
+  }
+
+  .grw-nav-slide-hr {
+    border-color: var(--color-link) !important;
+  }
+}
+
+/*
+ * cards
+ */
+.card.custom-card {
+  color: var(--color-global);
+  background-color: var(--bgcolor-card);
+  border-color: var(--light);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+/*
+ * Form Slider
+ */
+.admin-page {
+  span.slider {
+    background-color: $gray-300;
+
+    &:before {
+      background-color: white;
+    }
+  }
+
+  input:checked + .slider {
+    background-color: #007bff;
+  }
+
+  input:focus + .slider {
+    box-shadow: 0 0 1px #007bff;
+  }
+}
+
+/*
+ * GROWI wiki
+ */
+.wiki {
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  h7 {
+    &.blink {
+      @include mixins.blink-bgcolor(var(--bgcolor-blinked-section));
+    }
+  }
+
+  .highlighted-keyword {
+    background: linear-gradient(transparent 60%, $bgcolor-keyword-highlighted 60%);
+  }
+
+  a {
+    color: var(--color-link-wiki);
+
+    &:hover {
+      color: var(--color-link-wiki-hover);
+    }
+  }
+
+  // table with handsontable modal button
+  .editable-with-handsontable {
+    button {
+      color: var(--color-link-wiki);
+    }
+
+    button:hover {
+      color: var(--color-link-wiki-hover);
+    }
+  }
+}
+
+/*
+ * GROWI page-list
+ */
+.page-list {
+  // List group
+  .list-group {
+    .list-group-item {
+      background-color: var(--bgcolor-global) !important;
+      a {
+        svg {
+          fill: var(--color-global);
+        }
+
+        &:hover {
+          svg {
+            fill: var(--color-global);
+          }
+        }
+      }
+
+      .page-list-meta {
+        color: $color-page-list-group-item-meta;
+        svg {
+          fill: $color-page-list-group-item-meta;
+        }
+      }
+
+      &.list-group-item-action {
+        background-color: var(--bgcolor-list);
+        &.active {
+          border-left-color: var(--primary);
+        }
+      }
+    }
+  }
+}
+
+/*
+ * GROWI Editor
+ */
+.layout-root.editing {
+  background-color: hsl.darken(var(--bgcolor-global),2%);
+
+  &.builtin-editor {
+    .page-editor-editor-container {
+      border-right-color: var(--border-color-theme);
+    }
+  }
+
+  .navbar-editor {
+    background-color: var(--bgcolor-global); // same color with active tab
+    border-bottom-color: var(--border-color-theme);
+  }
+
+  .page-editor-preview-container {
+    background-color: var(--bgcolor-global);
+  }
+}
+
+
+/*
+ * Preview for editing /Sidebar
+ */
+.page-editor-preview-body.preview-sidebar {
+  color: var(--color-sidebar-context);
+  background-color: var(--bgcolor-sidebar-context);
+}
+
+/*
+ * GROWI Grid Edit Modal
+ */
+.grw-grid-edit-preview {
+  .desktop-preview,
+  .tablet-preview,
+  .mobile-preview {
+    background: var(--bgcolor-global);
+  }
+  .grid-edit-border-for-each-cols {
+    border: 2px solid var(--bgcolor-global);
+  }
+}
+
+.grid-preview-col-0 {
+  background: var.$growi-blue;
+}
+
+.grid-preview-col-1 {
+  background: var(--info);
+}
+
+.grid-preview-col-2 {
+  background: var(--success);
+}
+
+.grid-preview-col-3 {
+  background: var.$growi-green;
+}
+
+/*
+ * GROWI comment form
+ */
+.page-comments-row {
+  background: var(--bgcolor-subnav);
+  .page-comment .page-comment-main,
+  .page-comment-form .comment-form-main {
+    background-color: var(--bgcolor-global);
+
+    .nav.nav-tabs {
+      > li > a.active {
+        background: transparent;
+        border-bottom: solid 1px hsl.darken(var(--bgcolor-global),4%);
+        border-bottom-color: hsl.darken(var(--bgcolor-global),4%);
+      }
+    }
+  }
+}
+
+/*
+ * GROWI search result
+ */
+.search-result-base {
+  .grw-search-page-nav {
+    background-color: var(--bgcolor-subnav);
+  }
+  .search-control {
+    background-color: var(--bgcolor-global);
+  }
+  .page-list {
+    .highlighted-keyword {
+      background: linear-gradient(transparent 60%, $bgcolor-keyword-highlighted 60%);
+    }
+  }
+}
+
+/*
+ * react bootstrap typeahead
+ */
+mark.rbt-highlight-text {
+  // Temporarily the highlight color is black
+  color: black;
+}
+
+/*
+ * GROWI page content footer
+ */
+.page-content-footer {
+  background-color: hsl.darken(var(--bgcolor-global),2%);
+  border-top-color: var(--border-color-theme);
+}
+
+/*
+ * GROWI admin page #layoutOptions #themeOptions
+ */
+.admin-page {
+  #layoutOptions {
+    .customize-layout-card {
+      &.border-active {
+        border-color: var(--color-theme-color-box);
+      }
+    }
+  }
+
+  #themeOptions {
+    .theme-option-container.active {
+      .theme-option-name {
+        color: var(--color-global);
+      }
+      a {
+        background-color: var(--color-theme-color-box);
+        border-color: var(--color-theme-color-box);
+      }
+    }
+  }
+}
+
+/*
+ * HackMd
+ */
+.bg-box {
+  background-color: var(--bgcolor-global);
+}
+
+/*
+  Slack Integration
+*/
+.selecting-bot-type {
+  .bot-type-disc {
+    width: 20px;
+  }
+}
+
+/*
+  In App Notification
+*/
+.grw-unopend-notification {
+  width: 7px;
+  height: 7px;
+  background-color: var(--primary);
+}
+
+/*
+Emoji picker modal
+*/
+.emoji-picker-modal {
+  background-color: transparent !important;
+}
+
+/*
+Expand / compress button bookmark list on users page
+*/
+.grw-user-page-list-m {
+  .grw-expand-compress-btn {
+    color: $body-color;
+    background-color: $body-bg;
+    &.active {
+      background-color: hsl.darken($body-bg, 12%),
+    }
+  }
+}
+
+/*
+ * Questionnaire modal
+ */
+.grw-questionnaire-btn-group {
+  .btn-outline-primary {
+    @include hsl-button.button-outline-variant(
+      #{hsl.lighten(var(--primary), 30%)} !important,
+      #{hsl.contrast(var(--primary))} !important,
+      var(--primary) !important,
+      #{hsl.lighten(var(--primary), 30%)} !important,
+    );
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: #{hsl.contrast(var(--primary))} !important;
+      background-color: var(--primary) !important;
+    }
+  }
+}
+
+/*
+ * revision-history-diff
+ */
+.revision-history-diff {
+  background-color: white;
+}

+ 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?"
   },
   "page_comment": {
+    "comments": "Commments",
+    "comment": "Commment",
     "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/ja_JP/translation.json

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

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

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

+ 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 { pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { debounce } from 'throttle-debounce';
 
@@ -13,7 +14,6 @@ import { useCurrentUser } from '../stores/context';
 
 import type { CommentEditorProps } from './PageComment/CommentEditor';
 
-
 const { isTopPage } = pagePathUtils;
 
 
@@ -33,6 +33,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     pageId, pagePath, revision, onLoaded,
   } = props;
 
+  const { t } = useTranslation('');
+
   const { mutate } = useSWRxPageComment(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { data: isDeleted } = useIsTrashPage();
@@ -69,7 +71,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   };
 
   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}>
         <PageComment
           pageId={pageId}

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

@@ -9,8 +9,7 @@
 
   // reply button
   .btn-comment-reply {
-    margin-top: 0.5em;
-    border: none;
+    backdrop-filter: blur(10px);
   }
 
   // TODO: Refacotr Soft-coding
@@ -21,3 +20,31 @@
     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, {
-  FC, useState, useMemo, memo, useCallback,
+  useState, useMemo, memo, useCallback,
 } from 'react';
 
 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 { toastError } from '~/client/util/toastr';
-import { RendererOptions } from '~/interfaces/renderer-options';
+import type { RendererOptions } from '~/interfaces/renderer-options';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+import type { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
@@ -153,10 +154,10 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   return (
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
       <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) => {
 
-            const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+            const defaultCommentThreadClasses = 'page-comment-thread mb-2';
             const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
 
             let commentThreadClasses = '';
@@ -170,16 +171,15 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                   <div className="d-flex flex-row-reverse">
                     <NotAvailableForGuest>
                       <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)}
                         >
-                          <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>
                     </NotAvailableForGuest>
                   </div>

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

@@ -4,11 +4,6 @@
 @use './_comment-inheritance';
 
 .comment-styles :global {
-  .page-comment-writer {
-    @include bs.media-breakpoint-down(xs) {
-      height: 3.5em;
-    }
-  }
 
   .page-comment {
     position: relative;
@@ -16,11 +11,9 @@
 
     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
@@ -31,14 +24,6 @@
     // comment section
     .page-comment-main {
       @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;
 
@@ -57,8 +42,10 @@
 
     // comment body
     .page-comment-body {
-      margin-bottom: 0.5em;
       word-wrap: break-word;
+      .wiki p {
+        margin: 8px 0;
+      }
     }
 
     // older comments
@@ -73,6 +60,14 @@
       }
     }
 
+    .page-comment-revision {
+      .material-symbols-outlined {
+        font-size: 16px;
+        vertical-align: middle;
+      }
+    }
+
+
     .page-comment-meta {
       display: flex;
       justify-content: flex-end;
@@ -81,10 +76,6 @@
       color: bs.$gray-400;
     }
 
-    .page-comment-revision svg {
-      width: 16px;
-      height: 16px;
-    }
   }
 
   // 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);
   };
 
-  const renderText = (comment: string) => {
-    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
-  };
-
   const commentBody = useMemo(() => {
     if (rendererOptions == null) {
       return <></>;
@@ -151,24 +147,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
         />
       ) : (
         <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} />
               </Link>
-              { isEdited && (
-                <>
-                  <span id={editedDateId}>&nbsp;(edited)</span>
-                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
-                </>
-              ) }
               <span className="ms-2">
                 <Link
                   id={`page-comment-revision-${commentId}`}
@@ -183,6 +170,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
                 </UncontrolledTooltip>
               </span>
             </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) && (
               <CommentControl
                 onClickDeleteBtn={deleteBtnClickedHandler}

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

@@ -13,13 +13,13 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
   return (
     // The page-comment-control class is imported from Comment.module.scss
     <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>
       </button>
       <button
         data-testid="comment-delete-button"
         type="button"
-        className="btn btn-link p-2 me-2"
+        className="btn btn-link p-2 me-2 opacity-50"
         onClick={onClickDeleteBtn}
       >
         <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
 .comment-editor-styles :global {
-  .cm-editor {
-    height: 300px !important;
-  }
-
   .comment-form {
     position: relative;
-    margin-top: 1em;
+
+    // background
+    .bg-comment {
+      @extend %bg-comment
+    }
 
     // user icon
     .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;
   }
 }

+ 39 - 59
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -6,10 +6,11 @@ import {
   CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import {
-  Button, TabContent, TabPane,
+  TabContent, TabPane,
 } from 'reactstrap';
 
 import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
@@ -26,11 +27,12 @@ import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
-import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 import { CommentPreview } from './CommentPreview';
+import { SwitchingButtonGroup } from './SwitchingButtonGroup';
+
 
 import '@growi/editor/dist/style.css';
 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 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 = {
   pageId: string,
   isForNewComment?: boolean,
@@ -92,11 +82,13 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');
-  const [activeTab, setActiveTab] = useState('comment_editor');
+  const [showPreview, setShowPreview] = useState(false);
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [incremented, setIncremented] = useState(false);
 
+  const { t } = useTranslation('');
+
   const editorRef = useRef<IEditorMethods>(null);
 
   const router = useRouter();
@@ -113,8 +105,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     };
   }, [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
@@ -140,7 +132,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
 
     setComment('');
-    setActiveTab('comment_editor');
+    setShowPreview(false);
     setError(undefined);
     initializeSlackEnabled();
     // reset value
@@ -255,22 +247,24 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
   const renderBeforeReady = useCallback((): JSX.Element => {
     return (
-      <div className="text-center">
+      <div>
         <NotAvailableForGuest>
           <NotAvailableForReadOnlyUser>
             <button
               type="button"
-              className="btn btn-lg btn-link"
+              className="btn btn-outline-primary w-100 text-start py-3"
               onClick={() => setIsReadyToUse(true)}
               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>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
       </div>
     );
-  }, []);
+  }, [currentUser]);
 
   // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
   //   setComment(newValue);
@@ -304,33 +298,36 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
     const errorMessage = <span className="text-danger text-end me-2">{error}</span>;
     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}
       >
-        Cancel
-      </Button>
+        {t('Cancel')}
+      </button>
     );
     const submitButton = (
-      <Button
+      <button
+        type="button"
         data-testid="comment-submit-button"
-        outline
-        color="primary"
-        className="btn btn-outline-primary rounded-pill"
+        className="btn btn-primary"
         onClick={postCommentHandler}
       >
-        Comment
-      </Button>
+        {t('page_comment.comment')}
+      </button>
     );
 
     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">
               <CodeMirrorEditorComment
                 acceptedUploadFileType={acceptedUploadFileType}
@@ -339,37 +336,23 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onUpload={uploadHandler}
                 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 tabId="comment_preview">
-              <div className="comment-form-preview">
+              <div className="comment-preview-container">
                 {commentPreview}
               </div>
             </TabPane>
           </TabContent>
         </div>
 
-        <div className="comment-submit">
+        <div className="comment-submit px-4 pb-3 mb-2">
           <div className="d-flex">
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
 
             {isSlackConfigured && isSlackEnabled != null
               && (
-                <div className="align-self-center me-md-2">
+                <div className="align-self-center me-md-3">
                   <SlackNotification
                     isSlackEnabled={isSlackEnabled}
                     slackChannels={slackChannels}
@@ -398,10 +381,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   return (
     <div className={`${styles['comment-editor-styles']} form page-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
             ? renderReady()
             : 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 {
-  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';
 
+const moduleClass = styles['grw-comment-preview'] ?? '';
+
 
 type CommentPreviewPorps = {
   markdown: string,
@@ -21,7 +23,7 @@ export const CommentPreview = (props: CommentPreviewPorps): JSX.Element => {
   }
 
   return (
-    <div className={`grw-comment-preview ${styles['grw-comment-preview']}`}>
+    <div className={moduleClass}>
       <RevisionRenderer
         rendererOptions={rendererOptions}
         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
 .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) => {
     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
           rendererOptions={rendererOptions}
           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 {
-  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/PageEditor/_page-editor-inheritance.scss

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

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

@@ -205,7 +205,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   }, [isUploading, isDragAccept, isDragReject, acceptedUploadFileType]);
 
   return (
-    <div className={`${style['codemirror-editor']} flex-expand-vert overflow-y-hidden`}>
+    <div className={`${style['codemirror-editor']} flex-expand-vert overflow-y-hidden rounded`}>
       <div {...getRootProps()} className={`dropzone ${fileUploadState} flex-expand-vert`}>
         <input {...getInputProps()} />
         <FileDropzoneOverlay isEnabled={isDragActive} />

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

@@ -45,8 +45,6 @@ export const useCodeMirrorEditorIsolated = (
 
   if (shouldUpdate) {
     ref.current = newData;
-    // eslint-disable-next-line no-console
-    console.info('Initializing codemirror for main');
   }
 
   return useSWRStatic(swrKey, shouldUpdate ? newData : undefined);