Explorar el Código

Merge branch 'dev/7.0.x' into imprv/126522-change-font-size-in-editor-header-level

soumaeda hace 2 años
padre
commit
f65d2d7cc1
Se han modificado 98 ficheros con 1676 adiciones y 1201 borrados
  1. 0 67
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.module.scss
  2. 2 16
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.tsx
  3. 6 6
      apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss
  4. 1 0
      apps/app/package.json
  5. 2 2
      apps/app/public/static/locales/en_US/admin.json
  6. 2 2
      apps/app/public/static/locales/ja_JP/admin.json
  7. 2 2
      apps/app/public/static/locales/zh_CN/admin.json
  8. 2 2
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  9. 5 8
      apps/app/src/components/AuthorInfo/AuthorInfo.module.scss
  10. 5 1
      apps/app/src/components/AuthorInfo/AuthorInfo.tsx
  11. 1 0
      apps/app/src/components/AuthorInfo/index.ts
  12. 1 4
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx
  13. 0 0
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.module.scss
  14. 1 0
      apps/app/src/components/Common/CopyDropdown/index.ts
  15. 6 0
      apps/app/src/components/Common/PagePathHierarchicalLink/CollapsedParentsDropdown.module.scss
  16. 35 0
      apps/app/src/components/Common/PagePathHierarchicalLink/CollapsedParentsDropdown.tsx
  17. 4 0
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  18. 8 11
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  19. 1 0
      apps/app/src/components/Common/PagePathHierarchicalLink/index.ts
  20. 21 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  21. 121 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  22. 1 0
      apps/app/src/components/Common/PagePathNav/index.ts
  23. 0 0
      apps/app/src/components/Common/UserPictureList.jsx
  24. 0 0
      apps/app/src/components/ItemsTree/Item.module.scss
  25. 1 1
      apps/app/src/components/ItemsTree/ItemNode.ts
  26. 0 0
      apps/app/src/components/ItemsTree/ItemsTree.module.scss
  27. 7 9
      apps/app/src/components/ItemsTree/ItemsTree.tsx
  28. 2 2
      apps/app/src/components/ItemsTree/ItemsTreeContentSkeleton.tsx
  29. 2 0
      apps/app/src/components/ItemsTree/index.ts
  30. 0 10
      apps/app/src/components/Layout/Admin.module.scss
  31. 1 1
      apps/app/src/components/Layout/AdminLayout.tsx
  32. 2 1
      apps/app/src/components/Layout/BasicLayout.tsx
  33. 1 1
      apps/app/src/components/Layout/PageViewLayout.module.scss
  34. 3 1
      apps/app/src/components/Layout/PageViewLayout.tsx
  35. 4 26
      apps/app/src/components/Layout/SearchResultLayout.module.scss
  36. 4 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  37. 78 135
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  38. 9 10
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  39. 2 4
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  40. 6 0
      apps/app/src/components/Page/PageView.tsx
  41. 1 1
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx
  42. 5 0
      apps/app/src/components/PageAuthorInfo/PageAuthorInfo.module.scss
  43. 46 0
      apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx
  44. 2 2
      apps/app/src/components/PageContentFooter.tsx
  45. 0 0
      apps/app/src/components/PageControls/BookmarkButtons.module.scss
  46. 30 34
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  47. 0 0
      apps/app/src/components/PageControls/LikeButtons.module.scss
  48. 17 22
      apps/app/src/components/PageControls/LikeButtons.tsx
  49. 30 0
      apps/app/src/components/PageControls/PageControls.module.scss
  50. 19 18
      apps/app/src/components/PageControls/PageControls.tsx
  51. 0 4
      apps/app/src/components/PageControls/SeenUserInfo.module.scss
  52. 4 3
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  53. 0 0
      apps/app/src/components/PageControls/SubscribeButton.module.scss
  54. 0 0
      apps/app/src/components/PageControls/SubscribeButton.tsx
  55. 1 0
      apps/app/src/components/PageControls/index.ts
  56. 12 0
      apps/app/src/components/PageControls/user-list-popover.module.scss
  57. 1 0
      apps/app/src/components/PageEditor/Editor.tsx
  58. 1 1
      apps/app/src/components/PageList/PageListItemL.tsx
  59. 0 70
      apps/app/src/components/PagePathNav.tsx
  60. 65 0
      apps/app/src/components/PageSelectModal/PageSelectModal.tsx
  61. 30 0
      apps/app/src/components/PageSelectModal/TreeItemForModal.tsx
  62. 1 1
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  63. 0 6
      apps/app/src/components/SearchPage/SearchResultContent.module.scss
  64. 12 18
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  65. 6 0
      apps/app/src/components/ShareLinkPageView.tsx
  66. 0 569
      apps/app/src/components/Sidebar/PageTree/Item.tsx
  67. 3 3
      apps/app/src/components/Sidebar/PageTree/PageTree.tsx
  68. 5 1
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  69. 170 0
      apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx
  70. 177 0
      apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx
  71. 2 0
      apps/app/src/components/Sidebar/PageTreeItem/index.ts
  72. 2 2
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  73. 1 0
      apps/app/src/components/Sidebar/SidebarContents.tsx
  74. 18 0
      apps/app/src/components/TreeItem/ItemNode.ts
  75. 72 0
      apps/app/src/components/TreeItem/NewPageCreateButton.tsx
  76. 103 0
      apps/app/src/components/TreeItem/NewPageInput.tsx
  77. 278 0
      apps/app/src/components/TreeItem/SimpleItem.tsx
  78. 43 0
      apps/app/src/components/TreeItem/UseNewPageInput.tsx
  79. 3 0
      apps/app/src/components/TreeItem/index.ts
  80. 1 0
      apps/app/src/interfaces/ui.ts
  81. 4 8
      apps/app/src/pages/[[...path]].page.tsx
  82. 3 5
      apps/app/src/pages/share/[[...path]].page.tsx
  83. 7 16
      apps/app/src/pages/trash.page.tsx
  84. 36 2
      apps/app/src/stores/modal.tsx
  85. 0 40
      apps/app/src/styles/_editor.scss
  86. 1 0
      apps/app/src/styles/_layout.scss
  87. 0 16
      apps/app/src/styles/_mixins.scss
  88. 0 6
      apps/app/src/styles/_page-path.scss
  89. 0 7
      apps/app/src/styles/_variables.scss
  90. 0 1
      apps/app/src/styles/style-app.scss
  91. 3 3
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  92. 12 12
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--click-page-icons.cy.ts
  93. 4 4
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts
  94. 1 1
      apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts
  95. 1 1
      apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts
  96. 44 0
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  97. 7 1
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  98. 43 1
      yarn.lock

+ 0 - 67
apps/app/src/components/Navbar/GrowiSubNavigation.module.scss → apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -22,24 +22,11 @@
       min-height: var.$grw-subnav-min-height-md;
       min-height: var.$grw-subnav-min-height-md;
     }
     }
 
 
-    .grw-drawer-toggler {
-      width: 50px;
-      height: 50px;
-      font-size: 24px;
-    }
-
     h1 {
     h1 {
       @include mixins.variable-font-size(32px);
       @include mixins.variable-font-size(32px);
       line-height: 1.4em;
       line-height: 1.4em;
     }
     }
 
 
-    .grw-page-path-nav {
-      .separator {
-        margin-right: 0.2em;
-        margin-left: 0.2em;
-      }
-    }
-
     .btn-copy {
     .btn-copy {
       &:not(:hover):not(:active) {
       &:not(:hover):not(:active) {
         background-color: transparent !important;
         background-color: transparent !important;
@@ -47,19 +34,6 @@
       opacity: 0.5;
       opacity: 0.5;
     }
     }
 
 
-    .btn-edit-tags {
-      opacity: 0.5;
-
-      &.no-tags {
-        opacity: 0.7;
-      }
-    }
-
-    .btn-skeleton {
-      @extend %subnav-buttons-height;
-      width: 100%;
-    }
-
     .btn-subscribe {
     .btn-subscribe {
       @extend %subnav-buttons-height;
       @extend %subnav-buttons-height;
       font-size: 20px;
       font-size: 20px;
@@ -125,46 +99,5 @@
         opacity: unset;
         opacity: unset;
       }
       }
     }
     }
-
-    /*
-     * Compact Mode
-     */
-    &.grw-subnav-compact {
-      min-height: 70px;
-
-      @include bs.media-breakpoint-up(md) {
-        min-height: 90px;
-      }
-
-      .btn-skeleton {
-        @extend %compact-subnav-buttons-height;
-        width: 100%;
-      }
-
-      .btn-like,
-      .btn-bookmark,
-      .btn-subscribe {
-        width: 32px;
-        @extend %compact-subnav-buttons-height;
-        padding: 4px;
-        font-size: 16px;
-      }
-      .btn-seen-user {
-        width: 48px;
-        @extend %compact-subnav-buttons-height;
-        padding: 4px;
-        font-size: 16px;
-
-        svg {
-          width: 16px;
-          height: 16px;
-        }
-      }
-      .btn-page-item-control {
-        width: 32px;
-        @extend %compact-subnav-buttons-height;
-        font-size: 12px;
-      }
-    }
   }
   }
 }
 }

+ 2 - 16
apps/app/src/components/Navbar/GrowiSubNavigation.tsx → apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.tsx

@@ -6,8 +6,6 @@ import {
 
 
 import PagePathNav from '../PagePathNav';
 import PagePathNav from '../PagePathNav';
 
 
-import DrawerToggler from './DrawerToggler';
-
 
 
 import styles from './GrowiSubNavigation.module.scss';
 import styles from './GrowiSubNavigation.module.scss';
 
 
@@ -16,10 +14,7 @@ export type GrowiSubNavigationProps = {
   pagePath?: string,
   pagePath?: string,
   pageId?: string,
   pageId?: string,
   isNotFound?: boolean,
   isNotFound?: boolean,
-  showDrawerToggler?: boolean,
   isTagLabelsDisabled?: boolean,
   isTagLabelsDisabled?: boolean,
-  isDrawerMode?: boolean,
-  isCompactMode?: boolean,
   tags?: string[],
   tags?: string[],
   rightComponent?: React.FunctionComponent,
   rightComponent?: React.FunctionComponent,
   additionalClasses?: string[],
   additionalClasses?: string[],
@@ -31,32 +26,23 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
 
 
   const {
   const {
     pageId, pagePath,
     pageId, pagePath,
-    showDrawerToggler,
-    isDrawerMode, isCompactMode,
     rightComponent: RightComponent,
     rightComponent: RightComponent,
     additionalClasses = [],
     additionalClasses = [],
   } = props;
   } = props;
 
 
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
   const isEditorMode = !isViewMode;
   const isEditorMode = !isViewMode;
-  const compactModeClasses = isCompactMode ? 'grw-subnav-compact d-print-none' : '';
 
 
   return (
   return (
     <div className={`
     <div className={`
       grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between
       grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between
-      ${additionalClasses.join(' ')}
-      ${compactModeClasses}`}
+      ${additionalClasses.join(' ')}`}
     >
     >
       {/* Left side */}
       {/* Left side */}
       <div className="d-flex grw-subnav-start-side">
       <div className="d-flex grw-subnav-start-side">
-        { (showDrawerToggler && isDrawerMode) && (
-          <div className={`d-none d-md-flex align-items-center ${isEditorMode ? 'me-2 pe-2' : 'border-end me-4 pe-4'}`}>
-            <DrawerToggler />
-          </div>
-        ) }
         <div className="grw-path-nav-container">
         <div className="grw-path-nav-container">
           { pagePath != null && (
           { pagePath != null && (
-            <PagePathNav pageId={pageId} pagePath={pagePath} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
+            <PagePathNav pageId={pageId} pagePath={pagePath} isSingleLineMode={isEditorMode} />
           ) }
           ) }
         </div>
         </div>
       </div>
       </div>

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

@@ -180,9 +180,9 @@
   /*
   /*
   * GROWI subnavigation
   * GROWI subnavigation
   */
   */
-  .grw-subnav {
-    background-color: var(--bgcolor-subnav);
-  }
+  // .grw-subnav {
+  //   background-color: var(--bgcolor-subnav);
+  // }
 
 
   .grw-subnav-fixed-container .grw-subnav {
   .grw-subnav-fixed-container .grw-subnav {
     background-color: hsl.alpha(var(--bgcolor-subnav),85%);
     background-color: hsl.alpha(var(--bgcolor-subnav),85%);
@@ -207,9 +207,9 @@
   /**
   /**
    * GROWI PagePathHierarchicalLink
    * GROWI PagePathHierarchicalLink
    */
    */
-  .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
-    color: $gray-600;
-  }
+  // .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
+  //   color: $gray-600;
+  // }
   /*
   /*
   * GROWI Sidebar
   * GROWI Sidebar
   */
   */

+ 1 - 0
apps/app/package.json

@@ -247,6 +247,7 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-dropzone": "^11.2.4",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
+    "react-stickynode": "^4.1.0",
     "rehype-rewrite": "^3.0.6",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",
     "sass": "^1.53.0",

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

@@ -488,8 +488,8 @@
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "presentation_docs" : "GROWI Docs - Create slides for a presentation",
-      "presentation_docs_link": "https://docs.growi.org/en/guide/features/presentation.html"
+      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
+      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     },
     "custom_title": "Custom title",
     "custom_title": "Custom title",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",

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

@@ -497,8 +497,8 @@
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "presentation_docs" : "参考:GROWI Docs - プレゼンテーション機能を使う",
-      "presentation_docs_link": "https://docs.growi.org/ja/guide/features/presentation.html"
+      "marp_in_growi" : "参考:GROWI Docs - Marp でスライドを作成する",
+      "marp_in_growi_link": "https://docs.growi.org/ja/guide/features/marp.html"
     },
     },
     "custom_title": "カスタム Title",
     "custom_title": "カスタム Title",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",

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

@@ -496,8 +496,8 @@
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "presentation_docs" : "参考资料:GROWI Docs - Create slides for a presentation",
-      "presentation_docs_link": "https://docs.growi.org/en/guide/features/presentation.html"
+      "marp_in_growi" : "参考资料:GROWI Docs - Create slide using Marp",
+      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     },
     "custom_title": "自定义标题",
     "custom_title": "自定义标题",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -51,10 +51,10 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
               </a>
               </a>
               <br></br>
               <br></br>
               <a
               <a
-                href={`${t('admin:customize_settings.presentation_options.presentation_docs_link')}`}
+                href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presentation_options.presentation_docs')}`}
+              >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
               </a>
               </a>
             </p>
             </p>
           </CustomizePresentationOption>
           </CustomizePresentationOption>

+ 5 - 8
apps/app/src/components/Navbar/AuthorInfo.module.scss → apps/app/src/components/AuthorInfo/AuthorInfo.module.scss

@@ -4,10 +4,7 @@ $author-font-size: 12px;
 $date-font-size: 11px;
 $date-font-size: 11px;
 
 
 .grw-author-info :global {
 .grw-author-info :global {
-  li {
-    font-size: $author-font-size;
-    list-style: none;
-  }
+  font-size: $author-font-size;
 
 
   .text-date {
   .text-date {
     font-size: $date-font-size;
     font-size: $date-font-size;
@@ -25,7 +22,7 @@ $date-font-size: 11px;
   }
   }
 }
 }
 
 
-.grw-author-info-skeleton :global {
-  width: 139px;
-  height: calc((#{$author-font-size} + #{$date-font-size}) * #{bs.$line-height-base});
-}
+// .grw-author-info-skeleton :global {
+//   width: 139px;
+//   height: calc((#{$author-font-size} + #{$date-font-size}) * #{bs.$line-height-base});
+// }

+ 5 - 1
apps/app/src/components/Navbar/AuthorInfo.tsx → apps/app/src/components/AuthorInfo/AuthorInfo.tsx

@@ -6,6 +6,10 @@ import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
+
+import styles from './AuthorInfo.module.scss';
+
+
 export type AuthorInfoProps = {
 export type AuthorInfoProps = {
   date: Date,
   date: Date,
   user: IUser,
   user: IUser,
@@ -59,7 +63,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
   };
   };
 
 
   return (
   return (
-    <div className="d-flex align-items-center">
+    <div className={`grw-author-info ${styles['grw-author-info']} d-flex align-items-center`}>
       <div className="me-2">
       <div className="me-2">
         <UserPicture user={user} size="sm" />
         <UserPicture user={user} size="sm" />
       </div>
       </div>

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

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

+ 1 - 4
apps/app/src/components/Page/CopyDropdown.jsx → apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -25,7 +25,7 @@ const DropdownItemContents = ({ title, contents }) => (
 /* eslint-enable react/prop-types */
 /* eslint-enable react/prop-types */
 
 
 
 
-const CopyDropdown = (props) => {
+export const CopyDropdown = (props) => {
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
@@ -120,7 +120,6 @@ const CopyDropdown = (props) => {
 
 
         <DropdownMenu
         <DropdownMenu
           strategy="fixed"
           strategy="fixed"
-          style={{ zIndex: 1016 }} /* zIndex: 1016 // larger than z-index value of grw-subnav-fixed-container in GrowiSubNavigationSwitcher.module.scss */
         >
         >
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">
@@ -212,5 +211,3 @@ CopyDropdown.propTypes = {
   dropdownToggleClassName: PropTypes.string,
   dropdownToggleClassName: PropTypes.string,
   isShareLinkMode: PropTypes.bool,
   isShareLinkMode: PropTypes.bool,
 };
 };
-
-export default CopyDropdown;

+ 0 - 0
apps/app/src/components/Page/CopyDropdown.module.scss → apps/app/src/components/Common/CopyDropdown/CopyDropdown.module.scss


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

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

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

@@ -0,0 +1,6 @@
+
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.collapsed-parents-dropdown-menu {
+  --bs-dropdown-zindex: #{bs.$zindex-fixed};
+}

+ 35 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/CollapsedParentsDropdown.tsx

@@ -0,0 +1,35 @@
+import {
+  DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown,
+} from 'reactstrap';
+
+import LinkedPagePath from '~/models/linked-page-path';
+
+
+import styles from './CollapsedParentsDropdown.module.scss';
+
+
+type Props = {
+  linkedPagePath: LinkedPagePath,
+}
+
+export const CollapsedParentsDropdown = (props: Props): JSX.Element => {
+  const { linkedPagePath } = props;
+
+  return (
+    <UncontrolledDropdown className="d-inline-block">
+      <DropdownToggle color="transparent">...</DropdownToggle>
+      <DropdownMenu className={`dropdown-menu ${styles['collapsed-parents-dropdown-menu']}`} container="body">
+        {/* TODO: generate DropdownItems */}
+        <DropdownItem>
+          <a role="menuitem">foo</a>
+        </DropdownItem>
+        <DropdownItem>
+          <a role="menuitem">bar</a>
+        </DropdownItem>
+        <DropdownItem>
+          <a role="menuitem">baz</a>
+        </DropdownItem>
+      </DropdownMenu>
+    </UncontrolledDropdown>
+  );
+};

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

@@ -0,0 +1,4 @@
+.separator {
+  margin-right: 0.2em;
+  margin-left: 0.2em;
+}

+ 8 - 11
apps/app/src/components/PagePathHierarchicalLink.tsx → apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -3,7 +3,9 @@ import React, { memo, useCallback } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import LinkedPagePath from '../models/linked-page-path';
+import LinkedPagePath from '../../../models/linked-page-path';
+
+import styles from './PagePathHierarchicalLink.module.scss';
 
 
 
 
 type PagePathHierarchicalLinkProps = {
 type PagePathHierarchicalLinkProps = {
@@ -16,8 +18,7 @@ type PagePathHierarchicalLinkProps = {
   isInnerElem?: boolean,
   isInnerElem?: boolean,
 };
 };
 
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
+export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
   const {
   const {
     linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
     linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
   } = props;
   } = props;
@@ -26,7 +27,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
   const RootElm = useCallback(({ children }) => {
   const RootElm = useCallback(({ children }) => {
     return isInnerElem
     return isInnerElem
       ? <>{children}</>
       ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
+      : <span className="text-break">{children}</span>;
   }, [isInnerElem]);
   }, [isInnerElem]);
 
 
   // render root element
   // render root element
@@ -43,7 +44,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
               <i className="icon-trash"></i>
               <i className="icon-trash"></i>
             </Link>
             </Link>
           </span>
           </span>
-          <span className="separator"><a href="/">/</a></span>
+          <span className={`separator ${styles.separator}`}><a href="/">/</a></span>
         </RootElm>
         </RootElm>
       )
       )
       : (
       : (
@@ -51,7 +52,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/" prefetch={false}>
             <Link href="/" prefetch={false}>
               <i className="icon-home"></i>
               <i className="icon-home"></i>
-              <span className="separator">/</span>
+              <span className={`separator ${styles.separator}`}>/</span>
             </Link>
             </Link>
           </span>
           </span>
         </RootElm>
         </RootElm>
@@ -78,7 +79,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
         />
         />
       ) }
       ) }
       { isSeparatorRequired && (
       { isSeparatorRequired && (
-        <span className="separator">/</span>
+        <span className={`separator ${styles.separator}`}>/</span>
       ) }
       ) }
 
 
       <Link href={href} prefetch={false} legacyBehavior>
       <Link href={href} prefetch={false} legacyBehavior>
@@ -93,7 +94,3 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
     </RootElm>
     </RootElm>
   );
   );
 });
 });
-PagePathHierarchicalLink.displayName = 'PagePathHierarchicalLink';
-
-
-export default PagePathHierarchicalLink;

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

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

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

@@ -0,0 +1,21 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.grw-mr-02em {
+  margin-right: 0.2em;
+}
+
+.grw-page-path-nav-sticky :global {
+  min-height: 75px;
+
+  .sticky-inner-wrapper {
+    z-index: bs.$zindex-sticky;
+  }
+
+  // set smaller font-size when sticky
+  .sticky-inner-wrapper.active {
+    h1 {
+      font-size: 1.75rem !important;
+    }
+  }
+}
+

+ 121 - 0
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -0,0 +1,121 @@
+import React, { FC } from 'react';
+
+import { DevidedPagePath } from '@growi/core/dist/models';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import dynamic from 'next/dynamic';
+import Sticky from 'react-stickynode';
+
+import { useIsNotFound } from '~/stores/page';
+
+import LinkedPagePath from '../../../models/linked-page-path';
+import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
+import { CollapsedParentsDropdown } from '../PagePathHierarchicalLink/CollapsedParentsDropdown';
+
+import styles from './PagePathNav.module.scss';
+
+
+const { isTrashPage } = pagePathUtils;
+
+type Props = {
+  pagePath: string,
+  pageId?: string | null,
+  isSingleLineMode?: boolean,
+  isCollapseParents?: boolean,
+  formerLinkClassName?: string,
+  latterLinkClassName?: string,
+}
+
+const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
+
+const Separator = (): JSX.Element => {
+  return <span className={styles['grw-mr-02em']}>/</span>;
+};
+
+export const PagePathNav: FC<Props> = (props: Props) => {
+  const {
+    pageId, pagePath, isSingleLineMode, isCollapseParents,
+    formerLinkClassName, latterLinkClassName,
+  } = props;
+  const dPagePath = new DevidedPagePath(pagePath, false, true);
+
+  const { data: isNotFound } = useIsNotFound();
+
+  const isInTrash = isTrashPage(pagePath);
+
+  let formerLink;
+  let latterLink;
+
+  // one line
+  if (dPagePath.isRoot || dPagePath.isFormerRoot || (!isCollapseParents && isSingleLineMode)) {
+    const linkedPagePath = new LinkedPagePath(pagePath);
+    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+  }
+  // collapse parents
+  else if (isCollapseParents) {
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+    latterLink = (
+      <>
+        <CollapsedParentsDropdown linkedPagePath={linkedPagePathFormer} />
+        <Separator />
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+      </>
+    );
+  }
+  // two line
+  else {
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+    formerLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />;
+    latterLink = (
+      <>
+        <Separator />
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+      </>
+    );
+  }
+
+  const copyDropdownId = `copydropdown-${pageId}`;
+  const copyDropdownToggleClassName = 'd-block btn-outline-secondary btn-copy border-0 text-muted p-2';
+
+  return (
+    <div>
+      <span className={formerLinkClassName}>{formerLink}</span>
+      <div className="d-flex align-items-center">
+        <h1 className={`m-0 text-truncate ${latterLinkClassName}`}>
+          {latterLink}
+        </h1>
+        { pageId != null && !isNotFound && (
+          <div className="mx-2">
+            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName={copyDropdownToggleClassName}>
+              <i className="ti ti-clipboard"></i>
+            </CopyDropdown>
+          </div>
+        ) }
+      </div>
+    </div>
+  );
+};
+
+
+type PagePathNavStickyProps = Omit<Props, 'isCollapseParents'>;
+
+export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element => {
+  return (
+    // Controlling pointer-events
+    //  1. disable pointer-events with 'pe-none'
+    <Sticky className={`${styles['grw-page-path-nav-sticky']} mb-4`} innerClass="mt-1 pe-none" innerActiveClass="active">
+      {({ status }: { status: boolean }) => {
+        const isCollapseParents = status === Sticky.STATUS_FIXED;
+        return (
+          // Controlling pointer-events
+          //  2. enable pointer-events with 'pe-auto' only against the children
+          //      which width is minimized by 'd-inline-block'
+          <div className="d-inline-block pe-auto">
+            <PagePathNav {...props} isCollapseParents={isCollapseParents} latterLinkClassName={isCollapseParents ? 'fs-3' : 'fs-2'} />
+          </div>
+        );
+      }}
+    </Sticky>
+  );
+};

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

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

+ 0 - 0
apps/app/src/components/User/UserPictureList.jsx → apps/app/src/components/Common/UserPictureList.jsx


+ 0 - 0
apps/app/src/components/Sidebar/PageTree/Item.module.scss → apps/app/src/components/ItemsTree/Item.module.scss


+ 1 - 1
apps/app/src/components/Sidebar/PageTree/ItemNode.ts → apps/app/src/components/ItemsTree/ItemNode.ts

@@ -1,4 +1,4 @@
-import { IPageForItem } from '../../../interfaces/page';
+import { IPageForItem } from '../../interfaces/page';
 
 
 export class ItemNode {
 export class ItemNode {
 
 

+ 0 - 0
apps/app/src/components/Sidebar/PageTree/ItemsTree.module.scss → apps/app/src/components/ItemsTree/ItemsTree.module.scss


+ 7 - 9
apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx → apps/app/src/components/ItemsTree/ItemsTree.tsx

@@ -25,10 +25,9 @@ import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { ItemNode, SimpleItemProps } from '../TreeItem';
 
 
-import Item from './Item';
-import { ItemNode } from './ItemNode';
-import PageTreeContentSkeleton from './PageTreeContentSkeleton';
+import ItemsTreeContentSkeleton from './ItemsTreeContentSkeleton';
 
 
 import styles from './ItemsTree.module.scss';
 import styles from './ItemsTree.module.scss';
 
 
@@ -93,14 +92,15 @@ type ItemsTreeProps = {
   targetPath: string
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
   targetAndAncestorsData?: TargetAndAncestors
+  CustomTreeItem: React.FunctionComponent<SimpleItemProps>
 }
 }
 
 
 /*
 /*
  * ItemsTree
  * ItemsTree
  */
  */
-const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
+export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, CustomTreeItem,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -272,7 +272,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
       <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
       <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
-        <Item
+        <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}
@@ -287,7 +287,5 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     );
     );
   }
   }
 
 
-  return <PageTreeContentSkeleton />;
+  return <ItemsTreeContentSkeleton />;
 };
 };
-
-export default ItemsTree;

+ 2 - 2
apps/app/src/components/Sidebar/PageTree/PageTreeContentSkeleton.tsx → apps/app/src/components/ItemsTree/ItemsTreeContentSkeleton.tsx

@@ -4,7 +4,7 @@ import { Skeleton } from '~/components/Skeleton';
 
 
 import styles from './ItemsTree.module.scss';
 import styles from './ItemsTree.module.scss';
 
 
-const PageTreeContentSkeleton = (): JSX.Element => {
+const ItemsTreeContentSkeleton = (): JSX.Element => {
 
 
   return (
   return (
     <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`}>
     <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`}>
@@ -15,4 +15,4 @@ const PageTreeContentSkeleton = (): JSX.Element => {
   );
   );
 };
 };
 
 
-export default PageTreeContentSkeleton;
+export default ItemsTreeContentSkeleton;

+ 2 - 0
apps/app/src/components/ItemsTree/index.ts

@@ -0,0 +1,2 @@
+export { ItemNode } from './ItemNode';
+export * from './ItemsTree';

+ 0 - 10
apps/app/src/components/Layout/Admin.module.scss

@@ -5,16 +5,6 @@ $slack-work-space-name-card-background: #fff5ff;
 $slack-work-space-name-card-border: #efc1f6;
 $slack-work-space-name-card-border: #efc1f6;
 
 
 .admin-page :global {
 .admin-page :global {
-  .title {
-    padding-top: 1rem;
-    padding-bottom: 1rem;
-
-    line-height: 1em;
-
-    @include mixins.variable-font-size(28px);
-    line-height: 1.1em;
-  }
-
   .admin-user-menu {
   .admin-user-menu {
     .dropdown-menu {
     .dropdown-menu {
       right: 0;
       right: 0;

+ 1 - 1
apps/app/src/components/Layout/AdminLayout.tsx

@@ -29,7 +29,7 @@ const AdminLayout = ({
       <div className={`admin-page ${styles['admin-page']}`}>
       <div className={`admin-page ${styles['admin-page']}`}>
 
 
         <header className="py-0 container-fluid">
         <header className="py-0 container-fluid">
-          <h1 className="title px-3">{componentTitle}</h1>
+          <h1 className="p-3 fs-2">{componentTitle}</h1>
         </header>
         </header>
         <div id="main" className="main">
         <div id="main" className="main">
           <div className="container-fluid">
           <div className="container-fluid">

+ 2 - 1
apps/app/src/components/Layout/BasicLayout.tsx

@@ -30,6 +30,7 @@ type Props = {
   className?: string
   className?: string
 }
 }
 
 
+
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
   return (
     <RawLayout className={className ?? ''}>
     <RawLayout className={className ?? ''}>
@@ -38,7 +39,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <div className="page-wrapper flex-row">
         <div className="page-wrapper flex-row">
           <Sidebar />
           <Sidebar />
 
 
-          <div className="flex-expand-vert">{/* neccessary for nested {children} make expanded */}
+          <div className="d-flex flex-grow-1 flex-column">{/* neccessary for nested {children} make expanded */}
             <AlertSiteUrlUndefined />
             <AlertSiteUrlUndefined />
             {children}
             {children}
           </div>
           </div>

+ 1 - 1
apps/app/src/components/Layout/PageViewLayout.module.scss

@@ -1,3 +1,3 @@
 .page-view-layout :global {
 .page-view-layout :global {
-  min-height: calc(100vh - 116px - 250px); // 100vh - subnavigation height - page-comments-row minimum height
+  min-height: calc(100vh - 48px - 250px); // 100vh - subnavigation height - page-comments-row minimum height
 }
 }

+ 3 - 1
apps/app/src/components/Layout/PageViewLayout.tsx

@@ -4,19 +4,21 @@ import styles from './PageViewLayout.module.scss';
 
 
 type Props = {
 type Props = {
   children?: ReactNode,
   children?: ReactNode,
+  headerContents?: ReactNode,
   sideContents?: ReactNode,
   sideContents?: ReactNode,
   footerContents?: ReactNode,
   footerContents?: ReactNode,
 }
 }
 
 
 export const PageViewLayout = (props: Props): JSX.Element => {
 export const PageViewLayout = (props: Props): JSX.Element => {
   const {
   const {
-    children, sideContents, footerContents,
+    children, headerContents, sideContents, footerContents,
   } = props;
   } = props;
 
 
   return (
   return (
     <>
     <>
       <div id="main" className={`main page-view-layout ${styles['page-view-layout']}`}>
       <div id="main" className={`main page-view-layout ${styles['page-view-layout']}`}>
         <div id="content-main" className="content-main container-lg grw-container-convertible">
         <div id="content-main" className="content-main container-lg grw-container-convertible">
+          { headerContents != null && headerContents }
           { sideContents != null
           { sideContents != null
             ? (
             ? (
               <div className="d-flex flex-column flex-column-reverse flex-lg-row">
               <div className="d-flex flex-column flex-column-reverse flex-lg-row">

+ 4 - 26
apps/app/src/components/Layout/SearchResultLayout.module.scss

@@ -35,32 +35,10 @@
   }
   }
 
 
   .search-result-content {
   .search-result-content {
-    .search-result-content-nav {
-      min-height: var.$grw-subnav-search-preview-min-height;
-      overflow: auto;
-
-      .grw-subnav {
-        min-height: inherit;
-      }
-    }
-
-    .search-result-content {
-
-      > h2 {
-        margin-right: 10px;
-        font-size: 22px;
-        line-height: 1em;
-      }
-
-      &:first-child > h2 {
-        margin-top: 0;
-      }
-
-      .search-result-content-body-container {
-        .wiki {
-          padding: 16px;
-          font-size: 14px;
-        }
+    .search-result-content-body-container {
+      .wiki {
+        margin-top: 2em;
+        font-size: 14px;
       }
       }
     }
     }
   }
   }

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

@@ -0,0 +1,4 @@
+.grw-contextual-sub-navigation :global {
+  background-color: rgba(var(--bs-body-bg-rgb), 0.7);
+  backdrop-filter: blur(35px);
+}

+ 78 - 135
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
 
 
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import type {
 import type {
-  IUser, IPagePopulatedToShowRevision,
+  IPagePopulatedToShowRevision,
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '@growi/core';
 } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
@@ -12,22 +12,22 @@ import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
   useCurrentPathname,
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
-  usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
+  usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 import {
 import {
-  useSWRMUTxCurrentPage, useCurrentPageId, useIsNotFound, useSWRxPageInfo,
+  useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
 } from '~/stores/page';
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { mutatePageTree } from '~/stores/page-listing';
 import {
 import {
-  EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement,
-  useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
+  useEditorMode, useIsAbleToShowPageManagement,
+  useIsAbleToChangeEditorMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import CreateTemplateModal from '../CreateTemplateModal';
 import CreateTemplateModal from '../CreateTemplateModal';
@@ -38,30 +38,19 @@ import ShareLinkIcon from '../Icons/ShareLinkIcon';
 import { NotAvailable } from '../NotAvailable';
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 import { Skeleton } from '../Skeleton';
 
 
-import type { AuthorInfoProps } from './AuthorInfo';
-import { GrowiSubNavigation } from './GrowiSubNavigation';
-import type { SubNavButtonsProps } from './SubNavButtons';
-
-import AuthorInfoStyles from './AuthorInfo.module.scss';
+import styles from './GrowiContextualSubNavigation.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
-const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
-
 
 
 const PageEditorModeManager = dynamic(
 const PageEditorModeManager = dynamic(
   () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
 );
 );
-// TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal
-// { ssr: false, loading: () => <Skeleton additionalClass='btn-skeleton py-2' /> },
-const SubNavButtons = dynamic<SubNavButtonsProps>(
-  () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
+const PageControls = dynamic(
+  () => import('../PageControls').then(mod => mod.PageControls),
   { ssr: false, loading: () => <></> },
   { ssr: false, loading: () => <></> },
 );
 );
-const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), {
-  ssr: false,
-  loading: AuthorInfoSkeleton,
-});
+
 
 
 type PageOperationMenuItemsProps = {
 type PageOperationMenuItemsProps = {
   pageId: string,
   pageId: string,
@@ -183,8 +172,7 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
 
 
 type GrowiContextualSubNavigationProps = {
 type GrowiContextualSubNavigationProps = {
   currentPage?: IPagePopulatedToShowRevision | null,
   currentPage?: IPagePopulatedToShowRevision | null,
-  isCompactMode?: boolean,
-  isLinkSharingDisabled: boolean,
+  isLinkSharingDisabled?: boolean,
 };
 };
 
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
@@ -202,12 +190,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const revision = currentPage?.revision;
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
 
 
-  const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
-  const { data: isNotFound } = useIsNotFound();
-  const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
@@ -215,7 +200,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
-  const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
 
   // TODO: implement tags for editor
   // TODO: implement tags for editor
   // refs: https://redmine.weseek.co.jp/issues/132125
   // refs: https://redmine.weseek.co.jp/issues/132125
@@ -250,9 +234,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
 
-  const { isCompactMode, isLinkSharingDisabled } = props;
-
-  const isViewMode = editorMode === EditorMode.View;
+  const { isLinkSharingDisabled } = props;
 
 
 
 
   // TODO: implement tags for editor
   // TODO: implement tags for editor
@@ -309,124 +291,85 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     }
     }
   }, [isSharedPage, mutateCurrentPage]);
   }, [isSharedPage, mutateCurrentPage]);
 
 
-  const templateMenuItemClickHandler = useCallback(() => {
-    setIsPageTempleteModalShown(true);
-  }, []);
-
-
-  const RightComponent = () => {
-    const additionalMenuItemsRenderer = () => {
-      if (revisionId == null || pageId == null) {
-        return (
-          <>
-            {!isReadOnlyUser
-              && (
-                <CreateTemplateMenuItems
-                  onClickTemplateMenuItem={templateMenuItemClickHandler}
-                />
-              )
-            }
-          </>
-        );
-      }
+  const additionalMenuItemsRenderer = useCallback(() => {
+    if (revisionId == null || pageId == null) {
       return (
       return (
         <>
         <>
-          <PageOperationMenuItems
-            pageId={pageId}
-            revisionId={revisionId}
-            isLinkSharingDisabled={isLinkSharingDisabled}
-          />
-          {!isReadOnlyUser && (
-            <>
-              <DropdownItem divider />
+          {!isReadOnlyUser
+            && (
               <CreateTemplateMenuItems
               <CreateTemplateMenuItems
-                onClickTemplateMenuItem={templateMenuItemClickHandler}
+                onClickTemplateMenuItem={() => setIsPageTempleteModalShown(true)}
               />
               />
-            </>
-          )
+            )
           }
           }
         </>
         </>
       );
       );
-    };
-
+    }
     return (
     return (
       <>
       <>
-        <div className="d-flex">
-          <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
-            {isViewMode && (
-              <div className="h-50">
-                {pageId != null && (
-                  <SubNavButtons
-                    isCompactMode={isCompactMode}
-                    pageId={pageId}
-                    revisionId={revisionId}
-                    shareLinkId={shareLinkId}
-                    path={path ?? currentPathname} // If the page is empty, "path" is undefined
-                    expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
-                    disableSeenUserInfoPopover={isSharedUser}
-                    showPageControlDropdown={isAbleToShowPageManagement}
-                    additionalMenuItemRenderer={additionalMenuItemsRenderer}
-                    onClickDuplicateMenuItem={duplicateItemClickedHandler}
-                    onClickRenameMenuItem={renameItemClickedHandler}
-                    onClickDeleteMenuItem={deleteItemClickedHandler}
-                    onClickSwitchContentWidth={switchContentWidthHandler}
-                  />
-                )}
-              </div>
-            )}
-            {isAbleToChangeEditorMode && (
-              <PageEditorModeManager
-                editorMode={editorMode}
-                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
-                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-              />
-            )}
-          </div>
-          {(isAbleToShowPageAuthors && !isCompactMode && !pagePathUtils.isUsersHomepage(path ?? '')) && (
-            <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-start d-none d-lg-block d-edit-none py-2 ps-4 mb-0 ms-3`}>
-              <li className="pb-1">
-                {currentPage != null
-                  ? <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
-                  : <AuthorInfoSkeleton />
-                }
-              </li>
-              <li className="mt-1 pt-1 border-top">
-                {currentPage != null
-                  ? <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
-                  : <AuthorInfoSkeleton />
-                }
-              </li>
-            </ul>
-          )}
-        </div>
-
-        {path != null && currentUser != null && !isReadOnlyUser && (
-          <CreateTemplateModal
-            path={path}
-            isOpen={isPageTemplateModalShown}
-            onClose={() => setIsPageTempleteModalShown(false)}
-          />
-        )}
+        <PageOperationMenuItems
+          pageId={pageId}
+          revisionId={revisionId}
+          isLinkSharingDisabled={isLinkSharingDisabled}
+        />
+        {!isReadOnlyUser && (
+          <>
+            <DropdownItem divider />
+            <CreateTemplateMenuItems
+              onClickTemplateMenuItem={() => setIsPageTempleteModalShown(true)}
+            />
+          </>
+        )
+        }
       </>
       </>
     );
     );
-  };
-
-
-  const pagePath = isIdenticalPath || isNotFound
-    ? currentPathname
-    : currentPage?.path;
+  }, [isLinkSharingDisabled, isReadOnlyUser, pageId, revisionId]);
 
 
   return (
   return (
-    <GrowiSubNavigation
-      pagePath={pagePath}
-      pageId={currentPage?._id}
-      showDrawerToggler={isDrawerMode}
-      isDrawerMode={isDrawerMode}
-      isCompactMode={isCompactMode}
-      rightComponent={RightComponent}
-      additionalClasses={['container-fluid']}
-    />
+    <>
+      <div
+        className={`grw-contextual-sub-navigation ${styles['grw-contextual-sub-navigation']}
+          d-flex align-items-center justify-content-end px-2 py-1 gap-2 gap-md-4
+        `}
+        data-testid="grw-contextual-sub-nav"
+      >
+        <div className="h-50">
+          {pageId != null && (
+            <PageControls
+              pageId={pageId}
+              revisionId={revisionId}
+              shareLinkId={shareLinkId}
+              path={path ?? currentPathname} // If the page is empty, "path" is undefined
+              expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
+              disableSeenUserInfoPopover={isSharedUser}
+              showPageControlDropdown={isAbleToShowPageManagement}
+              additionalMenuItemRenderer={additionalMenuItemsRenderer}
+              onClickDuplicateMenuItem={duplicateItemClickedHandler}
+              onClickRenameMenuItem={renameItemClickedHandler}
+              onClickDeleteMenuItem={deleteItemClickedHandler}
+              onClickSwitchContentWidth={switchContentWidthHandler}
+            />
+          )}
+        </div>
+        {isAbleToChangeEditorMode && (
+          <PageEditorModeManager
+            editorMode={editorMode}
+            isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
+            onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
+          />
+        )}
+      </div>
+
+      {path != null && currentUser != null && !isReadOnlyUser && (
+        <CreateTemplateModal
+          path={path}
+          isOpen={isPageTemplateModalShown}
+          onClose={() => setIsPageTempleteModalShown(false)}
+        />
+      )}
+    </>
   );
   );
+
 };
 };
 
 
 
 

+ 9 - 10
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -7,22 +7,21 @@
     --bs-btn-font-size: 13px;
     --bs-btn-font-size: 13px;
     --bs-btn-border-width: 2px;
     --bs-btn-border-width: 2px;
 
 
-    width: 70px;
-
-    white-space: nowrap;
+    width: 90px;
+    @include bs.media-breakpoint-up(md) {
+      width: 70px;
+    }
 
 
     @include mixins.border-vertical('before', 70%, 1, true);
     @include mixins.border-vertical('before', 70%, 1, true);
-    .grw-page-editor-mode-manager-icon {
-      @include bs.media-breakpoint-down(sm) {
-        font-size: 16px;
-      }
-    }
   }
   }
 }
 }
 
 
 .grw-page-editor-mode-manager-skeleton :global {
 .grw-page-editor-mode-manager-skeleton :global {
-  width: 139px;
-  height: calc(var(--bs-btn-line-height) + bs.$btn-padding-y*2 + bs.$btn-border-width*2);
+  width: 179px;
+  @include bs.media-breakpoint-down(sm) {
+    width: 90px;
+  }
+  height: 30px;
 }
 }
 
 
 // == Colors
 // == Colors

+ 2 - 4
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -35,10 +35,8 @@ const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
       onClick={() => onClick?.(editorMode)}
       onClick={() => onClick?.(editorMode)}
       data-testid={`${editorMode}-button`}
       data-testid={`${editorMode}-button`}
     >
     >
-      <span className="d-flex flex-column flex-md-row justify-content-center">
-        <span className="grw-page-editor-mode-manager-icon me-md-1">{icon}</span>
-        <span className="grw-page-editor-mode-manager-label">{label}</span>
-      </span>
+      <span className="me-1">{icon}</span>
+      <span>{label}</span>
     </button>
     </button>
   );
   );
 });
 });

+ 6 - 0
apps/app/src/components/Page/PageView.tsx

@@ -17,6 +17,7 @@ import { useIsMobile } from '~/stores/ui';
 
 
 
 
 import type { CommentsProps } from '../Comments';
 import type { CommentsProps } from '../Comments';
+import { PagePathNavSticky } from '../Common/PagePathNav';
 import { PageViewLayout } from '../Layout/PageViewLayout';
 import { PageViewLayout } from '../Layout/PageViewLayout';
 import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import { PageContentFooter } from '../PageContentFooter';
@@ -98,6 +99,10 @@ export const PageView = (props: Props): JSX.Element => {
     }
     }
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
 
+  const headerContents = (
+    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} />
+  );
+
   const sideContents = !isNotFound && !isNotCreatable
   const sideContents = !isNotFound && !isNotCreatable
     ? (
     ? (
       <PageSideContents page={page} />
       <PageSideContents page={page} />
@@ -141,6 +146,7 @@ export const PageView = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <PageViewLayout
     <PageViewLayout
+      headerContents={headerContents}
       sideContents={sideContents}
       sideContents={sideContents}
       footerContents={footerContents}
       footerContents={footerContents}
     >
     >

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

@@ -3,7 +3,7 @@ import React from 'react';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import CopyDropdown from '../../Page/CopyDropdown';
+import { CopyDropdown } from '../../Common/CopyDropdown';
 
 
 
 
 type ShareLinkTrProps = {
 type ShareLinkTrProps = {

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

@@ -0,0 +1,5 @@
+.grw-page-author-info :global {
+  li {
+    list-style: none;
+  }
+}

+ 46 - 0
apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx

@@ -0,0 +1,46 @@
+import { memo } from 'react';
+
+import type { IUser } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
+
+import { useCurrentPathname } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useIsAbleToShowPageAuthors } from '~/stores/ui';
+
+import { AuthorInfo } from '../AuthorInfo';
+
+
+import styles from './PageAuthorInfo.module.scss';
+
+
+export const PageAuthorInfo = memo((): JSX.Element => {
+  const { data: currentPage } = useSWRxCurrentPage();
+
+  const { data: currentPathname } = useCurrentPathname();
+  const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
+
+  if (!isAbleToShowPageAuthors) {
+    return <></>;
+  }
+
+  const path = currentPage?.path ?? currentPathname;
+
+  if (pagePathUtils.isUsersHomepage(path ?? '')) {
+    return <></>;
+  }
+
+  return (
+    <ul className={`grw-page-author-info ${styles['grw-page-author-info']} text-nowrap border-start d-none d-lg-block d-edit-none py-2 ps-4 mb-0 ms-3`}>
+      <li className="pb-1">
+        {currentPage != null && (
+          <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
+        )}
+      </li>
+      <li className="mt-1 pt-1 border-top">
+        {currentPage != null && (
+          <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
+        )}
+      </li>
+    </ul>
+  );
+});

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

@@ -3,11 +3,11 @@ import React from 'react';
 import type { IPage, IUser } from '@growi/core';
 import type { IPage, IUser } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import type { AuthorInfoProps } from './Navbar/AuthorInfo';
+import type { AuthorInfoProps } from './AuthorInfo';
 
 
 import styles from './PageContentFooter.module.scss';
 import styles from './PageContentFooter.module.scss';
 
 
-const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./Navbar/AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
+const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
 
 
 export type PageContentFooterProps = {
 export type PageContentFooterProps = {
   page: IPage,
   page: IPage,

+ 0 - 0
apps/app/src/components/BookmarkButtons.module.scss → apps/app/src/components/PageControls/BookmarkButtons.module.scss


+ 30 - 34
apps/app/src/components/BookmarkButtons.tsx → apps/app/src/components/PageControls/BookmarkButtons.tsx

@@ -11,22 +11,22 @@ import UncontrolledTooltip from 'reactstrap/esm/UncontrolledTooltip';
 import { useSWRxBookmarkedUsers } from '~/stores/bookmark';
 import { useSWRxBookmarkedUsers } from '~/stores/bookmark';
 import { useIsGuestUser } from '~/stores/context';
 import { useIsGuestUser } from '~/stores/context';
 
 
-import { BookmarkFolderMenu } from './Bookmarks/BookmarkFolderMenu';
-import UserPictureList from './User/UserPictureList';
+import { BookmarkFolderMenu } from '../Bookmarks/BookmarkFolderMenu';
+import UserPictureList from '../Common/UserPictureList';
 
 
 import styles from './BookmarkButtons.module.scss';
 import styles from './BookmarkButtons.module.scss';
+import popoverStyles from './user-list-popover.module.scss';
 
 
 interface Props {
 interface Props {
   pageId: string,
   pageId: string,
   isBookmarked?: boolean,
   isBookmarked?: boolean,
   bookmarkCount: number,
   bookmarkCount: number,
-  hideTotalNumber?: boolean,
 }
 }
 
 
 export const BookmarkButtons: FC<Props> = (props: Props) => {
 export const BookmarkButtons: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    pageId, isBookmarked, bookmarkCount, hideTotalNumber,
+    pageId, isBookmarked, bookmarkCount,
   } = props;
   } = props;
 
 
   const [isBookmarkFolderMenuOpen, setBookmarkFolderMenuOpen] = useState(false);
   const [isBookmarkFolderMenuOpen, setBookmarkFolderMenuOpen] = useState(false);
@@ -73,7 +73,7 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
         <DropdownToggle
         <DropdownToggle
           id="bookmark-dropdown-btn"
           id="bookmark-dropdown-btn"
           color="transparent"
           color="transparent"
-          className={`shadow-none btn btn-bookmark border-0
+          className={`shadow-none btn btn-bookmark border-0 rounded-end-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
         >
           <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
           <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
@@ -83,35 +83,31 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 
-      { !hideTotalNumber && (
-        <>
-          <button
-            type="button"
-            id="po-total-bookmarks"
-            className={`shadow-none btn btn-bookmark border-0
-              total-bookmarks ${isBookmarked ? 'active' : ''}`}
-          >
-            {bookmarkCount}
-          </button>
-          <Popover placement="bottom" isOpen={isBookmarkUsersPopoverOpen} target="po-total-bookmarks" toggle={toggleBookmarkUsersPopover} trigger="legacy">
-            <PopoverBody className="user-list-popover">
-              { isLoadingBookmarkedUsers && <i className="fa fa-spinner fa-pulse"></i> }
-              { !isLoadingBookmarkedUsers && bookmarkedUsers != null && (
-                <>
-                  { bookmarkedUsers.length > 0
-                    ? (
-                      <div className="px-2 text-end user-list-content text-truncate text-muted">
-                        <UserPictureList users={bookmarkedUsers} />
-                      </div>
-                    )
-                    : t('No users have bookmarked yet')
-                  }
-                </>
-              ) }
-            </PopoverBody>
-          </Popover>
-        </>
-      ) }
+      <button
+        type="button"
+        id="po-total-bookmarks"
+        className={`shadow-none btn btn-bookmark border-0
+          total-counts ${isBookmarked ? 'active' : ''}`}
+      >
+        {bookmarkCount}
+      </button>
+      <Popover placement="bottom" isOpen={isBookmarkUsersPopoverOpen} target="po-total-bookmarks" toggle={toggleBookmarkUsersPopover} trigger="legacy">
+        <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
+          { isLoadingBookmarkedUsers && <i className="fa fa-spinner fa-pulse"></i> }
+          { !isLoadingBookmarkedUsers && bookmarkedUsers != null && (
+            <>
+              { bookmarkedUsers.length > 0
+                ? (
+                  <div className="px-2 text-end user-list-content text-truncate text-muted">
+                    <UserPictureList users={bookmarkedUsers} />
+                  </div>
+                )
+                : t('No users have bookmarked yet')
+              }
+            </>
+          ) }
+        </PopoverBody>
+      </Popover>
     </div>
     </div>
   );
   );
 };
 };

+ 0 - 0
apps/app/src/components/LikeButtons.module.scss → apps/app/src/components/PageControls/LikeButtons.module.scss


+ 17 - 22
apps/app/src/components/LikeButtons.tsx → apps/app/src/components/PageControls/LikeButtons.tsx

@@ -5,13 +5,12 @@ import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
 
 
 
-import UserPictureList from './User/UserPictureList';
+import UserPictureList from '../Common/UserPictureList';
 
 
 import styles from './LikeButtons.module.scss';
 import styles from './LikeButtons.module.scss';
 
 
 type LikeButtonsProps = {
 type LikeButtonsProps = {
 
 
-  hideTotalNumber?: boolean,
   sumOfLikers: number,
   sumOfLikers: number,
   likers: IUser[],
   likers: IUser[],
 
 
@@ -30,7 +29,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
   };
   };
 
 
   const {
   const {
-    hideTotalNumber, isGuestUser, isLiked, sumOfLikers, onLikeClicked,
+    isGuestUser, isLiked, sumOfLikers, onLikeClicked,
   } = props;
   } = props;
 
 
   const getTooltipMessage = useCallback(() => {
   const getTooltipMessage = useCallback(() => {
@@ -57,25 +56,21 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 
-      { !hideTotalNumber && (
-        <>
-          <button
-            type="button"
-            id="po-total-likes"
-            className={`shadow-none btn btn-like border-0
-              total-likes ${isLiked ? 'active' : ''}`}
-          >
-            {sumOfLikers}
-          </button>
-          <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-likes" toggle={togglePopover} trigger="legacy">
-            <PopoverBody className="user-list-popover">
-              <div className="px-2 text-end user-list-content text-truncate text-muted">
-                {props.likers?.length ? <UserPictureList users={props.likers} /> : t('No users have liked this yet.')}
-              </div>
-            </PopoverBody>
-          </Popover>
-        </>
-      ) }
+      <button
+        type="button"
+        id="po-total-likes"
+        className={`shadow-none btn btn-like border-0
+          total-counts ${isLiked ? 'active' : ''}`}
+      >
+        {sumOfLikers}
+      </button>
+      <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-likes" toggle={togglePopover} trigger="legacy">
+        <PopoverBody className="user-list-popover">
+          <div className="px-2 text-end user-list-content text-truncate text-muted">
+            {props.likers?.length ? <UserPictureList users={props.likers} /> : t('No users have liked this yet.')}
+          </div>
+        </PopoverBody>
+      </Popover>
     </div>
     </div>
   );
   );
 
 

+ 30 - 0
apps/app/src/components/PageControls/PageControls.module.scss

@@ -0,0 +1,30 @@
+%page-controls-buttons-height {
+  height: 40px;
+}
+
+.grw-page-controls :global {
+
+  .btn-subscribe {
+    --bs-btn-font-size: 18px;
+    @extend %page-controls-buttons-height;
+  }
+
+  .btn-like,
+  .btn-bookmark,
+  .btn-seen-user {
+    --bs-btn-font-size: 18px;
+
+    @extend %page-controls-buttons-height;
+    padding-right: 6px;
+    padding-left: 8px;
+  }
+
+  .total-counts {
+    font-size: 13px;
+  }
+
+  .btn-page-item-control {
+    @extend %page-controls-buttons-height;
+  }
+
+}

+ 19 - 18
apps/app/src/components/Navbar/SubNavButtons.tsx → apps/app/src/components/PageControls/PageControls.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
 
 
 import type {
 import type {
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
@@ -14,18 +14,22 @@ import {
 } from '~/client/services/page-operation';
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
-import { IPageForPageDuplicateModal } from '~/stores/modal';
+import type { IPageForPageDuplicateModal } from '~/stores/modal';
 
 
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import { useSWRxUsersList } from '../../stores/user';
-import { BookmarkButtons } from '../BookmarkButtons';
 import {
 import {
   AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
   AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
   PageItemControl,
   PageItemControl,
 } from '../Common/Dropdown/PageItemControl';
 } from '../Common/Dropdown/PageItemControl';
-import LikeButtons from '../LikeButtons';
-import SubscribeButton from '../SubscribeButton';
-import SeenUserInfo from '../User/SeenUserInfo';
+
+import { BookmarkButtons } from './BookmarkButtons';
+import LikeButtons from './LikeButtons';
+import SeenUserInfo from './SeenUserInfo';
+import SubscribeButton from './SubscribeButton';
+
+
+import styles from './PageControls.module.scss';
 
 
 
 
 type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
 type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
@@ -63,7 +67,6 @@ const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
 
 
 
 
 type CommonProps = {
 type CommonProps = {
-  isCompactMode?: boolean,
   disableSeenUserInfoPopover?: boolean,
   disableSeenUserInfoPopover?: boolean,
   showPageControlDropdown?: boolean,
   showPageControlDropdown?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
@@ -74,7 +77,7 @@ type CommonProps = {
   onClickSwitchContentWidth?: (pageId: string, value: boolean) => void,
   onClickSwitchContentWidth?: (pageId: string, value: boolean) => void,
 }
 }
 
 
-type SubNavButtonsSubstanceProps = CommonProps & {
+type PageControlsSubstanceProps = CommonProps & {
   pageId: string,
   pageId: string,
   shareLinkId?: string | null,
   shareLinkId?: string | null,
   revisionId: string | null,
   revisionId: string | null,
@@ -83,11 +86,11 @@ type SubNavButtonsSubstanceProps = CommonProps & {
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
 }
 }
 
 
-const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element => {
+const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element => {
   const {
   const {
     pageInfo,
     pageInfo,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
-    isCompactMode, disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
+    disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
     onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
     onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
   } = props;
   } = props;
 
 
@@ -212,7 +215,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   ];
   ];
 
 
   return (
   return (
-    <div className="d-flex" style={{ gap: '2px' }}>
+    <div className={`grw-page-controls ${styles['grw-page-controls']} d-flex`} style={{ gap: '2px' }}>
       {revisionId != null && (
       {revisionId != null && (
         <SubscribeButton
         <SubscribeButton
           status={pageInfo.subscriptionStatus}
           status={pageInfo.subscriptionStatus}
@@ -221,7 +224,6 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
       )}
       )}
       {revisionId != null && (
       {revisionId != null && (
         <LikeButtons
         <LikeButtons
-          hideTotalNumber={isCompactMode}
           onLikeClicked={likeClickhandler}
           onLikeClicked={likeClickhandler}
           sumOfLikers={sumOfLikers}
           sumOfLikers={sumOfLikers}
           isLiked={isLiked}
           isLiked={isLiked}
@@ -233,10 +235,9 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageId={pageId}
           pageId={pageId}
           isBookmarked={pageInfo.isBookmarked}
           isBookmarked={pageInfo.isBookmarked}
           bookmarkCount={pageInfo.bookmarkCount}
           bookmarkCount={pageInfo.bookmarkCount}
-          hideTotalNumber={isCompactMode}
         />
         />
       )}
       )}
-      {revisionId != null && !isCompactMode && (
+      {revisionId != null && (
         <SeenUserInfo
         <SeenUserInfo
           seenUsers={seenUsers}
           seenUsers={seenUsers}
           sumOfSeenUsers={sumOfSeenUsers}
           sumOfSeenUsers={sumOfSeenUsers}
@@ -262,7 +263,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   );
   );
 };
 };
 
 
-export type SubNavButtonsProps = CommonProps & {
+type PageControlsProps = CommonProps & {
   pageId: string,
   pageId: string,
   shareLinkId?: string | null,
   shareLinkId?: string | null,
   revisionId?: string | null,
   revisionId?: string | null,
@@ -270,7 +271,7 @@ export type SubNavButtonsProps = CommonProps & {
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
 };
 };
 
 
-export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
+export const PageControls = memo((props: PageControlsProps): JSX.Element => {
   const {
   const {
     pageId, revisionId, path, shareLinkId, expandContentWidth,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
     onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
     onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
@@ -287,7 +288,7 @@ export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <SubNavButtonsSubstance
+    <PageControlsSubstance
       {...props}
       {...props}
       pageInfo={pageInfo}
       pageInfo={pageInfo}
       pageId={pageId}
       pageId={pageId}
@@ -300,4 +301,4 @@ export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
       expandContentWidth={expandContentWidth}
       expandContentWidth={expandContentWidth}
     />
     />
   );
   );
-};
+});

+ 0 - 4
apps/app/src/components/User/SeenUserInfo.module.scss → apps/app/src/components/PageControls/SeenUserInfo.module.scss

@@ -6,14 +6,10 @@
     $color-seen-user: #549c79;
     $color-seen-user: #549c79;
 
 
     @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
     @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
-    @include mixins-buttons.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
 
 
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {
     &:not(:disabled):not(.disabled).active {
       color: $color-seen-user;
       color: $color-seen-user;
-      svg {
-        fill: $color-seen-user;
-      }
     }
     }
     &:not(:disabled):not(.disabled):not(:hover) {
     &:not(:disabled):not(.disabled):not(:hover) {
       background-color: transparent;
       background-color: transparent;

+ 4 - 3
apps/app/src/components/User/SeenUserInfo.tsx → apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -5,10 +5,11 @@ 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';
 
 
-import UserPictureList from './UserPictureList';
+import UserPictureList from '../Common/UserPictureList';
 
 
 
 
 import styles from './SeenUserInfo.module.scss';
 import styles from './SeenUserInfo.module.scss';
+import popoverStyles from './user-list-popover.module.scss';
 
 
 
 
 interface Props {
 interface Props {
@@ -31,10 +32,10 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
         <span className="me-1 footstamp-icon">
         <span className="me-1 footstamp-icon">
           <FootstampIcon />
           <FootstampIcon />
         </span>
         </span>
-        <span className="seen-user-count">{sumOfSeenUsers || seenUsers.length}</span>
+        <span className="total-counts">{sumOfSeenUsers || seenUsers.length}</span>
       </button>
       </button>
       <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-seen-user" toggle={togglePopover} trigger="legacy" disabled={disabled}>
       <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-seen-user" toggle={togglePopover} trigger="legacy" disabled={disabled}>
-        <PopoverBody className="user-list-popover">
+        <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
           <div className="px-2 text-end user-list-content text-truncate text-muted">
           <div className="px-2 text-end user-list-content text-truncate text-muted">
             <UserPictureList users={seenUsers} />
             <UserPictureList users={seenUsers} />
           </div>
           </div>

+ 0 - 0
apps/app/src/components/SubscribeButton.module.scss → apps/app/src/components/PageControls/SubscribeButton.module.scss


+ 0 - 0
apps/app/src/components/SubscribeButton.tsx → apps/app/src/components/PageControls/SubscribeButton.tsx


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

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

+ 12 - 0
apps/app/src/components/PageControls/user-list-popover.module.scss

@@ -0,0 +1,12 @@
+.user-list-popover :global {
+  --bs-popover-max-width: 200px;
+  --bs-popover-body-padding-x: .5rem;
+  --bs-popover-body-padding-y: .5rem;
+
+  .user-list-content {
+    direction: rtl;
+  }
+  .cls-1 {
+    isolation: isolate;
+  }
+}

+ 1 - 0
apps/app/src/components/PageEditor/Editor.tsx

@@ -24,6 +24,7 @@ import { Cheatsheet } from './Cheatsheet';
 import pasteHelper from './PasteHelper';
 import pasteHelper from './PasteHelper';
 import TextAreaEditor from './TextAreaEditor';
 import TextAreaEditor from './TextAreaEditor';
 
 
+
 import styles from './Editor.module.scss';
 import styles from './Editor.module.scss';
 
 
 export type EditorPropsType = {
 export type EditorPropsType = {

+ 1 - 1
apps/app/src/components/PageList/PageListItemL.tsx

@@ -33,7 +33,7 @@ import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 
 
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
-import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
+import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
 
 
 type Props = {
 type Props = {
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,

+ 0 - 70
apps/app/src/components/PagePathNav.tsx

@@ -1,70 +0,0 @@
-import React, { FC } from 'react';
-
-import { DevidedPagePath } from '@growi/core/dist/models';
-import { pagePathUtils } from '@growi/core/dist/utils';
-import dynamic from 'next/dynamic';
-
-import { useIsNotFound } from '~/stores/page';
-
-import LinkedPagePath from '../models/linked-page-path';
-
-import PagePathHierarchicalLink from './PagePathHierarchicalLink';
-
-const { isTrashPage } = pagePathUtils;
-
-type Props = {
-  pagePath: string,
-  pageId?: string | null,
-  isSingleLineMode?:boolean,
-  isCompactMode?:boolean,
-}
-
-const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
-
-const PagePathNav: FC<Props> = (props: Props) => {
-  const {
-    pageId, pagePath, isSingleLineMode, isCompactMode,
-  } = props;
-  const dPagePath = new DevidedPagePath(pagePath, false, true);
-
-  const { data: isNotFound } = useIsNotFound();
-
-  const isInTrash = isTrashPage(pagePath);
-
-  let formerLink;
-  let latterLink;
-
-  // one line
-  if (dPagePath.isRoot || dPagePath.isFormerRoot || isSingleLineMode) {
-    const linkedPagePath = new LinkedPagePath(pagePath);
-    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
-  }
-  // two line
-  else {
-    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    formerLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />;
-    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />;
-  }
-
-  const copyDropdownId = `copydropdown${isCompactMode ? '-subnav-compact' : ''}-${pageId}`;
-  const copyDropdownToggleClassName = 'd-block btn-outline-secondary btn-copy border-0 text-muted p-2';
-
-  return (
-    <div className="grw-page-path-nav">
-      {formerLink}
-      <div className="d-flex align-items-center">
-        <h1 className="m-0">{latterLink}</h1>
-        { pageId != null && !isNotFound && (
-          <div className="mx-2">
-            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName={copyDropdownToggleClassName}>
-              <i className="ti ti-clipboard"></i>
-            </CopyDropdown>
-          </div>
-        ) }
-      </div>
-    </div>
-  );
-};
-
-export default PagePathNav;

+ 65 - 0
apps/app/src/components/PageSelectModal/PageSelectModal.tsx

@@ -0,0 +1,65 @@
+import React from 'react';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter, Button,
+} from 'reactstrap';
+
+import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
+import { usePageSelectModal } from '~/stores/modal';
+import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
+
+import { ItemsTree } from '../ItemsTree';
+
+import { TreeItemForModal } from './TreeItemForModal';
+
+
+export const PageSelectModal = () => {
+  const {
+    data: PageSelectModalData,
+    close: closeModal,
+  } = usePageSelectModal();
+
+  const isOpened = PageSelectModalData?.isOpened ?? false;
+
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+  const { data: currentPath } = useCurrentPagePath();
+  const { data: targetId } = useCurrentPageId();
+  const { data: targetAndAncestorsData } = useTargetAndAncestors();
+
+  const targetPathOrId = targetId || currentPath;
+
+  if (isGuestUser == null) {
+    return null;
+  }
+
+  const path = currentPath || '/';
+
+  return (
+    <Modal
+      isOpen={isOpened}
+      toggle={() => closeModal()}
+      centered
+    >
+      <ModalHeader toggle={() => closeModal()}>modal</ModalHeader>
+      <ModalBody>
+        <ItemsTree
+          CustomTreeItem={TreeItemForModal}
+          isEnableActions={!isGuestUser}
+          isReadOnlyUser={!!isReadOnlyUser}
+          targetPath={path}
+          targetPathOrId={targetPathOrId}
+          targetAndAncestorsData={targetAndAncestorsData}
+        />
+      </ModalBody>
+      <ModalFooter>
+        <Button color="primary">
+          Do Something
+        </Button>{' '}
+        <Button color="secondary">
+          Cancel
+        </Button>
+      </ModalFooter>
+    </Modal>
+  );
+};

+ 30 - 0
apps/app/src/components/PageSelectModal/TreeItemForModal.tsx

@@ -0,0 +1,30 @@
+import React, { FC } from 'react';
+
+import {
+  SimpleItem, SimpleItemProps, SimpleItemTool, useNewPageInput,
+} from '../TreeItem';
+
+type Optional = 'itemRef' | 'itemClass' | 'mainClassName';
+type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
+
+export const TreeItemForModal: FC<PageTreeItemProps> = (props) => {
+
+  const { NewPageInputWrapper, NewPageCreateButtonWrapper } = useNewPageInput();
+
+  return (
+    <SimpleItem
+      key={props.key}
+      targetPathOrId={props.targetPathOrId}
+      itemNode={props.itemNode}
+      isOpen
+      isEnableActions={props.isEnableActions}
+      isReadOnlyUser={props.isReadOnlyUser}
+      onRenamed={props.onRenamed}
+      onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
+      onClickDeleteMenuItem={props.onClickDeleteMenuItem}
+      customNextComponents={[NewPageInputWrapper]}
+      itemClass={TreeItemForModal}
+      customEndComponents={[SimpleItemTool, NewPageCreateButtonWrapper]}
+    />
+  );
+};

+ 1 - 1
apps/app/src/components/SearchPage/SearchPageBase.tsx

@@ -213,7 +213,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
 
       </div>
       </div>
 
 
-      <div className="flex-expand-vert d-none d-lg-flex search-result-content">
+      <div className="flex-expand-vert d-none d-lg-flex">
         {pages != null && pages.length !== 0 && selectedPageWithMeta != null && (
         {pages != null && pages.length !== 0 && selectedPageWithMeta != null && (
           <SearchResultContent
           <SearchResultContent
             pageWithMeta={selectedPageWithMeta}
             pageWithMeta={selectedPageWithMeta}

+ 0 - 6
apps/app/src/components/SearchPage/SearchResultContent.module.scss

@@ -1,8 +1,2 @@
-/*
-* shadow
-*/
 .search-result-content :global {
 .search-result-content :global {
-  .grw-subnav {
-    box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
-  }
 }
 }

+ 12 - 18
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -24,8 +24,7 @@ import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 
 
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
-import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
+import { PagePathNav } from '../Common/PagePathNav';
 import { type RevisionLoaderProps } from '../Page/RevisionLoader';
 import { type RevisionLoaderProps } from '../Page/RevisionLoader';
 import { type PageCommentProps } from '../PageComment';
 import { type PageCommentProps } from '../PageComment';
 import type { PageContentFooterProps } from '../PageContentFooter';
 import type { PageContentFooterProps } from '../PageContentFooter';
@@ -33,8 +32,7 @@ import type { PageContentFooterProps } from '../PageContentFooter';
 import styles from './SearchResultContent.module.scss';
 import styles from './SearchResultContent.module.scss';
 
 
 
 
-const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
-const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
+const SubNavButtons = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
 const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
 const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
 const PageComment = dynamic<PageCommentProps>(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
 const PageComment = dynamic<PageCommentProps>(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
 const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
 const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
@@ -187,7 +185,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     const revisionId = getIdForRef(page.revision);
     const revisionId = getIdForRef(page.revision);
 
 
     return (
     return (
-      <div className="d-flex flex-column align-items-end justify-content-center py-md-2">
+      <div className="d-flex flex-column align-items-end justify-content-center px-2 py-1">
         <SubNavButtons
         <SubNavButtons
           pageId={page._id}
           pageId={page._id}
           revisionId={revisionId}
           revisionId={revisionId}
@@ -196,7 +194,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           showPageControlDropdown={showPageControlDropdown}
           showPageControlDropdown={showPageControlDropdown}
           forceHideMenuItems={forceHideMenuItems}
           forceHideMenuItems={forceHideMenuItems}
           additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
           additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
-          isCompactMode
           onClickDuplicateMenuItem={duplicateItemClickedHandler}
           onClickDuplicateMenuItem={duplicateItemClickedHandler}
           onClickRenameMenuItem={renameItemClickedHandler}
           onClickRenameMenuItem={renameItemClickedHandler}
           onClickDeleteMenuItem={deleteItemClickedHandler}
           onClickDeleteMenuItem={deleteItemClickedHandler}
@@ -215,21 +212,18 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       data-testid="search-result-content"
       data-testid="search-result-content"
       className={`dynamic-layout-root ${growiLayoutFluidClass} search-result-content ${styles['search-result-content']}`}
       className={`dynamic-layout-root ${growiLayoutFluidClass} search-result-content ${styles['search-result-content']}`}
     >
     >
-      <div className="grw-page-path-text-muted-container">
-        { isRenderable && (
-          <GrowiSubNavigation
-            pagePath={page.path}
-            pageId={page._id}
-            rightComponent={RightComponent}
-            isCompactMode
-            additionalClasses={['px-4']}
-          />
-        ) }
-      </div>
+      <RightComponent />
+
+      { isRenderable && (
+        <div className="container-lg grw-container-convertible pt-2 pb-2">
+          <PagePathNav pageId={page._id} pagePath={page.path} formerLinkClassName="small" latterLinkClassName="fs-3" />
+        </div>
+      ) }
+
       <div
       <div
         id="search-result-content-body-container"
         id="search-result-content-body-container"
         ref={scrollElementRef}
         ref={scrollElementRef}
-        className="search-result-content-body-container main container-lg grw-container-convertible overflow-y-scroll"
+        className="search-result-content-body-container container-lg grw-container-convertible overflow-y-scroll"
       >
       >
         { isRenderable && (
         { isRenderable && (
           <RevisionLoader
           <RevisionLoader

+ 6 - 0
apps/app/src/components/ShareLinkPageView.tsx

@@ -10,6 +10,7 @@ import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { PagePathNavSticky } from './Common/PagePathNav';
 import { PageViewLayout } from './Layout/PageViewLayout';
 import { PageViewLayout } from './Layout/PageViewLayout';
 import RevisionRenderer from './Page/RevisionRenderer';
 import RevisionRenderer from './Page/RevisionRenderer';
 import ShareLinkAlert from './Page/ShareLinkAlert';
 import ShareLinkAlert from './Page/ShareLinkAlert';
@@ -51,6 +52,10 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     }
     }
   }, [disableLinkSharing, props.disableLinkSharing]);
   }, [disableLinkSharing, props.disableLinkSharing]);
 
 
+  const headerContents = (
+    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} />
+  );
+
   const sideContents = !isNotFound
   const sideContents = !isNotFound
     ? (
     ? (
       <PageSideContents page={page} />
       <PageSideContents page={page} />
@@ -86,6 +91,7 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <PageViewLayout
     <PageViewLayout
+      headerContents={headerContents}
       sideContents={sideContents}
       sideContents={sideContents}
     >
     >
       { specialContents }
       { specialContents }

+ 0 - 569
apps/app/src/components/Sidebar/PageTree/Item.tsx

@@ -1,569 +0,0 @@
-import React, {
-  useCallback, useState, FC, useEffect, ReactNode,
-} from 'react';
-
-import nodePath from 'path';
-
-import type {
-  Nullable,
-  IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
-} from '@growi/core';
-import {
-  pathUtils, pagePathUtils,
-} from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
-import { useDrag, useDrop } from 'react-dnd';
-import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
-
-import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
-import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
-import { ValidationTarget } from '~/client/util/input-validator';
-import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
-import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
-import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
-import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
-import { IPageForPageDuplicateModal } from '~/stores/modal';
-import { useSWRMUTxPageInfo } from '~/stores/page';
-import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
-import { usePageTreeDescCountMap } from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-import { shouldRecoverPagePaths } from '~/utils/page-operation';
-
-import ClosableTextInput from '../../Common/ClosableTextInput';
-import CountBadge from '../../Common/CountBadge';
-import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
-
-import { ItemNode } from './ItemNode';
-
-
-import styles from './Item.module.scss';
-
-
-const logger = loggerFactory('growi:cli:Item');
-
-
-interface ItemProps {
-  isEnableActions: boolean
-  isReadOnlyUser: boolean
-  itemNode: ItemNode
-  targetPathOrId?: Nullable<string>
-  isOpen?: boolean
-  onRenamed?(fromPath: string | undefined, toPath: string): void
-  onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
-  onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
-}
-
-// Utility to mark target
-const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
-  if (targetPathOrId == null) {
-    return;
-  }
-
-  children.forEach((node) => {
-    if (node.page._id === targetPathOrId || node.page.path === targetPathOrId) {
-      node.page.isTarget = true;
-    }
-    else {
-      node.page.isTarget = false;
-    }
-    return node;
-  });
-};
-
-/**
- * Return new page path after the droppedPagePath is moved under the newParentPagePath
- * @param droppedPagePath
- * @param newParentPagePath
- * @returns
- */
-const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
-  const pageTitle = nodePath.basename(droppedPagePath);
-  return nodePath.join(newParentPagePath, pageTitle);
-};
-
-/**
- * Return whether the fromPage could be moved under the newParentPage
- * @param fromPage
- * @param newParentPage
- * @param printLog
- * @returns
- */
-const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
-  if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
-    if (printLog) {
-      logger.warn('Any of page, page.path or droppedPage.path is null');
-    }
-    return false;
-  }
-
-  const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
-  return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
-};
-
-// Component wrapper to make a child element not draggable
-// https://github.com/react-dnd/react-dnd/issues/335
-type NotDraggableProps = {
-  children: ReactNode,
-};
-const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
-  return <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
-};
-
-
-const Item: FC<ItemProps> = (props: ItemProps) => {
-  const { t } = useTranslation();
-  const {
-    itemNode, targetPathOrId, isOpen: _isOpen = false,
-    onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
-  } = props;
-
-  const { page, children } = itemNode;
-
-  const [currentChildren, setCurrentChildren] = useState(children);
-  const [isOpen, setIsOpen] = useState(_isOpen);
-  const [isNewPageInputShown, setNewPageInputShown] = useState(false);
-  const [shouldHide, setShouldHide] = useState(false);
-  const [isRenameInputShown, setRenameInputShown] = useState(false);
-  const [isCreating, setCreating] = useState(false);
-
-  const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
-  const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
-  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
-
-  // descendantCount
-  const { getDescCount } = usePageTreeDescCountMap();
-  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
-
-
-  // hasDescendants flag
-  const isChildrenLoaded = currentChildren?.length > 0;
-  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
-
-  // to re-show hidden item when useDrag end() callback
-  const displayDroppedItemByPageId = useCallback((pageId) => {
-    const target = document.getElementById(`pagetree-item-${pageId}`);
-    if (target == null) {
-      return;
-    }
-
-    // wait 500ms to avoid removing before d-none is set by useDrag end() callback
-    setTimeout(() => {
-      target.classList.remove('d-none');
-    }, 500);
-  }, []);
-
-  const [, drag] = useDrag({
-    type: 'PAGE_TREE',
-    item: { page },
-    canDrag: () => {
-      if (page.path == null) {
-        return false;
-      }
-      return !pagePathUtils.isUsersProtectedPages(page.path);
-    },
-    end: (item, monitor) => {
-      // in order to set d-none to dropped Item
-      const dropResult = monitor.getDropResult();
-      if (dropResult != null) {
-        setShouldHide(true);
-      }
-    },
-    collect: monitor => ({
-      isDragging: monitor.isDragging(),
-      canDrag: monitor.canDrag(),
-    }),
-  });
-
-  const pageItemDropHandler = async(item: ItemNode) => {
-    const { page: droppedPage } = item;
-
-    if (!isDroppable(droppedPage, page, true)) {
-      return;
-    }
-
-    if (droppedPage.path == null || page.path == null) {
-      return;
-    }
-
-    const newPagePath = getNewPathAfterMoved(droppedPage.path, page.path);
-
-    try {
-      await apiv3Put('/pages/rename', {
-        pageId: droppedPage._id,
-        revisionId: droppedPage.revision,
-        newPagePath,
-        isRenameRedirect: false,
-        updateMetadata: true,
-      });
-
-      await mutatePageTree();
-      await mutateChildren();
-
-      if (onRenamed != null) {
-        onRenamed(page.path, newPagePath);
-      }
-
-      // force open
-      setIsOpen(true);
-    }
-    catch (err) {
-      // display the dropped item
-      displayDroppedItemByPageId(droppedPage._id);
-
-      if (err.code === 'operation__blocked') {
-        toastWarning(t('pagetree.you_cannot_move_this_page_now'));
-      }
-      else {
-        toastError(t('pagetree.something_went_wrong_with_moving_page'));
-      }
-    }
-  };
-
-  const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
-    () => ({
-      accept: 'PAGE_TREE',
-      drop: pageItemDropHandler,
-      hover: (item, monitor) => {
-        // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
-        if (monitor.isOver()) {
-          setTimeout(() => {
-            if (monitor.isOver()) {
-              setIsOpen(true);
-            }
-          }, 600);
-        }
-      },
-      canDrop: (item) => {
-        const { page: droppedPage } = item;
-        return isDroppable(droppedPage, page);
-      },
-      collect: monitor => ({
-        isOver: monitor.isOver(),
-      }),
-    }),
-    [page],
-  );
-
-
-  const hasChildren = useCallback((): boolean => {
-    return currentChildren != null && currentChildren.length > 0;
-  }, [currentChildren]);
-
-  const onClickLoadChildren = useCallback(async() => {
-    setIsOpen(!isOpen);
-  }, [isOpen]);
-
-  const onClickPlusButton = useCallback(() => {
-    setNewPageInputShown(true);
-
-    if (hasDescendants) {
-      setIsOpen(true);
-    }
-  }, [hasDescendants]);
-
-  const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
-    const bookmarkOperation = _newValue ? bookmark : unbookmark;
-    await bookmarkOperation(_pageId);
-    mutateCurrentUserBookmarks();
-    mutatePageInfo();
-  };
-
-  const duplicateMenuItemClickHandler = useCallback((): void => {
-    if (onClickDuplicateMenuItem == null) {
-      return;
-    }
-
-    const { _id: pageId, path } = page;
-
-    if (pageId == null || path == null) {
-      throw Error('Any of _id and path must not be null.');
-    }
-
-    const pageToDuplicate = { pageId, path };
-
-    onClickDuplicateMenuItem(pageToDuplicate);
-  }, [onClickDuplicateMenuItem, page]);
-
-  const renameMenuItemClickHandler = useCallback(() => {
-    setRenameInputShown(true);
-  }, []);
-
-  const onPressEnterForRenameHandler = async(inputText: string) => {
-    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
-    const newPagePath = nodePath.resolve(parentPath, inputText);
-
-    if (newPagePath === page.path) {
-      setRenameInputShown(false);
-      return;
-    }
-
-    try {
-      setRenameInputShown(false);
-      await apiv3Put('/pages/rename', {
-        pageId: page._id,
-        revisionId: page.revision,
-        newPagePath,
-      });
-
-      if (onRenamed != null) {
-        onRenamed(page.path, newPagePath);
-      }
-
-      toastSuccess(t('renamed_pages', { path: page.path }));
-    }
-    catch (err) {
-      setRenameInputShown(true);
-      toastError(err);
-    }
-  };
-
-  const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
-    if (onClickDeleteMenuItem == null) {
-      return;
-    }
-
-    if (page._id == null || page.path == null) {
-      throw Error('_id and path must not be null.');
-    }
-
-    const pageToDelete: IPageToDeleteWithMeta = {
-      data: {
-        _id: page._id,
-        revision: page.revision as string,
-        path: page.path,
-      },
-      meta: pageInfo,
-    };
-
-    onClickDeleteMenuItem(pageToDelete);
-  }, [onClickDeleteMenuItem, page]);
-
-  const onPressEnterForCreateHandler = async(inputText: string) => {
-    setNewPageInputShown(false);
-    const parentPath = pathUtils.addTrailingSlash(page.path as string);
-    const newPagePath = nodePath.resolve(parentPath, inputText);
-    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
-
-    if (!isCreatable) {
-      toastWarning(t('you_can_not_create_page_with_this_name'));
-      return;
-    }
-
-    try {
-      setCreating(true);
-
-      await apiv3Post('/pages/', {
-        path: newPagePath,
-        body: undefined,
-        grant: page.grant,
-        grantUserGroupId: page.grantedGroup,
-      });
-
-      mutateChildren();
-
-      if (!hasDescendants) {
-        setIsOpen(true);
-      }
-
-      toastSuccess(t('successfully_saved_the_page'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    finally {
-      setCreating(false);
-    }
-  };
-
-
-  /**
-   * Users do not need to know if all pages have been renamed.
-   * Make resuming rename operation appears to be working fine to allow users for a seamless operation.
-   */
-  const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
-    try {
-      await resumeRenameOperation(pageId);
-      toastSuccess(t('page_operation.paths_recovered'));
-    }
-    catch {
-      toastError(t('page_operation.path_recovery_failed'));
-    }
-  };
-
-  // didMount
-  useEffect(() => {
-    if (hasChildren()) setIsOpen(true);
-  }, [hasChildren]);
-
-  /*
-   * Make sure itemNode.children and currentChildren are synced
-   */
-  useEffect(() => {
-    if (children.length > currentChildren.length) {
-      markTarget(children, targetPathOrId);
-      setCurrentChildren(children);
-    }
-  }, [children, currentChildren.length, targetPathOrId]);
-
-  /*
-   * When swr fetch succeeded
-   */
-  useEffect(() => {
-    if (isOpen && data != null) {
-      const newChildren = ItemNode.generateNodesFromPages(data.children);
-      markTarget(newChildren, targetPathOrId);
-      setCurrentChildren(newChildren);
-    }
-  }, [data, isOpen, targetPathOrId]);
-
-  // Rename process
-  // Icon that draw attention from users for some actions
-  const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
-
-  return (
-    <div
-      id={`pagetree-item-${page._id}`}
-      data-testid="grw-pagetree-item-container"
-      className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}
-        pagetree-item ${styles['pagetree-item']}
-        ${shouldHide ? 'd-none' : ''}`
-      }
-    >
-      <li
-        ref={(c) => { drag(c); drop(c) }}
-        className={`list-group-item list-group-item-action border-0 py-0 pe-3 d-flex align-items-center
-        ${page.isTarget ? 'grw-pagetree-current-page-item' : ''}`}
-        id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
-      >
-        <div className="grw-triangle-container d-flex justify-content-center">
-          {hasDescendants && (
-            <button
-              type="button"
-              className={`grw-pagetree-triangle-btn btn ${isOpen ? 'grw-pagetree-open' : ''}`}
-              onClick={onClickLoadChildren}
-            >
-              <div className="d-flex justify-content-center">
-                <span className="material-icons-round">arrow_right</span>
-              </div>
-            </button>
-          )}
-        </div>
-        {isRenameInputShown
-          ? (
-            <div className="flex-fill">
-              <NotDraggableForClosableTextInput>
-                <ClosableTextInput
-                  value={nodePath.basename(page.path ?? '')}
-                  placeholder={t('Input page name')}
-                  onClickOutside={() => { setRenameInputShown(false) }}
-                  onPressEnter={onPressEnterForRenameHandler}
-                  validationTarget={ValidationTarget.PAGE}
-                />
-              </NotDraggableForClosableTextInput>
-            </div>
-          )
-          : (
-            <>
-              {shouldShowAttentionIcon && (
-                <>
-                  <i id="path-recovery" className="fa fa-warning me-2 text-warning"></i>
-                  <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
-                    {t('tooltip.operation.attention.rename')}
-                  </UncontrolledTooltip>
-                </>
-              )}
-              {page != null && page.path != null && page._id != null && (
-                <Link
-                  href={pathUtils.returnPathForURL(page.path, page._id)}
-                  className="grw-pagetree-title-anchor flex-grow-1"
-                  prefetch={false}
-                >
-                  <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
-                </Link>
-              )}
-            </>
-          )}
-        {descendantCount > 0 && !isRenameInputShown && (
-          <div className="grw-pagetree-count-wrapper">
-            <CountBadge count={descendantCount} />
-          </div>
-        )}
-        <NotAvailableForGuest>
-          <div className="grw-pagetree-control d-flex">
-            <PageItemControl
-              pageId={page._id}
-              isEnableActions={isEnableActions}
-              isReadOnlyUser={isReadOnlyUser}
-              onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-              onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-              onClickRenameMenuItem={renameMenuItemClickHandler}
-              onClickDeleteMenuItem={deleteMenuItemClickHandler}
-              onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-              isInstantRename
-              // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
-              operationProcessData={page.processData}
-            >
-              {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
-              <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
-                <i id="option-button-in-page-tree" className="icon-options fa fa-rotate-90 p-1"></i>
-              </DropdownToggle>
-            </PageItemControl>
-          </div>
-        </NotAvailableForGuest>
-
-        {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
-          <NotAvailableForGuest>
-            <NotAvailableForReadOnlyUser>
-              <button
-                id="page-create-button-in-page-tree"
-                type="button"
-                className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
-                onClick={onClickPlusButton}
-              >
-                <i className="icon-plus d-block p-0" />
-              </button>
-            </NotAvailableForReadOnlyUser>
-          </NotAvailableForGuest>
-        )}
-      </li>
-
-      {isEnableActions && isNewPageInputShown && (
-        <div className="flex-fill">
-          <NotDraggableForClosableTextInput>
-            <ClosableTextInput
-              placeholder={t('Input page name')}
-              onClickOutside={() => { setNewPageInputShown(false) }}
-              onPressEnter={onPressEnterForCreateHandler}
-              validationTarget={ValidationTarget.PAGE}
-            />
-          </NotDraggableForClosableTextInput>
-        </div>
-      )}
-      {
-        isOpen && hasChildren() && currentChildren.map((node, index) => (
-          <div key={node.page._id} className="grw-pagetree-item-children">
-            <Item
-              isEnableActions={isEnableActions}
-              isReadOnlyUser={isReadOnlyUser}
-              itemNode={node}
-              isOpen={false}
-              targetPathOrId={targetPathOrId}
-              onRenamed={onRenamed}
-              onClickDuplicateMenuItem={onClickDuplicateMenuItem}
-              onClickDeleteMenuItem={onClickDeleteMenuItem}
-            />
-            {isCreating && (currentChildren.length - 1 === index) && (
-              <div className="text-muted text-center">
-                <i className="fa fa-spinner fa-pulse me-1"></i>
-              </div>
-            )}
-          </div>
-        ))
-      }
-    </div>
-  );
-
-};
-
-export default Item;

+ 3 - 3
apps/app/src/components/Sidebar/PageTree/PageTree.tsx

@@ -3,12 +3,12 @@ import { Suspense } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import PageTreeContentSkeleton from './PageTreeContentSkeleton';
+import ItemsTreeContentSkeleton from '../../ItemsTree/ItemsTreeContentSkeleton';
 import { PageTreeHeader } from './PageTreeSubstance';
 import { PageTreeHeader } from './PageTreeSubstance';
 
 
 const PageTreeContent = dynamic(
 const PageTreeContent = dynamic(
   () => import('./PageTreeSubstance').then(mod => mod.PageTreeContent),
   () => import('./PageTreeSubstance').then(mod => mod.PageTreeContent),
-  { ssr: false, loading: PageTreeContentSkeleton },
+  { ssr: false, loading: ItemsTreeContentSkeleton },
 );
 );
 
 
 
 
@@ -24,7 +24,7 @@ export const PageTree = (): JSX.Element => {
         </Suspense>
         </Suspense>
       </div>
       </div>
 
 
-      <Suspense fallback={<PageTreeContentSkeleton />}>
+      <Suspense fallback={<ItemsTreeContentSkeleton />}>
         <PageTreeContent />
         <PageTreeContent />
       </Suspense>
       </Suspense>
     </div>
     </div>

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

@@ -6,9 +6,10 @@ import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stor
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { mutatePageTree, useSWRxRootPage, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { mutatePageTree, useSWRxRootPage, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 
+import { ItemsTree } from '../../ItemsTree/ItemsTree';
+import { PageTreeItem } from '../PageTreeItem';
 import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 
-import ItemsTree from './ItemsTree';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 
 
 
 
@@ -44,6 +45,8 @@ const PageTreeUnavailable = () => {
 };
 };
 
 
 export const PageTreeContent = memo(() => {
 export const PageTreeContent = memo(() => {
+  const { t } = useTranslation();
+
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
@@ -75,6 +78,7 @@ export const PageTreeContent = memo(() => {
         targetPath={path}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetPathOrId={targetPathOrId}
         targetAndAncestorsData={targetAndAncestorsData}
         targetAndAncestorsData={targetAndAncestorsData}
+        CustomTreeItem={PageTreeItem}
       />
       />
 
 
       {!isGuestUser && !isReadOnlyUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
       {!isGuestUser && !isReadOnlyUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (

+ 170 - 0
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -0,0 +1,170 @@
+import React, {
+  useCallback, useState, FC,
+} from 'react';
+
+import nodePath from 'path';
+
+
+import type { IPageInfoAll, IPageToDeleteWithMeta } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { DropdownToggle } from 'reactstrap';
+
+import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { ValidationTarget } from '~/client/util/input-validator';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import { IPageForItem } from '~/interfaces/page';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRMUTxPageInfo } from '~/stores/page';
+
+import ClosableTextInput from '../../Common/ClosableTextInput';
+import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
+import {
+  SimpleItemToolProps, NotDraggableForClosableTextInput, SimpleItemTool,
+} from '../../TreeItem';
+
+type EllipsisProps = SimpleItemToolProps & {page: IPageForItem};
+
+export const Ellipsis: FC<EllipsisProps> = (props) => {
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
+  const { t } = useTranslation();
+
+  const {
+    page, onRenamed, onClickDuplicateMenuItem,
+    onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
+  } = props;
+
+  const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
+
+  const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
+    const bookmarkOperation = _newValue ? bookmark : unbookmark;
+    await bookmarkOperation(_pageId);
+    mutateCurrentUserBookmarks();
+    mutatePageInfo();
+  };
+
+  const duplicateMenuItemClickHandler = useCallback((): void => {
+    if (onClickDuplicateMenuItem == null) {
+      return;
+    }
+
+    const { _id: pageId, path } = page;
+
+    if (pageId == null || path == null) {
+      throw Error('Any of _id and path must not be null.');
+    }
+
+    const pageToDuplicate = { pageId, path };
+
+    onClickDuplicateMenuItem(pageToDuplicate);
+  }, [onClickDuplicateMenuItem, page]);
+
+  const renameMenuItemClickHandler = useCallback(() => {
+    setRenameInputShown(true);
+  }, []);
+
+  const onPressEnterForRenameHandler = async(inputText: string) => {
+    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
+    const newPagePath = nodePath.resolve(parentPath, inputText);
+
+    if (newPagePath === page.path) {
+      setRenameInputShown(false);
+      return;
+    }
+
+    try {
+      setRenameInputShown(false);
+      await apiv3Put('/pages/rename', {
+        pageId: page._id,
+        revisionId: page.revision,
+        newPagePath,
+      });
+
+      if (onRenamed != null) {
+        onRenamed(page.path, newPagePath);
+      }
+
+      toastSuccess(t('renamed_pages', { path: page.path }));
+    }
+    catch (err) {
+      setRenameInputShown(true);
+      toastError(err);
+    }
+  };
+
+  const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
+    if (onClickDeleteMenuItem == null) {
+      return;
+    }
+
+    if (page._id == null || page.path == null) {
+      throw Error('_id and path must not be null.');
+    }
+
+    const pageToDelete: IPageToDeleteWithMeta = {
+      data: {
+        _id: page._id,
+        revision: page.revision as string,
+        path: page.path,
+      },
+      meta: pageInfo,
+    };
+
+    onClickDeleteMenuItem(pageToDelete);
+  }, [onClickDeleteMenuItem, page]);
+
+  const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
+    try {
+      await resumeRenameOperation(pageId);
+      toastSuccess(t('page_operation.paths_recovered'));
+    }
+    catch {
+      toastError(t('page_operation.path_recovery_failed'));
+    }
+  };
+
+  return (
+    <>
+      {isRenameInputShown ? (
+        <div className="flex-fill">
+          <NotDraggableForClosableTextInput>
+            <ClosableTextInput
+              value={nodePath.basename(page.path ?? '')}
+              placeholder={t('Input page name')}
+              onClickOutside={() => { setRenameInputShown(false) }}
+              onPressEnter={onPressEnterForRenameHandler}
+              validationTarget={ValidationTarget.PAGE}
+            />
+          </NotDraggableForClosableTextInput>
+        </div>
+      ) : (
+        <SimpleItemTool page={page} isEnableActions={false} isReadOnlyUser={false} />
+      )}
+      <NotAvailableForGuest>
+        <div className="grw-pagetree-control d-flex">
+          <PageItemControl
+            pageId={page._id}
+            isEnableActions={isEnableActions}
+            isReadOnlyUser={isReadOnlyUser}
+            onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+            onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+            onClickRenameMenuItem={renameMenuItemClickHandler}
+            onClickDeleteMenuItem={deleteMenuItemClickHandler}
+            onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+            isInstantRename
+            // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
+            operationProcessData={page.processData}
+          >
+            {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
+            <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
+              <i id="option-button-in-page-tree" className="icon-options fa fa-rotate-90 p-1"></i>
+            </DropdownToggle>
+          </PageItemControl>
+        </div>
+      </NotAvailableForGuest>
+    </>
+  );
+};

+ 177 - 0
apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -0,0 +1,177 @@
+import React, {
+  useCallback, useState, FC,
+} from 'react';
+
+import nodePath from 'path';
+
+import type { IPageHasId } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { useDrag, useDrop } from 'react-dnd';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { toastWarning, toastError } from '~/client/util/toastr';
+import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
+import loggerFactory from '~/utils/logger';
+
+import {
+  SimpleItem, type SimpleItemProps, useNewPageInput, ItemNode,
+} from '../../TreeItem';
+
+import { Ellipsis } from './Ellipsis';
+
+const logger = loggerFactory('growi:cli:Item');
+
+type PageTreeItemPropsOptional = 'itemRef' | 'itemClass' | 'mainClassName';
+type PageTreeItemProps = Omit<SimpleItemProps, PageTreeItemPropsOptional> & {key};
+
+export const PageTreeItem: FC<PageTreeItemProps> = (props) => {
+  const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
+    const pageTitle = nodePath.basename(droppedPagePath);
+    return nodePath.join(newParentPagePath, pageTitle);
+  };
+
+  const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
+    if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
+      if (printLog) {
+        logger.warn('Any of page, page.path or droppedPage.path is null');
+      }
+      return false;
+    }
+
+    const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
+    return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
+  };
+
+  const { t } = useTranslation();
+
+  const {
+    itemNode, isOpen: _isOpen = false, onRenamed,
+  } = props;
+
+  const { page } = itemNode;
+  const [isOpen, setIsOpen] = useState(_isOpen);
+  const [shouldHide, setShouldHide] = useState(false);
+
+  const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
+
+  const displayDroppedItemByPageId = useCallback((pageId) => {
+    const target = document.getElementById(`pagetree-item-${pageId}`);
+    if (target == null) {
+      return;
+    }
+    //   // wait 500ms to avoid removing before d-none is set by useDrag end() callback
+    setTimeout(() => {
+      target.classList.remove('d-none');
+    }, 500);
+  }, []);
+
+  const [, drag] = useDrag({
+    type: 'PAGE_TREE',
+    item: { page },
+    canDrag: () => {
+      if (page.path == null) {
+        return false;
+      }
+      return !pagePathUtils.isUsersProtectedPages(page.path);
+    },
+    end: (item, monitor) => {
+      // in order to set d-none to dropped Item
+      const dropResult = monitor.getDropResult();
+      if (dropResult != null) {
+        setShouldHide(true);
+      }
+    },
+    collect: monitor => ({
+      isDragging: monitor.isDragging(),
+      canDrag: monitor.canDrag(),
+    }),
+  });
+
+  const pageItemDropHandler = async(item: ItemNode) => {
+    const { page: droppedPage } = item;
+    if (!isDroppable(droppedPage, page, true)) {
+      return;
+    }
+    if (droppedPage.path == null || page.path == null) {
+      return;
+    }
+    const newPagePath = getNewPathAfterMoved(droppedPage.path, page.path);
+    try {
+      await apiv3Put('/pages/rename', {
+        pageId: droppedPage._id,
+        revisionId: droppedPage.revision,
+        newPagePath,
+        isRenameRedirect: false,
+        updateMetadata: true,
+      });
+      await mutatePageTree();
+      await mutateChildren();
+      if (onRenamed != null) {
+        onRenamed(page.path, newPagePath);
+      }
+      // force open
+      setIsOpen(true);
+    }
+    catch (err) {
+      // display the dropped item
+      displayDroppedItemByPageId(droppedPage._id);
+      if (err.code === 'operation__blocked') {
+        toastWarning(t('pagetree.you_cannot_move_this_page_now'));
+      }
+      else {
+        toastError(t('pagetree.something_went_wrong_with_moving_page'));
+      }
+    }
+  };
+
+  const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
+    () => ({
+      accept: 'PAGE_TREE',
+      drop: pageItemDropHandler,
+      hover: (item, monitor) => {
+        // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
+        if (monitor.isOver()) {
+          setTimeout(() => {
+            if (monitor.isOver()) {
+              setIsOpen(true);
+            }
+          }, 600);
+        }
+      },
+      canDrop: (item) => {
+        const { page: droppedPage } = item;
+        return isDroppable(droppedPage, page);
+      },
+      collect: monitor => ({
+        isOver: monitor.isOver(),
+      }),
+    }),
+    [page],
+  );
+
+  const itemRef = (c) => { drag(c); drop(c) };
+
+  const mainClassName = `${isOver ? 'grw-pagetree-is-over' : ''} ${shouldHide ? 'd-none' : ''}`;
+
+  const { NewPageInputWrapper, NewPageCreateButtonWrapper } = useNewPageInput();
+
+  return (
+    <SimpleItem
+      key={props.key}
+      targetPathOrId={props.targetPathOrId}
+      itemNode={props.itemNode}
+      isOpen
+      isEnableActions={props.isEnableActions}
+      isReadOnlyUser={props.isReadOnlyUser}
+      onRenamed={props.onRenamed}
+      onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
+      onClickDeleteMenuItem={props.onClickDeleteMenuItem}
+      itemRef={itemRef}
+      itemClass={PageTreeItem}
+      mainClassName={mainClassName}
+      customEndComponents={[Ellipsis, NewPageCreateButtonWrapper]}
+      customNextComponents={[NewPageInputWrapper]}
+    />
+  );
+};

+ 2 - 0
apps/app/src/components/Sidebar/PageTreeItem/index.ts

@@ -0,0 +1,2 @@
+export * from './PageTreeItem';
+export * from './Ellipsis';

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

@@ -7,9 +7,9 @@ import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
 
 
 import { useKeywordManager } from '~/client/services/search-operation';
 import { useKeywordManager } from '~/client/services/search-operation';
+import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import InfiniteScroll from '~/components/InfiniteScroll';
 import InfiniteScroll from '~/components/InfiniteScroll';
-import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -53,7 +53,7 @@ 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="grw-page-path-text-muted-container small">
+    <div className="small">
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
     </div>
     </div>
   );
   );

+ 1 - 0
apps/app/src/components/Sidebar/SidebarContents.tsx

@@ -3,6 +3,7 @@ import React, { memo } from 'react';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 
+
 import { Bookmarks } from './Bookmarks';
 import { Bookmarks } from './Bookmarks';
 import { CustomSidebar } from './Custom';
 import { CustomSidebar } from './Custom';
 import { PageTree } from './PageTree';
 import { PageTree } from './PageTree';

+ 18 - 0
apps/app/src/components/TreeItem/ItemNode.ts

@@ -0,0 +1,18 @@
+import { IPageForItem } from '../../interfaces/page';
+
+export class ItemNode {
+
+  page: IPageForItem;
+
+  children: ItemNode[];
+
+  constructor(page: IPageForItem, children: ItemNode[] = []) {
+    this.page = page;
+    this.children = children;
+  }
+
+  static generateNodesFromPages(pages: IPageForItem[]): ItemNode[] {
+    return pages.map(page => new ItemNode(page));
+  }
+
+}

+ 72 - 0
apps/app/src/components/TreeItem/NewPageCreateButton.tsx

@@ -0,0 +1,72 @@
+import React, {
+  useCallback, FC,
+} from 'react';
+
+import { pagePathUtils } from '@growi/core/dist/utils';
+
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
+import { IPageForItem } from '~/interfaces/page';
+import { usePageTreeDescCountMap } from '~/stores/ui';
+
+import { ItemNode } from './ItemNode';
+
+type StateHandlersType = {
+  isOpen: boolean,
+  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
+  isCreating: boolean,
+  setCreating: React.Dispatch<React.SetStateAction<boolean>>,
+};
+
+export type NewPageCreateButtonProps = {
+  page: IPageForItem,
+  currentChildren: ItemNode[],
+  stateHandlers: StateHandlersType,
+  isNewPageInputShown?: boolean,
+  setNewPageInputShown: React.Dispatch<React.SetStateAction<boolean>>,
+};
+
+export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
+  const {
+    page, currentChildren, stateHandlers, setNewPageInputShown,
+  } = props;
+
+  const { setIsOpen } = stateHandlers;
+
+  // descendantCount
+  const { getDescCount } = usePageTreeDescCountMap();
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+  const isChildrenLoaded = currentChildren?.length > 0;
+  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+  const onClickPlusButton = useCallback(() => {
+    setNewPageInputShown(true);
+
+    if (hasDescendants) {
+      setIsOpen(true);
+    }
+  }, [hasDescendants, setIsOpen]);
+
+  const test = pagePathUtils;
+  console.dir(test);
+
+  return (
+    <>
+      {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+        <NotAvailableForGuest>
+          <NotAvailableForReadOnlyUser>
+            <button
+              id="page-create-button-in-page-tree"
+              type="button"
+              className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
+              onClick={onClickPlusButton}
+            >
+              <i className="icon-plus d-block p-0" />
+            </button>
+          </NotAvailableForReadOnlyUser>
+        </NotAvailableForGuest>
+      )}
+    </>
+  );
+};

+ 103 - 0
apps/app/src/components/TreeItem/NewPageInput.tsx

@@ -0,0 +1,103 @@
+import React, { FC, useCallback, useEffect } from 'react';
+
+import nodePath from 'path';
+
+
+import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { ValidationTarget } from '~/client/util/input-validator';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
+import ClosableTextInput from '~/components/Common/ClosableTextInput';
+import { useSWRxPageChildren } from '~/stores/page-listing';
+import { usePageTreeDescCountMap } from '~/stores/ui';
+
+import { NewPageCreateButtonProps } from './NewPageCreateButton';
+import { NotDraggableForClosableTextInput } from './SimpleItem';
+
+type NewPageInputProps = NewPageCreateButtonProps & {isEnableActions: boolean};
+
+export const NewPageInput: FC<NewPageInputProps> = (props) => {
+  const { t } = useTranslation();
+
+  const {
+    page, isEnableActions, currentChildren, stateHandlers, isNewPageInputShown, setNewPageInputShown,
+  } = props;
+
+  const { isOpen, setIsOpen, setCreating } = stateHandlers;
+
+  const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
+
+  const { getDescCount } = usePageTreeDescCountMap();
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+  const isChildrenLoaded = currentChildren?.length > 0;
+  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+  const onPressEnterForCreateHandler = async(inputText: string) => {
+    setNewPageInputShown(false);
+    // closeNewPageInput();
+    const parentPath = pathUtils.addTrailingSlash(page.path as string);
+    const newPagePath = nodePath.resolve(parentPath, inputText);
+    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
+
+    if (!isCreatable) {
+      toastWarning(t('you_can_not_create_page_with_this_name'));
+      return;
+    }
+
+    try {
+      setCreating(true);
+
+      await apiv3Post('/pages/', {
+        path: newPagePath,
+        body: undefined,
+        grant: page.grant,
+        grantUserGroupId: page.grantedGroup,
+      });
+
+      mutateChildren();
+
+      if (!hasDescendants) {
+        setIsOpen(true);
+      }
+
+      toastSuccess(t('successfully_saved_the_page'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+    finally {
+      setCreating(false);
+    }
+  };
+
+  const onPressEscHandler = useCallback((event) => {
+    if (event.keyCode === 27) {
+      setNewPageInputShown(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    document.addEventListener('keydown', onPressEscHandler, false);
+    return () => {
+      document.removeEventListener('keydown', onPressEscHandler, false);
+    };
+  }, [onPressEscHandler]);
+
+  return (
+    <>
+      {isEnableActions && isNewPageInputShown && (
+        <NotDraggableForClosableTextInput>
+          <ClosableTextInput
+            placeholder={t('Input page name')}
+            onClickOutside={() => { setNewPageInputShown(false) }}
+            onPressEnter={onPressEnterForCreateHandler}
+            validationTarget={ValidationTarget.PAGE}
+          />
+        </NotDraggableForClosableTextInput>
+      )}
+    </>
+  );
+};

+ 278 - 0
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -0,0 +1,278 @@
+import React, {
+  useCallback, useState, FC, useEffect, ReactNode,
+} from 'react';
+
+import nodePath from 'path';
+
+import type { Nullable, IPageToDeleteWithMeta } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { IPageForItem } from '~/interfaces/page';
+import { IPageForPageDuplicateModal } from '~/stores/modal';
+import { useSWRxPageChildren } from '~/stores/page-listing';
+import { usePageTreeDescCountMap } from '~/stores/ui';
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
+
+import CountBadge from '../Common/CountBadge';
+
+import { ItemNode } from './ItemNode';
+
+
+export type SimpleItemProps = {
+  isEnableActions: boolean
+  isReadOnlyUser: boolean
+  itemNode: ItemNode
+  targetPathOrId?: Nullable<string>
+  isOpen?: boolean
+  onRenamed?(fromPath: string | undefined, toPath: string): void
+  onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
+  onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
+  itemRef?
+  itemClass?: React.FunctionComponent<SimpleItemProps>
+  mainClassName?: string
+  customEndComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
+  customNextComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
+};
+
+// Utility to mark target
+const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
+  if (targetPathOrId == null) {
+    return;
+  }
+
+  children.forEach((node) => {
+    if (node.page._id === targetPathOrId || node.page.path === targetPathOrId) {
+      node.page.isTarget = true;
+    }
+    else {
+      node.page.isTarget = false;
+    }
+    return node;
+  });
+};
+
+/**
+ * Return new page path after the droppedPagePath is moved under the newParentPagePath
+ * @param droppedPagePath
+ * @param newParentPagePath
+ * @returns
+ */
+
+/**
+ * Return whether the fromPage could be moved under the newParentPage
+ * @param fromPage
+ * @param newParentPage
+ * @param printLog
+ * @returns
+ */
+
+// Component wrapper to make a child element not draggable
+// https://github.com/react-dnd/react-dnd/issues/335
+type NotDraggableProps = {
+  children: ReactNode,
+};
+export const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
+  return <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
+};
+
+type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName';
+export type SimpleItemToolProps = Omit<SimpleItemProps, SimpleItemToolPropsOptional> & {page: IPageForItem};
+
+export const SimpleItemTool: FC<SimpleItemToolProps> = (props) => {
+  const { t } = useTranslation();
+  const router = useRouter();
+  const { getDescCount } = usePageTreeDescCountMap();
+
+  const page = props.page;
+
+  const pageName = nodePath.basename(page.path ?? '') || '/';
+
+  const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
+
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+  const pageTreeItemClickHandler = (e) => {
+    e.preventDefault();
+
+    if (page.path == null || page._id == null) {
+      return;
+    }
+
+    const link = pathUtils.returnPathForURL(page.path, page._id);
+
+    router.push(link);
+  };
+
+  return (
+    <>
+      {shouldShowAttentionIcon && (
+        <>
+          <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
+          <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
+            {t('tooltip.operation.attention.rename')}
+          </UncontrolledTooltip>
+        </>
+      )}
+      {page != null && page.path != null && page._id != null && (
+        <div className="grw-pagetree-title-anchor flex-grow-1">
+          <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
+        </div>
+      )}
+      {descendantCount > 0 && (
+        <div className="grw-pagetree-count-wrapper">
+          <CountBadge count={descendantCount} />
+        </div>
+      )}
+    </>
+  );
+};
+
+export const SimpleItem: FC<SimpleItemProps> = (props) => {
+  const {
+    itemNode, targetPathOrId, isOpen: _isOpen = false,
+    onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
+    itemRef, itemClass, mainClassName,
+  } = props;
+
+  const { page, children } = itemNode;
+
+  const [currentChildren, setCurrentChildren] = useState(children);
+  const [isOpen, setIsOpen] = useState(_isOpen);
+  const [isCreating, setCreating] = useState(false);
+
+  const { data } = useSWRxPageChildren(isOpen ? page._id : null);
+
+  const stateHandlers = {
+    isOpen,
+    setIsOpen,
+    isCreating,
+    setCreating,
+  };
+
+  // descendantCount
+  const { getDescCount } = usePageTreeDescCountMap();
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+
+  // hasDescendants flag
+  const isChildrenLoaded = currentChildren?.length > 0;
+  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+  const hasChildren = useCallback((): boolean => {
+    return currentChildren != null && currentChildren.length > 0;
+  }, [currentChildren]);
+
+  const onClickLoadChildren = useCallback(async() => {
+    setIsOpen(!isOpen);
+  }, [isOpen]);
+
+  // didMount
+  useEffect(() => {
+    if (hasChildren()) setIsOpen(true);
+  }, [hasChildren]);
+
+  /*
+   * Make sure itemNode.children and currentChildren are synced
+   */
+  useEffect(() => {
+    if (children.length > currentChildren.length) {
+      markTarget(children, targetPathOrId);
+      setCurrentChildren(children);
+    }
+  }, [children, currentChildren.length, targetPathOrId]);
+
+  /*
+   * When swr fetch succeeded
+   */
+  useEffect(() => {
+    if (isOpen && data != null) {
+      const newChildren = ItemNode.generateNodesFromPages(data.children);
+      markTarget(newChildren, targetPathOrId);
+      setCurrentChildren(newChildren);
+    }
+  }, [data, isOpen, targetPathOrId]);
+
+  const ItemClassFixed = itemClass ?? SimpleItem;
+
+  const commonProps = {
+    isEnableActions,
+    isReadOnlyUser,
+    isOpen: false,
+    targetPathOrId,
+    onRenamed,
+    onClickDuplicateMenuItem,
+    onClickDeleteMenuItem,
+    stateHandlers,
+  };
+
+  const CustomEndComponents = props.customEndComponents;
+
+  const SimpleItemContent = CustomEndComponents ?? [SimpleItemTool];
+
+  const SimpleItemContentProps = {
+    itemNode,
+    page,
+    onRenamed,
+    onClickDuplicateMenuItem,
+    onClickDeleteMenuItem,
+    isEnableActions,
+    isReadOnlyUser,
+    children,
+    stateHandlers,
+  };
+
+  const CustomNextComponents = props.customNextComponents;
+
+
+  return (
+    <div
+      id={`pagetree-item-${page._id}`}
+      data-testid="grw-pagetree-item-container"
+      className={`grw-pagetree-item-container ${mainClassName}`}
+    >
+      <li
+        ref={itemRef}
+        className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center
+        ${page.isTarget ? 'grw-pagetree-current-page-item' : ''}`}
+        id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
+      >
+        <div className="grw-triangle-container d-flex justify-content-center">
+          {hasDescendants && (
+            <button
+              type="button"
+              className={`grw-pagetree-triangle-btn btn ${isOpen ? 'grw-pagetree-open' : ''}`}
+              onClick={onClickLoadChildren}
+            >
+              <div className="d-flex justify-content-center">
+                <span className="material-icons-round">arrow_right</span>
+              </div>
+            </button>
+          )}
+        </div>
+        {SimpleItemContent.map(ItemContent => (
+          <ItemContent {...SimpleItemContentProps} />
+        ))}
+      </li>
+
+      {CustomNextComponents?.map(UnderItemContent => (
+        <UnderItemContent {...SimpleItemContentProps} />
+      ))}
+
+      {
+        isOpen && hasChildren() && currentChildren.map((node, index) => (
+          <div key={node.page._id} className="grw-pagetree-item-children">
+            <ItemClassFixed itemNode={node} {...commonProps} />
+            {isCreating && (currentChildren.length - 1 === index) && (
+              <div className="text-muted text-center">
+                <i className="fa fa-spinner fa-pulse mr-1"></i>
+              </div>
+            )}
+          </div>
+        ))
+      }
+    </div>
+  );
+};

+ 43 - 0
apps/app/src/components/TreeItem/UseNewPageInput.tsx

@@ -0,0 +1,43 @@
+import React, { useState, FC } from 'react';
+
+import { ItemNode } from './ItemNode';
+import { NewPageCreateButton } from './NewPageCreateButton';
+import { NewPageInput } from './NewPageInput';
+import { SimpleItemToolProps } from './SimpleItem';
+
+type UseNewPageInputProps = SimpleItemToolProps & {children: ItemNode[], stateHandlers};
+
+export const useNewPageInput = () => {
+
+  const [isNewPageInputShown, setNewPageInputShown] = useState(false);
+
+  const NewPageCreateButtonWrapper: FC<UseNewPageInputProps> = (props) => {
+    return (
+      <NewPageCreateButton
+        page={props.page}
+        currentChildren={props.children}
+        stateHandlers={props.stateHandlers}
+        setNewPageInputShown={setNewPageInputShown}
+      />
+    );
+  };
+
+  const NewPageInputWrapper = (props) => {
+    return (
+      <NewPageInput
+        page={props.page}
+        isEnableActions={props.isEnableActions}
+        currentChildren={props.chilren}
+        stateHandlers={props.stateHandlers}
+        isNewPageInputShown={isNewPageInputShown}
+        setNewPageInputShown={setNewPageInputShown}
+      />
+    );
+  };
+
+
+  return {
+    NewPageInputWrapper,
+    NewPageCreateButtonWrapper,
+  };
+};

+ 3 - 0
apps/app/src/components/TreeItem/index.ts

@@ -0,0 +1,3 @@
+export { useNewPageInput } from './UseNewPageInput';
+export * from './SimpleItem';
+export * from './ItemNode';

+ 1 - 0
apps/app/src/interfaces/ui.ts

@@ -26,3 +26,4 @@ export type OnRenamedFunction = (path: string) => void;
 export type OnDuplicatedFunction = (fromPath: string, toPath: string) => void;
 export type OnDuplicatedFunction = (fromPath: string, toPath: string) => void;
 export type OnPutBackedFunction = (path: string) => void;
 export type OnPutBackedFunction = (path: string) => void;
 export type onDeletedBookmarkFolderFunction = (bookmarkFolderId: string) => void;
 export type onDeletedBookmarkFolderFunction = (bookmarkFolderId: string) => void;
+export type OnSelectedFunction = () => void;

+ 4 - 8
apps/app/src/pages/[[...path]].page.tsx

@@ -120,9 +120,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { isLinkSharingDisabled } = props;
   const { isLinkSharingDisabled } = props;
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   return (
   return (
-    <div data-testid="grw-contextual-sub-nav">
-      <GrowiContextualSubNavigationSubstance currentPage={currentPage} isLinkSharingDisabled={isLinkSharingDisabled} />
-    </div>
+    <GrowiContextualSubNavigationSubstance currentPage={currentPage} isLinkSharingDisabled={isLinkSharingDisabled} />
   );
   );
 };
 };
 
 
@@ -325,11 +323,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
-        <header className="py-0 position-relative">
-          <div id="grw-subnav-container">
-            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
-          </div>
-        </header>
+        <nav className="sticky-top">
+          <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+        </nav>
 
 
         <DisplaySwitcher
         <DisplaySwitcher
           pageView={(
           pageView={(

+ 3 - 5
apps/app/src/pages/share/[[...path]].page.tsx

@@ -76,9 +76,7 @@ const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavi
   const { page, isLinkSharingDisabled } = props;
   const { page, isLinkSharingDisabled } = props;
 
 
   return (
   return (
-    <div data-testid="grw-contextual-sub-nav">
-      <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled} />
-    </div>
+    <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled} />
   );
   );
 };
 };
 
 
@@ -122,9 +120,9 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
       </Head>
       </Head>
 
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
-        <header className="py-0 position-relative">
+        <nav className="sticky-top">
           <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
           <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
-        </header>
+        </nav>
 
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
 

+ 7 - 16
apps/app/src/pages/trash.page.tsx

@@ -6,17 +6,16 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
+import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { useCurrentPageId } from '~/stores/page';
 import { useCurrentPageId } from '~/stores/page';
-import { useDrawerMode } from '~/stores/ui';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
 import {
   useCurrentUser, useCurrentPathname, useGrowiCloudUri,
   useCurrentUser, useCurrentPathname, useGrowiCloudUri,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser, useIsReadOnlyUser,
+  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
@@ -28,6 +27,7 @@ import {
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
 
 
+
 type Props = CommonProps & {
 type Props = CommonProps & {
   currentUser: IUser,
   currentUser: IUser,
   isSearchServiceConfigured: boolean,
   isSearchServiceConfigured: boolean,
@@ -56,10 +56,6 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
 
   useShowPageLimitationXL(props.showPageLimitationXL);
   useShowPageLimitationXL(props.showPageLimitationXL);
 
 
-  const { data: isDrawerMode } = useDrawerMode();
-  const { data: isGuestUser } = useIsGuestUser();
-  const { data: isReadOnlyUser } = useIsReadOnlyUser();
-
   const title = generateCustomTitleForPage(props, '/trash');
   const title = generateCustomTitleForPage(props, '/trash');
 
 
   return (
   return (
@@ -68,17 +64,12 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
       <div className="dynamic-layout-root">
       <div className="dynamic-layout-root">
-        <header className="py-0 position-relative">
-          <GrowiSubNavigation
-            pagePath="/trash"
-            showDrawerToggler={isDrawerMode}
-            isTagLabelsDisabled={!!isGuestUser || !!isReadOnlyUser}
-            isDrawerMode={isDrawerMode}
-            additionalClasses={['container-fluid']}
-          />
-        </header>
+        <nav className="sticky-top">
+          TODO: implement navigation for /trash
+        </nav>
 
 
         <div className="content-main container-lg grw-container-convertible mb-5 pb-5">
         <div className="content-main container-lg grw-container-convertible mb-5 pb-5">
+          <PagePathNavSticky pagePath="/trash" />
           <TrashPageList />
           <TrashPageList />
         </div>
         </div>
 
 

+ 36 - 2
apps/app/src/stores/modal.tsx

@@ -8,8 +8,8 @@ import { SWRResponse } from 'swr';
 import Linker from '~/client/models/Linker';
 import Linker from '~/client/models/Linker';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
-import {
-  OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction, onDeletedBookmarkFolderFunction,
+import type {
+  OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction, onDeletedBookmarkFolderFunction, OnSelectedFunction,
 } from '~/interfaces/ui';
 } from '~/interfaces/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -739,3 +739,37 @@ export const useLinkEditModal = (): SWRResponse<LinkEditModalStatus, Error> & Li
     },
     },
   });
   });
 };
 };
+
+/*
+* PageSelectModal
+*/
+export type IPageSelectModalOption = {
+  onSelected?: OnSelectedFunction,
+}
+
+type PageSelectModalStatus = {
+  isOpened: boolean
+  opts?: IPageSelectModalOption
+}
+
+type PageSelectModalStatusUtils = {
+  open(): Promise<PageSelectModalStatus | undefined>
+  close(): Promise<PageSelectModalStatus | undefined>
+}
+
+export const usePageSelectModal = (
+    status?: PageSelectModalStatus,
+): SWRResponse<PageSelectModalStatus, Error> & PageSelectModalStatusUtils => {
+  const initialStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<PageSelectModalStatus, Error>('PageSelectModal', status, { fallbackData: initialStatus });
+
+  return {
+    ...swrResponse,
+    open: (
+        opts?: IPageSelectModalOption,
+    ) => swrResponse.mutate({
+      isOpened: true, opts,
+    }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};

+ 0 - 40
apps/app/src/styles/_editor.scss

@@ -7,24 +7,6 @@
 .layout-root.editing {
 .layout-root.editing {
   overflow-y: hidden !important;
   overflow-y: hidden !important;
 
 
-  .grw-navbar {
-    position: fixed !important;
-    width: 100vw;
-  }
-
-  // restrict height of subnav
-  .grw-subnav {
-    height: var.$grw-subnav-height-on-edit;
-    min-height: unset;
-    padding-top: 0;
-    padding-right: 15px;
-    padding-left: 15px;
-
-    @include bs.media-breakpoint-up(lg) {
-      height: var.$grw-subnav-height-lg-on-edit;
-    }
-  }
-
   .page-wrapper {
   .page-wrapper {
     top: 0;
     top: 0;
     height: 100vh;
     height: 100vh;
@@ -98,28 +80,6 @@
     }
     }
   }
   }
 
 
-  // ellipsis .grw-page-path-hierarchical-link
-  .grw-subnav-start-side {
-    overflow: hidden;
-    .grw-path-nav-container {
-      margin-right: 1rem;
-      overflow: hidden;
-      .grw-page-path-nav {
-        white-space: nowrap;
-
-        .grw-page-path-hierarchical-link {
-          width: 100%;
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
-
-        h1 {
-          overflow: hidden;
-        }
-      }
-    }
-  }
-
   .grw-copy-dropdown {
   .grw-copy-dropdown {
     .btn-copy {
     .btn-copy {
       padding: 3px !important; // overwrite padding
       padding: 3px !important; // overwrite padding

+ 1 - 0
apps/app/src/styles/_layout.scss

@@ -13,6 +13,7 @@
 
 
 .dynamic-layout-root {
 .dynamic-layout-root {
   @extend %flex-expand-vert;
   @extend %flex-expand-vert;
+  overflow-y: unset;
 }
 }
 
 
 .dynamic-layout-root.growi-layout-fluid .grw-container-convertible {
 .dynamic-layout-root.growi-layout-fluid .grw-container-convertible {

+ 0 - 16
apps/app/src/styles/_mixins.scss

@@ -1,22 +1,6 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use './variables' as var;
 @use './variables' as var;
 
 
-@mixin variable-font-size($basesize) {
-  font-size: $basesize * 0.6;
-
-  @include bs.media-breakpoint-only(sm) {
-    font-size: #{$basesize * 0.7};
-  }
-  @include bs.media-breakpoint-only(md) {
-    font-size: #{$basesize * 0.8};
-  }
-  @include bs.media-breakpoint-only(lg) {
-    font-size: #{$basesize * 0.9};
-  }
-  @include bs.media-breakpoint-up(xl) {
-    font-size: $basesize;
-  }
-}
 
 
 @mixin apply-navigation-transition() {
 @mixin apply-navigation-transition() {
   transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
   transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);

+ 0 - 6
apps/app/src/styles/_page-path.scss

@@ -1,6 +0,0 @@
-.grw-page-path-hierarchical-link {
-  .separator {
-    margin-right: 0.2em;
-    margin-left: 0.2em;
-  }
-}

+ 0 - 7
apps/app/src/styles/_variables.scss

@@ -6,13 +6,6 @@ $grw-marker-cyan: #6ff;
 $grw-marker-green: #6f6;
 $grw-marker-green: #6f6;
 
 
 //== Layout
 //== Layout
-$grw-subnav-min-height: 95px;
-$grw-subnav-min-height-md: 115px;
-$grw-subnav-height-on-edit: 95px;
-$grw-subnav-height-lg-on-edit: 50px;
-
-$grw-subnav-search-preview-min-height: 90px;
-
 $grw-navbar-bottom-height: 48px;
 $grw-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 
 

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

@@ -23,7 +23,6 @@
 @import 'layout';
 @import 'layout';
 @import 'mirror_mode';
 @import 'mirror_mode';
 @import 'modal';
 @import 'modal';
-@import 'page-path';
 @import 'tag';
 @import 'tag';
 @import 'installer';
 @import 'installer';
 
 

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

@@ -258,7 +258,7 @@ context('Access to Template Editing Mode', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // wait until
       // wait until
@@ -293,7 +293,7 @@ context('Access to Template Editing Mode', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // Wait until
       // Wait until
@@ -327,7 +327,7 @@ context('Access to Template Editing Mode', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       //do
       //do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // wait until
       // wait until

+ 12 - 12
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--click-page-icons.cy.ts

@@ -25,7 +25,7 @@ context('Click page icons button', () => {
     cy.getByTestid('subscribe-button-tooltip').should('not.exist');
     cy.getByTestid('subscribe-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}1-subscribe-page`) })
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}1-subscribe-page`) })
 
 
     // Unsubscribe
     // Unsubscribe
     cy.get('#subscribe-button').click({force: true});
     cy.get('#subscribe-button').click({force: true});
@@ -40,7 +40,7 @@ context('Click page icons button', () => {
     cy.getByTestid('subscribe-button-tooltip').should('not.exist');
     cy.getByTestid('subscribe-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}2-unsubscribe-page`) })
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}2-unsubscribe-page`) })
   });
   });
 
 
   it('Successfully Like / Dislike a page', () => {
   it('Successfully Like / Dislike a page', () => {
@@ -60,12 +60,12 @@ context('Click page icons button', () => {
     cy.getByTestid('like-button-tooltip').should('not.exist');
     cy.getByTestid('like-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}3-like-page`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}3-like-page`) });
 
 
     // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
     // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
     // cy.get('#po-total-likes').click({force: true});
     // cy.get('#po-total-likes').click({force: true});
     // cy.get('.user-list-popover').should('be.visible')
     // cy.get('.user-list-popover').should('be.visible')
-    // cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}4-likes-counter`) });
+    // cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}4-likes-counter`) });
 
 
     // unlike
     // unlike
     cy.get('#like-button').click({force: true});
     cy.get('#like-button').click({force: true});
@@ -80,12 +80,12 @@ context('Click page icons button', () => {
     cy.getByTestid('like-button-tooltip').should('not.exist');
     cy.getByTestid('like-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}5-dislike-page`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}5-dislike-page`) });
 
 
     // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
     // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
     // cy.get('#po-total-likes').click({force: true});
     // cy.get('#po-total-likes').click({force: true});
     // cy.get('.user-list-popover').should('be.visible');
     // cy.get('.user-list-popover').should('be.visible');
-    // cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}6-likes-counter`) });
+    // cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}6-likes-counter`) });
   });
   });
 
 
   it('Successfully Bookmark / Unbookmark a page', () => {
   it('Successfully Bookmark / Unbookmark a page', () => {
@@ -105,7 +105,7 @@ context('Click page icons button', () => {
     cy.getByTestid('bookmark-button-tooltip').should('not.exist');
     cy.getByTestid('bookmark-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}7-bookmark-page`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}7-bookmark-page`) });
 
 
     // total bookmarker
     // total bookmarker
     cy.waitUntil(() => {
     cy.waitUntil(() => {
@@ -117,7 +117,7 @@ context('Click page icons button', () => {
       });
       });
     });
     });
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}8-bookmarks-counter`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}8-bookmarks-counter`) });
 
 
     // unbookmark
     // unbookmark
     cy.get('#bookmark-dropdown-btn').click({force: true});
     cy.get('#bookmark-dropdown-btn').click({force: true});
@@ -134,7 +134,7 @@ context('Click page icons button', () => {
     cy.getByTestid('bookmark-button-tooltip').should('not.exist');
     cy.getByTestid('bookmark-button-tooltip').should('not.exist');
 
 
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}9-unbookmark-page`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}9-unbookmark-page`) });
 
 
     // total bookmarker
     // total bookmarker
     cy.waitUntil(() => {
     cy.waitUntil(() => {
@@ -146,7 +146,7 @@ context('Click page icons button', () => {
       });
       });
     });
     });
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
-    cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}10-bookmarks-counter`) });
+    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}10-bookmarks-counter`) });
   });
   });
 
 
   // user-list-popover is commented out because it is sometimes displayed and sometimes not
   // user-list-popover is commented out because it is sometimes displayed and sometimes not
@@ -154,7 +154,7 @@ context('Click page icons button', () => {
   //   cy.visit('/Sandbox');
   //   cy.visit('/Sandbox');
   //   cy.waitUntilSkeletonDisappear();
   //   cy.waitUntilSkeletonDisappear();
 
 
-  //   cy.get('#grw-subnav-container').within(() => {
+  //   cy.getByTestid('grw-contextual-sub-nav').within(() => {
   //     cy.get('div.grw-seen-user-info').find('button#btn-seen-user').click({force: true});
   //     cy.get('div.grw-seen-user-info').find('button#btn-seen-user').click({force: true});
   //   });
   //   });
 
 
@@ -168,7 +168,7 @@ context('Click page icons button', () => {
 
 
   //   cy.get('.user-list-popover').should('be.visible')
   //   cy.get('.user-list-popover').should('be.visible')
 
 
-  //   cy.get('#grw-subnav-container').within(() => {
+  //   cy.getByTestid('grw-contextual-sub-nav').within(() => {
   //     cy.screenshot(`${ssPrefix}11-seen-user-list`);
   //     cy.screenshot(`${ssPrefix}11-seen-user-list`);
   //   });
   //   });
   // });
   // });

+ 4 - 4
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

@@ -14,7 +14,7 @@ context('Modal for page operation', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       //wait until
       //wait until
@@ -47,7 +47,7 @@ context('Modal for page operation', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // wait until
       // wait until
@@ -65,7 +65,7 @@ context('Modal for page operation', () => {
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // wait until
       // wait until
@@ -97,7 +97,7 @@ context('Modal for page operation', () => {
 //   it('PresentationModal for "/" is shown successfully', () => {
 //   it('PresentationModal for "/" is shown successfully', () => {
 //     cy.visit('/');
 //     cy.visit('/');
 
 
-//     cy.get('#grw-subnav-container').within(() => {
+//     cy.getByTestid('grw-contextual-sub-nav').within(() => {
 //       cy.getByTestid('open-page-item-control-btn').click({force: true});
 //       cy.getByTestid('open-page-item-control-btn').click({force: true});
 //       cy.getByTestid('open-presentation-modal-btn').click({force: true});
 //       cy.getByTestid('open-presentation-modal-btn').click({force: true});
 //     });
 //     });

+ 1 - 1
apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts

@@ -14,7 +14,7 @@ context('Access to sharelink by guest', () => {
     // open dropdown
     // open dropdown
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn', { timeout: 14000 }).find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn', { timeout: 14000 }).find('button').click({force: true});
       });
       });
       // wait until
       // wait until

+ 1 - 1
apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts

@@ -143,7 +143,7 @@ context.skip('Editor while navigation', () => {
     // open duplicate modal
     // open duplicate modal
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('grw-contextual-sub-nav').within(() => {
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
         cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
       });
       });
       // wait until
       // wait until

+ 44 - 0
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -3,6 +3,7 @@ import {
 } from 'react';
 } from 'react';
 
 
 import { indentUnit } from '@codemirror/language';
 import { indentUnit } from '@codemirror/language';
+import { EditorView } from '@codemirror/view';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
 
 import { GlobalCodeMirrorEditorKey } from '../../consts';
 import { GlobalCodeMirrorEditorKey } from '../../consts';
@@ -54,6 +55,49 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
 
   }, [codeMirrorEditor, indentSize]);
   }, [codeMirrorEditor, indentSize]);
 
 
+  useEffect(() => {
+    const handlePaste = (event: ClipboardEvent) => {
+      event.preventDefault();
+
+      if (event.clipboardData == null) {
+        return;
+      }
+
+      if (onUpload != null && event.clipboardData.types.includes('Files')) {
+        onUpload(Array.from(event.clipboardData.files));
+      }
+
+      if (event.clipboardData.types.includes('text/plain')) {
+        const textData = event.clipboardData.getData('text/plain');
+        codeMirrorEditor?.replaceText(textData);
+      }
+    };
+
+    const extension = EditorView.domEventHandlers({
+      paste: handlePaste,
+    });
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(extension);
+    return cleanupFunction;
+
+  }, [codeMirrorEditor, onUpload]);
+
+  useEffect(() => {
+
+    const handleDrop = (event: DragEvent) => {
+      // prevents conflicts between codemirror and react-dropzone during file drops.
+      event.preventDefault();
+    };
+
+    const extension = EditorView.domEventHandlers({
+      drop: handleDrop,
+    });
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(extension);
+    return cleanupFunction;
+
+  }, [codeMirrorEditor]);
+
   const { getRootProps, open } = useFileDropzone({ onUpload });
   const { getRootProps, open } = useFileDropzone({ onUpload });
 
 
   return (
   return (

+ 7 - 1
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -58,11 +58,17 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
       {
       {
         extensions: defaultExtensions,
         extensions: defaultExtensions,
         // Reset settings of react-codemirror.
         // Reset settings of react-codemirror.
-        // The extension defined first will be used, so it must be disabled here.
+        // Extensions are defined first will be used if they have the same priority.
+        // If extensions conflict, disable them here.
+        // And add them to defaultExtensions: Extension[] with a lower priority.
+        // ref: https://codemirror.net/examples/config/
+        // ------- Start -------
         indentWithTab: false,
         indentWithTab: false,
         basicSetup: {
         basicSetup: {
           defaultKeymap: false,
           defaultKeymap: false,
+          dropCursor: false,
         },
         },
+        // ------- End -------
       },
       },
     );
     );
   }, [props]);
   }, [props]);

+ 43 - 1
yarn.lock

@@ -5855,6 +5855,11 @@ cjs-module-lexer@^1.0.0:
   resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
   resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
   integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
   integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
 
 
+classnames@^2.0.0:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
+  integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
+
 classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.6:
 classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.6:
   version "2.3.1"
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
@@ -6312,6 +6317,11 @@ core-js@^3, core-js@^3.0.1, core-js@^3.2.1:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
   integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==
   integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==
 
 
+core-js@^3.6.5:
+  version "3.33.0"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
+  integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
+
 core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0:
 core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -7846,7 +7856,7 @@ eventemitter2@6.4.7:
   resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
   resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
   integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
   integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
 
 
-eventemitter3@^3.1.0:
+eventemitter3@^3.0.0, eventemitter3@^3.1.0:
   version "3.1.2"
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
   integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
   integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
@@ -13377,6 +13387,13 @@ quick-lru@^4.0.1:
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
 
 
+raf@^3.0.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+  dependencies:
+    performance-now "^2.1.0"
+
 random-bytes@~1.0.0:
 random-bytes@~1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
   resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
@@ -13652,6 +13669,17 @@ react-scroll@^1.8.7:
     lodash.throttle "^4.1.1"
     lodash.throttle "^4.1.1"
     prop-types "^15.7.2"
     prop-types "^15.7.2"
 
 
+react-stickynode@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/react-stickynode/-/react-stickynode-4.1.0.tgz#ecd80987f64b98f999c589cd4b992eee6bed9562"
+  integrity sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==
+  dependencies:
+    classnames "^2.0.0"
+    core-js "^3.6.5"
+    prop-types "^15.6.0"
+    shallowequal "^1.0.0"
+    subscribe-ui-event "^2.0.6"
+
 react-syntax-highlighter@^15.5.0:
 react-syntax-highlighter@^15.5.0:
   version "15.5.0"
   version "15.5.0"
   resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
   resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
@@ -14626,6 +14654,11 @@ sha.js@^2.4.11:
     inherits "^2.0.1"
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
     safe-buffer "^5.0.1"
 
 
+shallowequal@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+  integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
 shebang-command@^1.2.0:
 shebang-command@^1.2.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -15421,6 +15454,15 @@ stylis@^4.1.2:
   resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
   resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
   integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
   integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
 
 
+subscribe-ui-event@^2.0.6:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz#8d18b6339c35b25246a5335775573f0e5dc461f8"
+  integrity sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==
+  dependencies:
+    eventemitter3 "^3.0.0"
+    lodash "^4.17.15"
+    raf "^3.0.0"
+
 superjson@^1.9.1:
 superjson@^1.9.1:
   version "1.9.1"
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.9.1.tgz#e23bd2e8cf0f4ade131d6d769754cac7eaa8ab34"
   resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.9.1.tgz#e23bd2e8cf0f4ade131d6d769754cac7eaa8ab34"