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

Merge pull request #8160 from weseek/imprv/sidebar-mode

imprv: Sidebar mode
Yuki Takei 2 лет назад
Родитель
Сommit
25467b5624
51 измененных файлов с 1175 добавлено и 1007 удалено
  1. 0 0
      apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx
  2. 0 0
      apps/app/_obsolete/src/components/Sidebar/NavigationResizeHexagon.tsx
  3. 18 23
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  4. 1 11
      apps/app/src/client/services/user-ui-settings.ts
  5. 10 12
      apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  6. 0 2
      apps/app/src/components/ItemsTree/ItemsTree.module.scss
  7. 4 2
      apps/app/src/components/Layout/BasicLayout.tsx
  8. 94 0
      apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss
  9. 46 0
      apps/app/src/components/Sidebar/AppTitle/AppTitle.tsx
  10. 37 0
      apps/app/src/components/Sidebar/ResizableArea/ResizableArea.module.scss
  11. 99 0
      apps/app/src/components/Sidebar/ResizableArea/ResizableArea.tsx
  12. 1 0
      apps/app/src/components/Sidebar/ResizableArea/index.ts
  13. 45 189
      apps/app/src/components/Sidebar/Sidebar.module.scss
  14. 136 286
      apps/app/src/components/Sidebar/Sidebar.tsx
  15. 23 20
      apps/app/src/components/Sidebar/SidebarContents.tsx
  16. 26 0
      apps/app/src/components/Sidebar/SidebarHead/SidebarHead.module.scss
  17. 17 0
      apps/app/src/components/Sidebar/SidebarHead/SidebarHead.tsx
  18. 52 0
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss
  19. 41 0
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx
  20. 1 0
      apps/app/src/components/Sidebar/SidebarHead/index.ts
  21. 0 134
      apps/app/src/components/Sidebar/SidebarNav.module.scss
  22. 0 138
      apps/app/src/components/Sidebar/SidebarNav.tsx
  23. 71 0
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.module.scss
  24. 112 0
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  25. 45 0
      apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.module.scss
  26. 49 0
      apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx
  27. 33 0
      apps/app/src/components/Sidebar/SidebarNav/SidebarNav.module.scss
  28. 31 0
      apps/app/src/components/Sidebar/SidebarNav/SidebarNav.tsx
  29. 1 0
      apps/app/src/components/Sidebar/SidebarNav/_variables.scss
  30. 1 0
      apps/app/src/components/Sidebar/SidebarNav/index.ts
  31. 29 0
      apps/app/src/components/Sidebar/_button-styles.scss
  32. 1 0
      apps/app/src/components/Sidebar/_variables.scss
  33. 1 2
      apps/app/src/interfaces/sidebar-config.ts
  34. 8 0
      apps/app/src/interfaces/ui.ts
  35. 1 3
      apps/app/src/interfaces/user-ui-settings.ts
  36. 1 2
      apps/app/src/pages/_private-legacy-pages.page.tsx
  37. 1 2
      apps/app/src/pages/_search.page.tsx
  38. 1 2
      apps/app/src/pages/me/[[...path]].page.tsx
  39. 1 2
      apps/app/src/pages/tags.page.tsx
  40. 1 2
      apps/app/src/pages/trash.page.tsx
  41. 3 6
      apps/app/src/pages/utils/commons.ts
  42. 1 2
      apps/app/src/server/models/config.ts
  43. 1 3
      apps/app/src/server/models/user-ui-settings.ts
  44. 5 9
      apps/app/src/server/routes/apiv3/customize-setting.js
  45. 2 6
      apps/app/src/server/routes/apiv3/user-ui-settings.ts
  46. 52 0
      apps/app/src/stores/admin/sidebar-config.tsx
  47. 52 115
      apps/app/src/stores/ui.tsx
  48. 0 6
      apps/app/src/styles/_variables.scss
  49. 2 2
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  50. 14 22
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  51. 4 4
      apps/app/test/cypress/support/commands.ts

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


+ 0 - 0
apps/app/src/components/Sidebar/NavigationResizeHexagon.tsx → apps/app/_obsolete/src/components/Sidebar/NavigationResizeHexagon.tsx


+ 18 - 23
apps/app/_obsolete/src/styles/theme/apply-colors.scss

@@ -206,29 +206,24 @@ ul.pagination {
   $bgcolor-resize-button: var(--bgcolor-resize-button,white);
   $bgcolor-resize-button: var(--bgcolor-resize-button,white);
   $color-resize-button-hover: var(--color-resize-button-hover,var(--color-reversal));
   $color-resize-button-hover: var(--color-resize-button-hover,var(--color-reversal));
   $bgcolor-resize-button-hover: var(--bgcolor-resize-button-hover,#{hsl.lighten(var(--bgcolor-resize-button), 5%)});
   $bgcolor-resize-button-hover: var(--bgcolor-resize-button-hover,#{hsl.lighten(var(--bgcolor-resize-button), 5%)});
-  .grw-navigation-resize-button {
-    .hexagon-container svg {
-      .background {
-        fill: var(--bgcolor-resize-button);
-      }
-      .icon {
-        fill: var(--color-resize-button);
-      }
-    }
-    &:hover .hexagon-container svg {
-      .background {
-        fill: var(--bgcolor-resize-button-hover);
-      }
-      .icon {
-        fill: var(--color-resize-button-hover);
-      }
-    }
-  }
-  div.grw-global-navigation {
-    > div {
-      background-color: var(--bgcolor-sidebar);
-    }
-  }
+  // .grw-navigation-resize-button {
+  //   .hexagon-container svg {
+  //     .background {
+  //       fill: var(--bgcolor-resize-button);
+  //     }
+  //     .icon {
+  //       fill: var(--color-resize-button);
+  //     }
+  //   }
+  //   &:hover .hexagon-container svg {
+  //     .background {
+  //       fill: var(--bgcolor-resize-button-hover);
+  //     }
+  //     .icon {
+  //       fill: var(--color-resize-button-hover);
+  //     }
+  //   }
+  // }
   div.grw-contextual-navigation {
   div.grw-contextual-navigation {
     > div {
     > div {
       color: var(--color-sidebar-context);
       color: var(--color-sidebar-context);

+ 1 - 11
apps/app/src/client/services/user-ui-settings.ts

@@ -18,7 +18,7 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
 const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 
 
 type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
 type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
-const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
+export const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
   settingsForBulk = {
   settingsForBulk = {
     ...settingsForBulk,
     ...settingsForBulk,
     ...settings,
     ...settings,
@@ -26,13 +26,3 @@ const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>
 
 
   return _putUserUISettingsInBulkDebounced();
   return _putUserUISettingsInBulkDebounced();
 };
 };
-
-type UserUISettingsUtil = {
-  scheduleToPut: ScheduleToPutFunction | (() => void),
-}
-export const useUserUISettings = (): UserUISettingsUtil => {
-
-  return {
-    scheduleToPut,
-  };
-};

+ 10 - 12
apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -4,14 +4,14 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useSWRxSidebarConfig } from '~/stores/ui';
+import { useSWRxSidebarConfig } from '~/stores/admin/sidebar-config';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 
 
 const CustomizeSidebarsetting = (): JSX.Element => {
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
   const {
   const {
-    update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
+    update, isSidebarCollapsedMode, setIsSidebarCollapsedMode,
   } = useSWRxSidebarConfig();
   } = useSWRxSidebarConfig();
 
 
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
@@ -45,8 +45,8 @@ const CustomizeSidebarsetting = (): JSX.Element => {
             <div id="layoutOptions" className="row row-cols-2">
             <div id="layoutOptions" className="row row-cols-2">
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${isSidebarDrawerMode ? 'border-active' : ''}`}
-                  onClick={() => setIsSidebarDrawerMode(true)}
+                  className={`card customize-layout-card ${isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  onClick={() => setIsSidebarCollapsedMode(true)}
                   role="button"
                   role="button"
                 >
                 >
                   <img src={drawerIconFileName} />
                   <img src={drawerIconFileName} />
@@ -57,8 +57,8 @@ const CustomizeSidebarsetting = (): JSX.Element => {
               </div>
               </div>
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${!isSidebarDrawerMode ? 'border-active' : ''}`}
-                  onClick={() => setIsSidebarDrawerMode(false)}
+                  className={`card customize-layout-card ${!isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  onClick={() => setIsSidebarCollapsedMode(false)}
                   role="button"
                   role="button"
                 >
                 >
                   <img src={dockIconFileName} />
                   <img src={dockIconFileName} />
@@ -83,9 +83,8 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 id="is-open"
                 id="is-open"
                 className="form-check-input"
                 className="form-check-input"
                 name="mailVisibility"
                 name="mailVisibility"
-                checked={isSidebarDrawerMode === false && isSidebarClosedAtDockMode === false}
-                disabled={isSidebarDrawerMode}
-                onChange={() => setIsSidebarClosedAtDockMode(false)}
+                checked={isSidebarCollapsedMode === false}
+                onChange={() => setIsSidebarCollapsedMode(false)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-open">
               <label className="form-label form-check-label" htmlFor="is-open">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
@@ -97,9 +96,8 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 id="is-closed"
                 id="is-closed"
                 className="form-check-input"
                 className="form-check-input"
                 name="mailVisibility"
                 name="mailVisibility"
-                checked={isSidebarDrawerMode === false && isSidebarClosedAtDockMode === true}
-                disabled={isSidebarDrawerMode}
-                onChange={() => setIsSidebarClosedAtDockMode(true)}
+                checked={isSidebarCollapsedMode === true}
+                onChange={() => setIsSidebarCollapsedMode(true)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-closed">
               <label className="form-label form-check-label" htmlFor="is-closed">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}

+ 0 - 2
apps/app/src/components/ItemsTree/ItemsTree.module.scss

@@ -17,8 +17,6 @@ $grw-pagetree-item-container-height: 40px;
   }
   }
 
 
   :global {
   :global {
-    min-height: calc(100vh - ($grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
-
     .btn-page-item-control {
     .btn-page-item-control {
       .icon-plus::before {
       .icon-plus::before {
         font-size: 18px;
         font-size: 18px;

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

@@ -37,9 +37,11 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
       <DndProvider backend={HTML5Backend}>
       <DndProvider backend={HTML5Backend}>
 
 
         <div className="page-wrapper flex-row">
         <div className="page-wrapper flex-row">
-          <Sidebar />
+          <div className="z-2">
+            <Sidebar />
+          </div>
 
 
-          <div className="d-flex flex-grow-1 flex-column">{/* neccessary for nested {children} make expanded */}
+          <div className="d-flex flex-grow-1 flex-column z-1">{/* neccessary for nested {children} make expanded */}
             <AlertSiteUrlUndefined />
             <AlertSiteUrlUndefined />
             {children}
             {children}
           </div>
           </div>

+ 94 - 0
apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -0,0 +1,94 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '@growi/core/scss/growi-official-colors';
+
+@use '../button-styles';
+@use '../variables' as var;
+
+// GROWI Logo
+.grw-app-title :global {
+  .grw-logo {
+    $width: var.$grw-sidebar-nav-width;
+    $height: var.$grw-sidebar-nav-width; // declare $height with the same value as the sidebar nav width
+    $logomark-width: 27.7px;
+    $logomark-height: 24px;
+
+    width: $width;
+
+    svg {
+      width: $width;
+      height: $height;
+      padding: (($height - $logomark-height) / 2) (($width - $logomark-width) / 2);
+    }
+  }
+}
+
+
+// == Location
+.on-subnavigation {
+  $grw-contextual-sub-navigation-width: 500px;
+
+  left: var.$grw-sidebar-nav-width;
+  // set width for truncation
+  width: calc(100vw - $grw-contextual-sub-navigation-width);
+}
+
+.on-sidebar-head {
+  $toggle-collapse-button-width: 50px;
+
+  // set width for truncation
+  width: calc(100% - $toggle-collapse-button-width);
+}
+
+
+// == Interaction
+@keyframes bounce-to-right {
+  10% { transform:translateX(3px); }
+  20% { transform:translateX(0%); }
+  25% { transform:translateX(2px); }
+  27% { transform:translateX(0%); }
+}
+
+.on-subnavigation {
+  animation: bounce-to-right 1s ease;
+}
+
+@keyframes bounce-to-left {
+  0% { transform:translateX(48px); }
+  100% { transform:translateX(0%); }
+}
+
+.on-sidebar-head {
+  animation: bounce-to-left 0.2s ease;
+}
+
+
+
+// == Colors
+.grw-app-title :global {
+  .grw-logo {
+    // set transition for fill
+    svg, svg * {
+      transition: fill 0.8s ease-out;
+    }
+
+    fill: var(--grw-app-title-color, var(--bs-tertiary-color));
+
+    &:hover {
+      svg {
+        .group1 {
+          fill: growi-official-colors.$growi-green;
+        }
+
+        .group2 {
+          fill: growi-official-colors.$growi-blue;
+        }
+      }
+    }
+  }
+
+  .grw-site-name {
+    --bs-link-color-rgb: var(--grw-app-title-color-rgb, var(--bs-tertiary-color-rgb));
+    --bs-link-opacity: 0.5;
+  }
+}

+ 46 - 0
apps/app/src/components/Sidebar/AppTitle/AppTitle.tsx

@@ -0,0 +1,46 @@
+import React, { memo } from 'react';
+
+import Link from 'next/link';
+
+import { useAppTitle, useIsDefaultLogo } from '~/stores/context';
+
+import { SidebarBrandLogo } from '../SidebarBrandLogo';
+
+import styles from './AppTitle.module.scss';
+
+
+type Props = {
+  className?: string,
+}
+
+const AppTitleSubstance = memo((props: Props): JSX.Element => {
+
+  const { className } = props;
+
+  const { data: isDefaultLogo } = useIsDefaultLogo();
+  const { data: appTitle } = useAppTitle();
+
+  return (
+    <div className={`${styles['grw-app-title']} ${className} d-flex d-edit-none`}>
+      {/* Brand Logo  */}
+      <Link href="/" className="grw-logo d-block">
+        <SidebarBrandLogo isDefaultLogo={isDefaultLogo} />
+      </Link>
+      <div className="flex-grow-1 d-flex align-items-center justify-content-between gap-3 overflow-hidden">
+        <div className="grw-site-name text-truncate">
+          <Link href="/" className="fs-4">
+            {appTitle}
+          </Link>
+        </div>
+      </div>
+    </div>
+  );
+});
+
+export const AppTitleOnSubnavigation = memo((): JSX.Element => {
+  return <AppTitleSubstance className={`position-absolute ${styles['on-subnavigation']}`} />;
+});
+
+export const AppTitleOnSidebarHead = memo((): JSX.Element => {
+  return <AppTitleSubstance className={`position-absolute z-1 ${styles['on-sidebar-head']}`} />;
+});

+ 37 - 0
apps/app/src/components/Sidebar/ResizableArea/ResizableArea.module.scss

@@ -0,0 +1,37 @@
+.grw-resizable-area :global {
+  will-change: width;
+}
+
+.grw-resizable-area:not(:global .dragging) {
+  transition: width 100ms cubic-bezier(0.2, 0, 0, 1) 0s;
+}
+
+
+.grw-navigation-draggable :global {
+  position: absolute;
+  top: 0px;
+  bottom: 0px;
+  left: 100%;
+
+  .grw-navigation-draggable-hitarea {
+    position: absolute;
+    left: -4px;
+    width: 24px;
+    height: 100%;
+    cursor: ew-resize;
+  }
+  .grw-navigation-draggable-line {
+    position: absolute;
+    left: -1px;
+    display: none;
+    width: 3px;
+    height: 100%;
+    background-color: rgb(76, 154, 255);
+  }
+}
+
+.grw-navigation-draggable:hover :global {
+  .grw-navigation-draggable-line {
+    display: block;
+  }
+}

+ 99 - 0
apps/app/src/components/Sidebar/ResizableArea/ResizableArea.tsx

@@ -0,0 +1,99 @@
+import React, { memo, useCallback, useRef } from 'react';
+
+
+import styles from './ResizableArea.module.scss';
+
+
+type Props = {
+  className?: string,
+  width?: number,
+  minWidth?: number,
+  disabled?: boolean,
+  children?: React.ReactNode,
+  onResize?: (newWidth: number) => void,
+  onResizeDone?: (newWidth: number) => void,
+  onCollapsed?: () => void,
+}
+
+export const ResizableArea = memo((props: Props): JSX.Element => {
+  const {
+    className,
+    width, minWidth = 0,
+    disabled, children,
+    onResize, onResizeDone, onCollapsed,
+  } = props;
+
+  const resizableContainer = useRef<HTMLDivElement>(null);
+
+  const draggableAreaMoveHandler = useCallback((event: MouseEvent) => {
+    event.preventDefault();
+
+    const widthByMousePos = event.pageX;
+
+    const newWidth = Math.max(widthByMousePos, minWidth);
+    onResize?.(newWidth);
+    resizableContainer.current?.classList.add('dragging');
+  }, [minWidth, onResize]);
+
+  const dragableAreaMouseUpHandler = useCallback((event: MouseEvent) => {
+    if (resizableContainer.current == null) {
+      return;
+    }
+
+    const widthByMousePos = event.pageX;
+
+    if (widthByMousePos < minWidth / 2) {
+      // force collapsed
+      onCollapsed?.();
+    }
+    else {
+      const newWidth = resizableContainer.current.clientWidth;
+      onResizeDone?.(newWidth);
+    }
+
+    resizableContainer.current.classList.remove('dragging');
+
+  }, [minWidth, onCollapsed, onResizeDone]);
+
+  const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => {
+    if (disabled) {
+      return;
+    }
+
+    event.preventDefault();
+
+    const removeEventListeners = () => {
+      document.removeEventListener('mousemove', draggableAreaMoveHandler);
+      document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
+      document.removeEventListener('mouseup', removeEventListeners);
+    };
+
+    document.addEventListener('mousemove', draggableAreaMoveHandler);
+    document.addEventListener('mouseup', dragableAreaMouseUpHandler);
+    document.addEventListener('mouseup', removeEventListeners);
+
+  }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, disabled]);
+
+  return (
+    <>
+      <div
+        ref={resizableContainer}
+        className={`${styles['grw-resizable-area']} ${className}`}
+        style={{ width }}
+      >
+        {children}
+      </div>
+      <div className={styles['grw-navigation-draggable']}>
+        { !disabled && (
+          <>
+            <div
+              className="grw-navigation-draggable-hitarea"
+              onMouseDown={dragableAreaMouseDownHandler}
+            />
+            <div className="grw-navigation-draggable-line"></div>
+          </>
+        ) }
+      </div>
+    </>
+  );
+});

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

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

+ 45 - 189
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -1,139 +1,20 @@
-@use '~/styles/mixins';
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 
 
-.grw-sidebar :global {
-  // sticky
-  position: sticky;
-  top: 0;
+@use '~/styles/mixins';
 
 
-  // set the max value that should be taken when sticky
-  height: 100vh;
+@use './variables' as var;
 
 
-  border-right : 1px solid var(--bs-border-color);
+.grw-sidebar :global {
+  top: 0;
 
 
-  .data-layout-container {
-    display: flex;
-    flex-direction: row;
-    height: 100vh;
-    margin-top: 0px;
+  .sidebar-contents-container {
+    backdrop-filter: blur(20px);
   }
   }
-  .navigation {
-    .grw-navigation-wrap {
-      display: flex;
-      flex-direction: row;
-      height: 100%;
-      overflow: hidden;
-
-      .grw-contextual-navigation {
-        position: relative;
-        width: 240px;
-        height: 100%;
-        &:not(.dragging) {
-          transition: width 200ms cubic-bezier(0.2, 0, 0, 1) 0s;
-        }
-        will-change: width;
-
-        .grw-contextual-navigation-child {
-          height: 100%;
-          overflow-x: hidden;
-        }
-
-        .grw-drawer-toggler {
-          display: none; // invisible in default
-        }
-
-      }
+}
 
 
-      .simplebar-mask {
-        z-index: 110; // greater than the value of .grw-navigation-draggable to fix https://redmine.weseek.co.jp/issues/86678
-      }
-    }
-    .grw-navigation-draggable {
-      position: absolute;
-      top: 0px;
-      bottom: 0px;
-      left: 100%;
-      z-index: 10; // greater than the value of SimpleBar
-      width: 0;
-      .grw-navigation-draggable-hitarea {
-        position: relative;
-        left: -4px;
-        width: 24px;
-        height: 100%;
-        cursor: ew-resize;
-        .grw-navigation-draggable-hitarea-child {
-          position: absolute;
-          left: 3px;
-          display: none;
-          width: 2px;
-          height: 100%;
-          background-color: rgb(76, 154, 255);
-        }
-      }
-      .grw-navigation-resize-button {
-        position: fixed;
-
-        $width: 27.691px;
-        $height: 23.999px;
-
-        @mixin hitarea($size-hitarea) {
-          top: ($width - $size-hitarea) / 2;
-          left: ($height - $size-hitarea) / 2;
-          width: $size-hitarea;
-          height: $size-hitarea;
-        }
-
-        // locate to the center of screen
-        top: calc(50vh - $height/2);
-
-        display: none;
-        padding: 0px;
-        background-color: transparent;
-        border: 0;
-        transform: translateX(-50%);
-
-        .hexagon-container {
-          // set transform
-          svg * {
-            transition: fill 100ms linear;
-          }
-          svg {
-            width: $width + 2px; // add 1px for drop-shadow
-            height: $height + 2px; // add 1px for drop-shadow
-            .background {
-              filter: drop-shadow(0px 1px 0px rgba(#999, 60%));
-            }
-          }
-        }
-        .hitarea {
-          position: absolute;
-          border-radius: bs.$border-radius-pill;
-
-          @include hitarea(30px);
-        }
-
-        // reverse and center icon at the time of collapsed
-        &.collapsed {
-          display: block;
-          .hexagon-container svg {
-            transform: rotate(180deg);
-          }
-          .hitarea {
-            @include hitarea(80px);
-          }
-        }
-      }
-      &:hover {
-        .grw-navigation-draggable-hitarea-child {
-          display: block;
-        }
-        .grw-navigation-resize-button {
-          display: block;
-        }
-      }
-    }
-  }
 
 
+// TODO: commonize reload button style
+.grw-sidebar :global {
   .grw-sidebar-content-header {
   .grw-sidebar-content-header {
     .grw-btn-reload {
     .grw-btn-reload {
       font-size: 18px;
       font-size: 18px;
@@ -141,91 +22,66 @@
   }
   }
 }
 }
 
 
-
 // Dock Mode
 // Dock Mode
-@mixin dock() {
-}
-
-// Drawer Mode
-@mixin drawer() {
-  z-index: bs.$zindex-fixed + 2;
-
-  .data-layout-container {
-    position: fixed;
-    top: 0;
-    width: 0;
-  }
-  div.navigation.transition-enabled {
-    max-width: 80vw;
-
-    // apply transition
-    transition-property: transform;
-    @include mixins.apply-navigation-transition();
-  }
-
-  &:not(.open) {
-    div.navigation {
-      transform: translateX(-100%);
+.grw-sidebar {
+  &:global {
+    &.grw-sidebar-dock {
+      position: sticky;
     }
     }
   }
   }
-  &.open {
-    div.navigation {
-      transform: translateX(0);
-    }
+}
 
 
-    .grw-contextual-navigation-child {
-      .grw-drawer-toggler {
-        display: block;
-      }
-    }
-  }
+// Collapsed Mode
+.grw-sidebar {
+  &:global {
+    &.grw-sidebar-collapsed {
+      position: sticky;
 
 
-  .grw-navigation-resize-button {
-    display: none !important;
-  }
+      .sidebar-contents-container {
+        border-color: var(--bs-border-color);
+        border-style: solid;
+        border-width : 1px 1px 1px 0;
+      }
 
 
-  .grw-contextual-navigation-child {
-    .grw-drawer-toggler {
-      @include bs.media-breakpoint-down(sm) {
-        position: fixed;
-        right: -15px;
-        bottom: 15px;
-        width: 42px;
-        height: 42px;
-        font-size: 18px;
-        transform: translateX(100%);
+      // open
+      .sidebar-contents-container.open {
+        position: absolute;
+        left: var.$grw-sidebar-nav-width;
+        min-height: 50vh;
+        max-height: calc(100vh - var.$grw-sidebar-nav-width * 2);
       }
       }
     }
     }
   }
   }
 }
 }
 
 
+// Drawer Mode
 .grw-sidebar {
 .grw-sidebar {
   &:global {
   &:global {
     &.grw-sidebar-drawer {
     &.grw-sidebar-drawer {
-      @include drawer();
-    }
-    &.grw-sidebar-dock {
-      @include bs.media-breakpoint-down(sm) {
-        @include drawer();
+      position: fixed;
+      z-index: bs.$zindex-fixed + 2;
+      width: 348px;
+
+      // apply transition
+      transition-property: transform;
+      @include mixins.apply-navigation-transition();
+
+      &:not(.open) {
+        transform: translateX(-100%);
       }
       }
-      @include bs.media-breakpoint-up(md) {
-        @include dock();
+      &.open {
+        transform: translateX(0);
       }
       }
     }
     }
   }
   }
 }
 }
 
 
 
 
-.grw-sidebar :global {
-  .grw-contextual-navigation {
-    backdrop-filter: blur(20px);
-  }
-}
 @include bs.color-mode(light) {
 @include bs.color-mode(light) {
   .grw-sidebar :global {
   .grw-sidebar :global {
     --bs-border-color: var(--grw-highlight-200);
     --bs-border-color: var(--grw-highlight-200);
 
 
-    .grw-contextual-navigation {
+    .sidebar-contents-container {
       background-color: rgba(var(--grw-highlight-100-rgb), .5);
       background-color: rgba(var(--grw-highlight-100-rgb), .5);
     }
     }
   }
   }
@@ -236,7 +92,7 @@
     --bs-color: var(--bs-gray-400);
     --bs-color: var(--bs-gray-400);
     --bs-border-color: var(--grw-highlight-800);
     --bs-border-color: var(--grw-highlight-800);
 
 
-    .grw-contextual-navigation {
+    .sidebar-contents-container {
       background-color: rgba(var(--grw-highlight-800-rgb), .5);
       background-color: rgba(var(--grw-highlight-800-rgb), .5);
     }
     }
   }
   }

+ 136 - 286
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -1,24 +1,24 @@
 import React, {
 import React, {
-  memo, useCallback, useEffect, useRef, useState,
+  type FC,
+  memo, useCallback, useEffect, useState,
 } from 'react';
 } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import { useUserUISettings } from '~/client/services/user-ui-settings';
+import { scheduleToPut } from '~/client/services/user-ui-settings';
+import { SidebarMode } from '~/interfaces/ui';
 import {
 import {
-  useDrawerMode, useDrawerOpened,
-  useSidebarCollapsed,
-  useCurrentSidebarContents,
+  useDrawerOpened,
+  useCollapsedContentsOpened,
   useCurrentProductNavWidth,
   useCurrentProductNavWidth,
-  useSidebarResizeDisabled,
-  useSidebarScrollerRef,
+  usePreferCollapsedMode,
+  useSidebarMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
-import DrawerToggler from '../Navbar/DrawerToggler';
-import { StickyStretchableScrollerProps } from '../StickyStretchableScroller';
-
-import { NavigationResizeHexagon } from './NavigationResizeHexagon';
-import { SidebarNav } from './SidebarNav';
+import { AppTitleOnSidebarHead, AppTitleOnSubnavigation } from './AppTitle/AppTitle';
+import { ResizableArea } from './ResizableArea/ResizableArea';
+import { SidebarHead } from './SidebarHead';
+import { SidebarNav, type SidebarNavProps } from './SidebarNav';
 
 
 import styles from './Sidebar.module.scss';
 import styles from './Sidebar.module.scss';
 
 
@@ -26,328 +26,178 @@ import styles from './Sidebar.module.scss';
 const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
 const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
 
 
 
 
-const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('../StickyStretchableScroller')
-  .then(mod => mod.StickyStretchableScroller), { ssr: false });
-
-const sidebarMinWidth = 240;
-const sidebarMinimizeWidth = 20;
-const sidebarFixedWidthInDrawerMode = 320;
-
-const GlobalNavigation = memo(() => {
-  const { data: isDrawerMode } = useDrawerMode();
-  const { data: currentContents } = useCurrentSidebarContents();
-  const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
-
-  const { scheduleToPut } = useUserUISettings();
-
-  const itemSelectedHandler = useCallback((selectedContents) => {
-    if (isDrawerMode) {
-      return;
-    }
-
-    let newValue = false;
-
-    // already selected
-    if (currentContents === selectedContents) {
-      // toggle collapsed
-      newValue = !isCollapsed;
-    }
-
-    mutateSidebarCollapsed(newValue, false);
-    scheduleToPut({ isSidebarCollapsed: newValue });
-
-  }, [currentContents, isCollapsed, isDrawerMode, mutateSidebarCollapsed, scheduleToPut]);
-
-  return <SidebarNav onItemSelected={itemSelectedHandler} />;
-
-});
-GlobalNavigation.displayName = 'GlobalNavigation';
-
-const SidebarContentsWrapper = memo(() => {
-  const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
+const resizableAreaMinWidth = 348;
+const sidebarNavCollapsedWidth = 48;
 
 
-  const calcViewHeight = useCallback(() => {
-    const elem = document.querySelector('#grw-sidebar-contents-wrapper');
-    return elem != null
-      ? window.innerHeight - elem?.getBoundingClientRect().top
-      : window.innerHeight;
-  }, []);
 
 
-  return (
-    <>
-      <div id="grw-sidebar-contents-wrapper" style={{ minHeight: '100%' }}>
-        <StickyStretchableScroller
-          simplebarRef={mutateSidebarScroller}
-          stickyElemSelector=".grw-sidebar"
-          calcViewHeight={calcViewHeight}
-        >
-          <SidebarContents />
-        </StickyStretchableScroller>
-      </div>
-
-      <DrawerToggler iconClass="icon-arrow-left" />
-    </>
-  );
-});
-SidebarContentsWrapper.displayName = 'SidebarContentsWrapper';
+type ResizableContainerProps = {
+  children?: React.ReactNode,
+}
 
 
+const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element => {
 
 
-export const Sidebar = memo((): JSX.Element => {
+  const { children } = props;
 
 
-  const { data: isDrawerMode } = useDrawerMode();
-  const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
+  const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
+  const { mutate: mutateDrawerOpened } = useDrawerOpened();
   const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
   const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
-  const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
-  const { data: isResizeDisabled, mutate: mutateSidebarResizeDisabled } = useSidebarResizeDisabled();
+  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
 
-  const { scheduleToPut } = useUserUISettings();
+  const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
 
 
-  const [isHover, setHover] = useState(false);
-  const [isHoverOnResizableContainer, setHoverOnResizableContainer] = useState(false);
-  const [isDragging, setDrag] = useState(false);
+  const resizeHandler = useCallback((newWidth: number) => {
+    setResizableAreaWidth(newWidth);
+  }, []);
 
 
-  const resizableContainer = useRef<HTMLDivElement>(null);
+  const resizeDoneHandler = useCallback((newWidth: number) => {
+    mutateProductNavWidth(newWidth, false);
+    scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
+  }, [mutateProductNavWidth]);
 
 
-  const timeoutIdRef = useRef<NodeJS.Timeout>();
+  const collapsedByResizableAreaHandler = useCallback(() => {
+    mutatePreferCollapsedMode(true);
+    mutateCollapsedContentsOpened(false);
+    scheduleToPut({ preferCollapsedModeByUser: true });
+  }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
 
-  const isResizableByDrag = !isResizeDisabled && !isDrawerMode && (!isCollapsed || isHover);
 
 
-  const toggleDrawerMode = useCallback((bool) => {
-    const isStateModified = isResizeDisabled !== bool;
-    if (!isStateModified) {
-      return;
+  // open/close resizable container when drawer mode
+  useEffect(() => {
+    if (isDrawerMode()) {
+      setResizableAreaWidth(undefined);
     }
     }
-
-    // Drawer <-- Dock
-    if (bool) {
-      // disable resize
-      mutateSidebarResizeDisabled(true, false);
+    else if (isCollapsedMode()) {
+      setResizableAreaWidth(sidebarNavCollapsedWidth);
     }
     }
-    // Drawer --> Dock
     else {
     else {
-      // enable resize
-      mutateSidebarResizeDisabled(false, false);
-    }
-  }, [isResizeDisabled, mutateSidebarResizeDisabled]);
-
-  const setContentWidth = useCallback((newWidth: number) => {
-    if (resizableContainer.current == null) {
-      return;
-    }
-    resizableContainer.current.style.width = `${newWidth}px`;
-  }, []);
-
-  const hoverOnHandler = useCallback(() => {
-    if (!isCollapsed || isDrawerMode || isDragging) {
-      return;
-    }
-
-    setHover(true);
-  }, [isCollapsed, isDragging, isDrawerMode]);
-
-  const hoverOutHandler = useCallback(() => {
-    if (!isCollapsed || isDrawerMode || isDragging) {
-      return;
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      setResizableAreaWidth(currentProductNavWidth!);
     }
     }
 
 
-    setHover(false);
-  }, [isCollapsed, isDragging, isDrawerMode]);
+    mutateDrawerOpened(false);
+  }, [currentProductNavWidth, isCollapsedMode, isDrawerMode, mutateDrawerOpened]);
 
 
-  const hoverOnResizableContainerHandler = useCallback(() => {
-    if (!isCollapsed || isDrawerMode || isDragging) {
-      return;
-    }
+  return (
+    <ResizableArea
+      className="flex-expand-vert"
+      width={resizableAreaWidth}
+      minWidth={resizableAreaMinWidth}
+      disabled={!isDockMode()}
+      onResize={resizeHandler}
+      onResizeDone={resizeDoneHandler}
+      onCollapsed={collapsedByResizableAreaHandler}
+    >
+      {children}
+    </ResizableArea>
+  );
 
 
-    setHoverOnResizableContainer(true);
-  }, [isCollapsed, isDrawerMode, isDragging]);
+});
 
 
-  const hoverOutResizableContainerHandler = useCallback(() => {
-    if (!isCollapsed || isDrawerMode || isDragging) {
-      return;
-    }
 
 
-    setHoverOnResizableContainer(false);
-  }, [isCollapsed, isDrawerMode, isDragging]);
+type CollapsibleContainerProps = {
+  Nav: FC<SidebarNavProps>,
+  className?: string,
+  children?: React.ReactNode,
+}
 
 
-  const toggleNavigationBtnClickHandler = useCallback(() => {
-    const newValue = !isCollapsed;
-    mutateSidebarCollapsed(newValue, false);
-    scheduleToPut({ isSidebarCollapsed: newValue });
-  }, [isCollapsed, mutateSidebarCollapsed, scheduleToPut]);
+const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Element => {
 
 
-  useEffect(() => {
-    if (isCollapsed) {
-      setContentWidth(sidebarMinimizeWidth);
-    }
-    else {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      setContentWidth(currentProductNavWidth!);
-    }
-  }, [currentProductNavWidth, isCollapsed, setContentWidth]);
+  const { Nav, className, children } = props;
 
 
-  const draggableAreaMoveHandler = useCallback((event: MouseEvent) => {
-    event.preventDefault();
+  const { isCollapsedMode } = useSidebarMode();
+  const { data: currentProductNavWidth } = useCurrentProductNavWidth();
+  const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
 
-    const newWidth = event.pageX - 60;
-    if (resizableContainer.current != null) {
-      setContentWidth(newWidth);
-      resizableContainer.current.classList.add('dragging');
-    }
-  }, [setContentWidth]);
 
 
-  const dragableAreaMouseUpHandler = useCallback(() => {
-    if (resizableContainer.current == null) {
+  // open menu when collapsed mode
+  const primaryItemHoverHandler = useCallback(() => {
+    // reject other than collapsed mode
+    if (!isCollapsedMode()) {
       return;
       return;
     }
     }
 
 
-    setDrag(false);
-
-    if (resizableContainer.current.clientWidth < sidebarMinWidth) {
-      // force collapsed
-      mutateSidebarCollapsed(true);
-      mutateProductNavWidth(sidebarMinWidth, false);
-      scheduleToPut({ isSidebarCollapsed: true, currentProductNavWidth: sidebarMinWidth });
-    }
-    else {
-      const newWidth = resizableContainer.current.clientWidth;
-      mutateSidebarCollapsed(false);
-      mutateProductNavWidth(newWidth, false);
-      scheduleToPut({ isSidebarCollapsed: false, currentProductNavWidth: newWidth });
-    }
-
-    resizableContainer.current.classList.remove('dragging');
-
-  }, [mutateProductNavWidth, mutateSidebarCollapsed, scheduleToPut]);
+    mutateCollapsedContentsOpened(true);
+  }, [isCollapsedMode, mutateCollapsedContentsOpened]);
 
 
-  const dragableAreaMouseDownHandler = useCallback((event: React.MouseEvent) => {
-    if (!isResizableByDrag) {
+  // close menu when collapsed mode
+  const mouseLeaveHandler = useCallback(() => {
+    // reject other than collapsed mode
+    if (!isCollapsedMode()) {
       return;
       return;
     }
     }
 
 
-    event.preventDefault();
+    mutateCollapsedContentsOpened(false);
+  }, [isCollapsedMode, mutateCollapsedContentsOpened]);
 
 
-    setDrag(true);
+  const openClass = `${isCollapsedContentsOpened ? 'open' : ''}`;
+  const collapsibleContentsWidth = isCollapsedMode() ? currentProductNavWidth : undefined;
 
 
-    const removeEventListeners = () => {
-      document.removeEventListener('mousemove', draggableAreaMoveHandler);
-      document.removeEventListener('mouseup', dragableAreaMouseUpHandler);
-      document.removeEventListener('mouseup', removeEventListeners);
-    };
+  return (
+    <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
+      <Nav onPrimaryItemHover={primaryItemHoverHandler} />
+      <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto ${openClass}`} style={{ width: collapsibleContentsWidth }}>
+        {children}
+      </div>
+    </div>
+  );
 
 
-    document.addEventListener('mousemove', draggableAreaMoveHandler);
-    document.addEventListener('mouseup', dragableAreaMouseUpHandler);
-    document.addEventListener('mouseup', removeEventListeners);
+});
 
 
-  }, [dragableAreaMouseUpHandler, draggableAreaMoveHandler, isResizableByDrag]);
 
 
-  useEffect(() => {
-    toggleDrawerMode(isDrawerMode);
-  }, [isDrawerMode, toggleDrawerMode]);
+type DrawableContainerProps = {
+  className?: string,
+  children?: React.ReactNode,
+}
 
 
-  // open/close resizable container
-  useEffect(() => {
-    if (!isCollapsed) {
-      return;
-    }
+const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
 
 
-    if (isHoverOnResizableContainer) {
-      // schedule to open
-      timeoutIdRef.current = setTimeout(() => {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        setContentWidth(currentProductNavWidth!);
-      }, 70);
-    }
-    else if (timeoutIdRef.current != null) {
-      // cancel schedule to open
-      clearTimeout(timeoutIdRef.current);
-      timeoutIdRef.current = undefined;
-    }
+  const { className, children } = props;
 
 
-    // close
-    if (!isHover) {
-      setContentWidth(sidebarMinimizeWidth);
-      timeoutIdRef.current = undefined;
-    }
-  }, [isCollapsed, isHover, isHoverOnResizableContainer, currentProductNavWidth, setContentWidth]);
+  const { data: isDrawerOpened } = useDrawerOpened();
 
 
-  // open/close resizable container when drawer mode
-  useEffect(() => {
-    if (isDrawerMode) {
-      setContentWidth(sidebarFixedWidthInDrawerMode);
-    }
-    else if (isCollapsed) {
-      setContentWidth(sidebarMinimizeWidth);
-    }
-    else {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      setContentWidth(currentProductNavWidth!);
-    }
-  }, [currentProductNavWidth, isCollapsed, isDrawerMode, setContentWidth]);
+  const openClass = `${isDrawerOpened ? 'open' : ''}`;
+
+  return (
+    <div className={`${className} ${openClass}`}>
+      {children}
+    </div>
+  );
+});
 
 
 
 
-  const showContents = isDrawerMode || isHover || !isCollapsed;
+export const Sidebar = (): JSX.Element => {
 
 
+  const { data: sidebarMode, isCollapsedMode } = useSidebarMode();
 
 
   // css styles
   // css styles
-  const grwSidebarClass = `grw-sidebar ${styles['grw-sidebar']}`;
-  const sidebarModeClass = `${isDrawerMode ? 'grw-sidebar-drawer' : 'grw-sidebar-dock'}`;
-  const isOpenClass = `${isDrawerOpened ? 'open' : ''}`;
+  const grwSidebarClass = styles['grw-sidebar'];
+  // eslint-disable-next-line no-nested-ternary
+  let modeClass;
+  switch (sidebarMode) {
+    case SidebarMode.DRAWER:
+      modeClass = 'grw-sidebar-drawer';
+      break;
+    case SidebarMode.COLLAPSED:
+      modeClass = 'grw-sidebar-collapsed';
+      break;
+    case SidebarMode.DOCK:
+      modeClass = 'grw-sidebar-dock';
+      break;
+  }
+
   return (
   return (
     <>
     <>
-      <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`} data-testid="grw-sidebar">
-        <div className="data-layout-container">
-          <div
-            className="navigation transition-enabled"
-            onMouseEnter={hoverOnHandler}
-            onMouseLeave={hoverOutHandler}
-          >
-            <div className="grw-navigation-wrap">
-              <div className="grw-global-navigation">
-                <GlobalNavigation></GlobalNavigation>
-              </div>
-              <div
-                ref={resizableContainer}
-                className="grw-contextual-navigation"
-                onMouseEnter={hoverOnResizableContainerHandler}
-                onMouseLeave={hoverOutResizableContainerHandler}
-                style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
-              >
-                <div className={`grw-contextual-navigation-child ${showContents ? '' : 'd-none'}`} data-testid="grw-contextual-navigation-child">
-                  <SidebarContents />
-                  <DrawerToggler iconClass="icon-arrow-left" />
-                </div>
-              </div>
-            </div>
-            <div className="grw-navigation-draggable">
-              { isResizableByDrag && (
-                <div
-                  className="grw-navigation-draggable-hitarea"
-                  onMouseDown={dragableAreaMouseDownHandler}
-                >
-                  <div className="grw-navigation-draggable-hitarea-child"></div>
-                </div>
-              ) }
-              <button
-                data-testid="grw-navigation-resize-button"
-                className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
-                type="button"
-                aria-expanded="true"
-                aria-label="Toggle navigation"
-                disabled={isDrawerMode}
-                onClick={toggleNavigationBtnClickHandler}
-              >
-                <span className="hexagon-container" role="presentation">
-                  <NavigationResizeHexagon />
-                </span>
-                <span className="hitarea" role="presentation"></span>
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-
+      { sidebarMode != null && isCollapsedMode() && <AppTitleOnSubnavigation /> }
+      <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
+        <ResizableContainer>
+          { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }
+          <SidebarHead />
+          <CollapsibleContainer Nav={SidebarNav} className="border-top">
+            <SidebarContents />
+          </CollapsibleContainer>
+        </ResizableContainer>
+      </DrawableContainer>
     </>
     </>
   );
   );
-
-});
-Sidebar.displayName = 'Sidebar';
+};

+ 23 - 20
apps/app/src/components/Sidebar/SidebarContents.tsx

@@ -1,7 +1,7 @@
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
 
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { useCurrentSidebarContents } from '~/stores/ui';
+import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
 
 
 
 
 import { Bookmarks } from './Bookmarks';
 import { Bookmarks } from './Bookmarks';
@@ -14,28 +14,31 @@ import styles from './SidebarContents.module.scss';
 
 
 
 
 export const SidebarContents = memo(() => {
 export const SidebarContents = memo(() => {
+  const { isCollapsedMode } = useSidebarMode();
+  const { data: isCollapsedContentsOpened } = useCollapsedContentsOpened();
+
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
-  let Contents;
-  switch (currentSidebarContents) {
-    case SidebarContentsType.RECENT:
-      Contents = RecentChanges;
-      break;
-    case SidebarContentsType.CUSTOM:
-      Contents = CustomSidebar;
-      break;
-    case SidebarContentsType.TAG:
-      Contents = Tag;
-      break;
-    case SidebarContentsType.BOOKMARKS:
-      Contents = Bookmarks;
-      break;
-    default:
-      Contents = PageTree;
-  }
+  const Contents = useMemo(() => {
+    switch (currentSidebarContents) {
+      case SidebarContentsType.RECENT:
+        return RecentChanges;
+      case SidebarContentsType.CUSTOM:
+        return CustomSidebar;
+      case SidebarContentsType.TAG:
+        return Tag;
+      case SidebarContentsType.BOOKMARKS:
+        return Bookmarks;
+      default:
+        return PageTree;
+    }
+  }, [currentSidebarContents]);
+
+  const isHidden = isCollapsedMode() && !isCollapsedContentsOpened;
+  const classToHide = isHidden ? 'd-none' : '';
 
 
   return (
   return (
-    <div className={`grw-sidebar-contents ${styles['grw-sidebar-contents']}`}>
+    <div className={`grw-sidebar-contents ${styles['grw-sidebar-contents']} ${classToHide}`} data-testid="grw-sidebar-contents">
       <Contents />
       <Contents />
     </div>
     </div>
   );
   );

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

@@ -0,0 +1,26 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .grw-sidebar-head :global {
+    background-color: var(
+      --grw-sidebar-head-bg,
+      var(
+        --grw-sidebar-nav-bg,
+        var(--grw-highlight-100)
+      )
+    );
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-sidebar-head :global {
+    background-color: var(
+      --grw-sidebar-head-bg,
+      var(
+        --grw-sidebar-nav-bg,
+        var(--grw-highlight-800)
+      )
+    );
+  }
+}

+ 17 - 0
apps/app/src/components/Sidebar/SidebarHead/SidebarHead.tsx

@@ -0,0 +1,17 @@
+import React, {
+  type FC, memo,
+} from 'react';
+
+import { ToggleCollapseButton } from './ToggleCollapseButton';
+
+import styles from './SidebarHead.module.scss';
+
+
+export const SidebarHead: FC = memo(() => {
+  return (
+    <div className={`${styles['grw-sidebar-head']} d-flex justify-content-end w-100`}>
+      <ToggleCollapseButton />
+    </div>
+  );
+
+});

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

@@ -0,0 +1,52 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '../button-styles';
+@use '../variables' as var;
+
+.btn-toggle-collapse :global {
+  @extend %btn-primary-basis;
+
+  $height: var.$grw-sidebar-nav-width; // declare $height with the same value as the sidebar nav width
+  height: $height;
+}
+
+// icon
+.btn-toggle-collapse :global {
+  .material-icons {
+    transition: transform 0.25s;
+
+    // rotation
+    &.rotate180 {
+      transform: rotate(180deg);
+    }
+  }
+}
+
+// == Colors
+.btn-toggle-collapse {
+  &:global {
+    &.btn.btn-primary {
+      @extend %btn-primary-color-vars;
+    }
+  }
+}
+@include bs.color-mode(light) {
+  .btn-toggle-collapse {
+    &:global {
+      &.btn.btn-primary {
+        --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--bs-gray-500));
+        --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-300));
+      }
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .btn-toggle-collapse {
+    &:global {
+      &.btn.btn-primary {
+        --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--bs-gray-600));
+        --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
+      }
+    }
+  }
+}

+ 41 - 0
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx

@@ -0,0 +1,41 @@
+import { memo, useCallback } from 'react';
+
+import {
+  useCollapsedContentsOpened, usePreferCollapsedMode, useDrawerOpened, useSidebarMode,
+} from '~/stores/ui';
+
+
+import styles from './ToggleCollapseButton.module.scss';
+
+
+export const ToggleCollapseButton = memo((): JSX.Element => {
+
+  const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
+  const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
+  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
+
+  const toggleDrawer = useCallback(() => {
+    mutateDrawerOpened(!isDrawerOpened);
+  }, [isDrawerOpened, mutateDrawerOpened]);
+
+  const toggleCollapsed = useCallback(() => {
+    mutatePreferCollapsedMode(!isCollapsedMode());
+    mutateCollapsedContentsOpened(false);
+  }, [isCollapsedMode, mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
+
+  const rotationClass = isCollapsedMode() ? 'rotate180' : '';
+  const icon = isDrawerMode() || isDockMode()
+    ? 'first_page'
+    : 'keyboard_double_arrow_left';
+
+  return (
+    <button
+      type="button"
+      className={`btn btn-primary ${styles['btn-toggle-collapse']} p-2`}
+      onClick={isDrawerMode() ? toggleDrawer : toggleCollapsed}
+    >
+      <span className={`material-icons fs-2 ${rotationClass}`}>{icon}</span>
+    </button>
+  );
+});

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

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

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

@@ -1,134 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-@use '~/styles/variables' as var;
-
-.grw-sidebar-nav :global {
-  // set position and z-index to prevent dropdowns covered by other element
-  position: relative;
-  z-index: bs.$zindex-fixed;
-
-  height: 100vh;
-
-  border-right : 1px solid var(--bs-border-color);
-
-  .grw-logo {
-    svg {
-      width: var.$grw-logo-width;
-      height: var.$grw-logo-width;
-      padding: (var.$grw-logo-width - var.$grw-logomark-width) / 2;
-    }
-  }
-
-  .grw-apperance-mode-dropdown,
-  .grw-personal-dropdown {
-    .dropdown-menu {
-      min-width: 15rem;
-
-      .grw-icon-container svg {
-        width: 18px;
-        height: 18px;
-      }
-    }
-  }
-
-  .btn {
-    width: var.$grw-sidebar-nav-width;
-    height: var.$grw-sidebar-nav-height;
-    padding-top: .75rem;
-    padding-bottom: .75rem;
-    line-height: 1em;
-    border: 0;
-    border-radius: 0;
-    box-shadow: none !important;
-
-    // icon opacity
-    &:not(.active) {
-      i {
-        opacity: 0.7;
-      }
-      &:hover,
-      &:focus {
-        i {
-          opacity: 0.8;
-        }
-      }
-    }
-  }
-
-  .grw-sidebar-nav-primary-container {
-    $btn-active-indicator-height: 34px;
-
-    .btn {
-      &.active {
-        position: relative;
-
-        // indicator
-        &:after {
-          position: absolute;
-          top: 0;
-          left: 0;
-          display: block;
-          width: 3px;
-          height: $btn-active-indicator-height;
-          content: '';
-          background-color: var(--bs-primary);
-          transform: translateY(#{(var.$grw-sidebar-nav-height - $btn-active-indicator-height) / 2});
-        }
-      }
-    }
-  }
-
-  .grw-sidebar-nav-secondary-container {
-    position: fixed;
-    bottom: 1.5rem;
-
-    .btn {
-      i {
-        opacity: 0.4;
-      }
-    }
-  }
-}
-
-
-// == Colors
-.grw-sidebar-nav :global {
-  .btn.btn-primary {
-    --bs-btn-bg: transparent;
-    --bs-btn-active-bg: transparent;
-    --bs-btn-hover-color: var(
-      --grw-sidebar-nav-btn-hover-color,
-      var(
-        --grw-sidebar-nav-btn-color,
-        var(--bs-btn-color)
-      ),
-    );
-    --bs-btn-active-color: var(
-      --grw-sidebar-nav-btn-active-color,
-      var(
-        --grw-sidebar-nav-btn-color,
-        var(--bs-btn-color)
-      ),
-    );
-  }
-}
-@include bs.color-mode(light) {
-  .grw-sidebar-nav :global {
-    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-100));
-
-    .btn-primary {
-      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-500));
-      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-300));
-    }
-  }
-}
-@include bs.color-mode(dark) {
-  .grw-sidebar-nav :global {
-    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-800));
-
-    .btn-primary {
-      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-400));
-      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
-    }
-  }
-}

+ 0 - 138
apps/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,138 +0,0 @@
-import React, {
-  FC, memo, useCallback,
-} from 'react';
-
-import dynamic from 'next/dynamic';
-import Link from 'next/link';
-
-import { useUserUISettings } from '~/client/services/user-ui-settings';
-import { SidebarContentsType } from '~/interfaces/ui';
-import {
-  useIsAdmin, useGrowiCloudUri, useIsDefaultLogo, useIsGuestUser,
-} from '~/stores/context';
-import { useCurrentSidebarContents } from '~/stores/ui';
-
-import DrawerToggler from '../Navbar/DrawerToggler';
-
-import { PageCreateButton } from './PageCreateButton';
-import { SidebarBrandLogo } from './SidebarBrandLogo';
-
-import styles from './SidebarNav.module.scss';
-
-
-const PersonalDropdown = dynamic(() => import('./PersonalDropdown').then(mod => mod.PersonalDropdown), { ssr: false });
-const InAppNotificationDropdown = dynamic(() => import('../InAppNotification/InAppNotificationDropdown')
-  .then(mod => mod.InAppNotificationDropdown), { ssr: false });
-const AppearanceModeDropdown = dynamic(() => import('./AppearanceModeDropdown').then(mod => mod.AppearanceModeDropdown), { ssr: false });
-
-
-type PrimaryItemProps = {
-  contents: SidebarContentsType,
-  label: string,
-  iconName: string,
-  onItemSelected: (contents: SidebarContentsType) => void,
-}
-
-const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
-  const {
-    contents, label, iconName, onItemSelected,
-  } = props;
-
-  const { data: currentContents, mutate } = useCurrentSidebarContents();
-  const { scheduleToPut } = useUserUISettings();
-
-  const isSelected = contents === currentContents;
-
-  const itemSelectedHandler = useCallback(() => {
-    if (onItemSelected != null) {
-      onItemSelected(contents);
-    }
-
-    mutate(contents, false);
-
-    scheduleToPut({ currentSidebarContents: contents });
-  }, [contents, mutate, onItemSelected, scheduleToPut]);
-
-  const labelForTestId = label.toLowerCase().replace(' ', '-');
-
-  return (
-    <button
-      type="button"
-      data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
-      className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
-      onClick={itemSelectedHandler}
-    >
-      <i className="material-icons">{iconName}</i>
-    </button>
-  );
-};
-
-type SecondaryItemProps = {
-  label: string,
-  href: string,
-  iconName: string,
-  isBlank?: boolean,
-}
-
-const SecondaryItem: FC<SecondaryItemProps> = memo((props: SecondaryItemProps) => {
-  const { iconName, href, isBlank } = props;
-
-  return (
-    <Link
-      href={href}
-      className="d-block btn btn-primary"
-      target={`${isBlank ? '_blank' : ''}`}
-      prefetch={false}
-    >
-      <i className="material-icons">{iconName}</i>
-    </Link>
-  );
-});
-SecondaryItem.displayName = 'SecondaryItem';
-
-
-type Props = {
-  onItemSelected: (contents: SidebarContentsType) => void,
-}
-
-export const SidebarNav: FC<Props> = (props: Props) => {
-  const { data: isAdmin } = useIsAdmin();
-  const { data: isGuestUser } = useIsGuestUser();
-  const { data: growiCloudUri } = useGrowiCloudUri();
-  const { data: isDefaultLogo } = useIsDefaultLogo();
-
-  const { onItemSelected } = props;
-
-  const isAuthenticated = isGuestUser === false;
-
-  return (
-    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
-      {/* Brand Logo  */}
-      <div className="navbar-brand">
-        <Link href="/" className="grw-logo d-block">
-          <SidebarBrandLogo isDefaultLogo={isDefaultLogo} />
-        </Link>
-      </div>
-
-      <PageCreateButton />
-
-      <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
-        <PrimaryItem contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onItemSelected={onItemSelected} />
-        <PrimaryItem contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onItemSelected={onItemSelected} />
-        <PrimaryItem contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onItemSelected={onItemSelected} />
-        <PrimaryItem contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmark" onItemSelected={onItemSelected} />
-        <PrimaryItem contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onItemSelected={onItemSelected} />
-        <InAppNotificationDropdown />
-      </div>
-      <div className="grw-sidebar-nav-secondary-container">
-        {/* TODO: This setting will be consolidated in "Settings" on My Page, so delete it from here. */}
-        {/* <AppearanceModeDropdown isAuthenticated={isAuthenticated} /> */}
-        <PersonalDropdown />
-        <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
-        {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
-        <SecondaryItem label="Trash" href="/trash" iconName="delete" />
-      </div>
-    </div>
-  );
-
-};

+ 71 - 0
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.module.scss

@@ -0,0 +1,71 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '../button-styles';
+@use '../variables' as var;
+
+@use './variables' as sidebarNavVar;
+
+.grw-primary-items :global {
+  .btn {
+    @extend %btn-primary-basis;
+
+    height: sidebarNavVar.$grw-sidebar-primary-button-height;
+
+    i {
+      opacity: 0.7;
+
+      &:hover,
+      &:focus {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+
+// Add indicator
+.grw-primary-items :global {
+  $btn-height: sidebarNavVar.$grw-sidebar-primary-button-height;
+  $btn-active-indicator-height: 34px;
+
+  .btn {
+    &.active {
+      position: relative;
+
+      // indicator
+      &:after {
+        position: absolute;
+        top: 0;
+        left: 0;
+        display: block;
+        width: 3px;
+        height: $btn-active-indicator-height;
+        content: '';
+        background-color: var(--bs-primary);
+        transform: translateY(#{($btn-height - $btn-active-indicator-height) / 2});
+      }
+    }
+  }
+}
+
+// == Colors
+.grw-primary-items :global {
+  .btn.btn-primary {
+    @extend %btn-primary-color-vars;
+  }
+}
+@include bs.color-mode(light) {
+  .grw-primary-items :global {
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-600));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-300));
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-primary-items :global {
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-300));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
+    }
+  }
+}

+ 112 - 0
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -0,0 +1,112 @@
+import { FC, memo, useCallback } from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { scheduleToPut } from '~/client/services/user-ui-settings';
+import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
+import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
+
+import styles from './PrimaryItems.module.scss';
+
+
+const InAppNotificationDropdown = dynamic(() => import('../../InAppNotification/InAppNotificationDropdown')
+  .then(mod => mod.InAppNotificationDropdown), { ssr: false });
+
+
+/**
+ * @returns String for className to switch the indicator is active or not
+ */
+const useIndicator = (sidebarMode: SidebarMode, isSelected: boolean): string => {
+  const { data: isCollapsedContentsOpened } = useCollapsedContentsOpened();
+
+  if (sidebarMode === SidebarMode.COLLAPSED && !isCollapsedContentsOpened) {
+    return '';
+  }
+
+  return isSelected ? 'active' : '';
+};
+
+
+type PrimaryItemProps = {
+  contents: SidebarContentsType,
+  label: string,
+  iconName: string,
+  sidebarMode: SidebarMode,
+  onHover?: (contents: SidebarContentsType) => void,
+}
+
+const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
+  const {
+    contents, label, iconName, sidebarMode,
+    onHover,
+  } = props;
+
+  const { data: currentContents, mutate: mutateContents } = useCurrentSidebarContents();
+
+  const indicatorClass = useIndicator(sidebarMode, contents === currentContents);
+
+  const selectThisItem = useCallback(() => {
+    mutateContents(contents, false);
+    scheduleToPut({ currentSidebarContents: contents });
+  }, [contents, mutateContents]);
+
+  const itemClickedHandler = useCallback(() => {
+    // do nothing ONLY WHEN the collapse mode
+    if (sidebarMode === SidebarMode.COLLAPSED) {
+      return;
+    }
+
+    selectThisItem();
+  }, [selectThisItem, sidebarMode]);
+
+  const mouseEnteredHandler = useCallback(() => {
+    // ignore other than collapsed mode
+    if (sidebarMode !== SidebarMode.COLLAPSED) {
+      return;
+    }
+
+    selectThisItem();
+    onHover?.(contents);
+  }, [contents, onHover, selectThisItem, sidebarMode]);
+
+
+  const labelForTestId = label.toLowerCase().replace(' ', '-');
+
+  return (
+    <button
+      type="button"
+      data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
+      className={`d-block btn btn-primary ${indicatorClass}`}
+      onClick={itemClickedHandler}
+      onMouseEnter={mouseEnteredHandler}
+    >
+      <i className="material-icons">{iconName}</i>
+    </button>
+  );
+};
+
+
+type Props = {
+  onItemHover?: (contents: SidebarContentsType) => void,
+}
+
+export const PrimaryItems = memo((props: Props) => {
+  const { onItemHover } = props;
+
+  const { data: sidebarMode } = useSidebarMode();
+
+  if (sidebarMode == null) {
+    return <></>;
+  }
+
+  return (
+    <div className={styles['grw-primary-items']}>
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmark" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onHover={onItemHover} />
+      <InAppNotificationDropdown />
+    </div>
+  );
+});

+ 45 - 0
apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.module.scss

@@ -0,0 +1,45 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '../button-styles';
+
+@use './variables' as sidebarNavVar;
+
+.grw-secondary-items :global {
+  .btn {
+    @extend %btn-primary-basis;
+
+    height: sidebarNavVar.$grw-sidebar-primary-button-height;
+
+    i {
+      opacity: 0.6;
+
+      &:hover,
+      &:focus {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+
+// == Colors
+.grw-secondary-items :global {
+  .btn.btn-primary {
+    @extend %btn-primary-color-vars;
+  }
+}
+@include bs.color-mode(light) {
+  .grw-secondary-items :global {
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-600));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-secondary-items :global {
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-500));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
+    }
+  }
+}

+ 49 - 0
apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx

@@ -0,0 +1,49 @@
+import { FC, memo } from 'react';
+
+import dynamic from 'next/dynamic';
+import Link from 'next/link';
+
+import { useGrowiCloudUri, useIsAdmin } from '~/stores/context';
+
+import styles from './SecondaryItems.module.scss';
+
+
+const PersonalDropdown = dynamic(() => import('../PersonalDropdown').then(mod => mod.PersonalDropdown), { ssr: false });
+
+
+type SecondaryItemProps = {
+  label: string,
+  href: string,
+  iconName: string,
+  isBlank?: boolean,
+}
+
+const SecondaryItem: FC<SecondaryItemProps> = (props: SecondaryItemProps) => {
+  const { iconName, href, isBlank } = props;
+
+  return (
+    <Link
+      href={href}
+      className="d-block btn btn-primary"
+      target={`${isBlank ? '_blank' : ''}`}
+      prefetch={false}
+    >
+      <i className="material-icons">{iconName}</i>
+    </Link>
+  );
+};
+
+export const SecondaryItems: FC = memo(() => {
+
+  const { data: isAdmin } = useIsAdmin();
+  const { data: growiCloudUri } = useGrowiCloudUri();
+
+  return (
+    <div className={styles['grw-secondary-items']}>
+      <PersonalDropdown />
+      <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
+      {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
+      <SecondaryItem label="Trash" href="/trash" iconName="delete" />
+    </div>
+  );
+});

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

@@ -0,0 +1,33 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '../variables' as var;
+
+.grw-sidebar-nav :global {
+  // set position and z-index to prevent dropdowns covered by other element
+  position: relative;
+  z-index: bs.$zindex-fixed;
+
+  width: var.$grw-sidebar-nav-width;
+
+  border-right : 1px solid var(--bs-border-color);
+
+  .grw-sidebar-nav-secondary-container {
+    position: fixed;
+    bottom: 1.5rem;
+  }
+}
+
+
+// == Colors
+.grw-sidebar-nav :global {
+}
+@include bs.color-mode(light) {
+  .grw-sidebar-nav :global {
+    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-100));
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-sidebar-nav :global {
+    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-800));
+  }
+}

+ 31 - 0
apps/app/src/components/Sidebar/SidebarNav/SidebarNav.tsx

@@ -0,0 +1,31 @@
+import React, { memo } from 'react';
+
+import { SidebarContentsType } from '~/interfaces/ui';
+
+import { PageCreateButton } from '../PageCreateButton';
+
+import { PrimaryItems } from './PrimaryItems';
+import { SecondaryItems } from './SecondaryItems';
+
+import styles from './SidebarNav.module.scss';
+
+export type SidebarNavProps = {
+  onPrimaryItemHover?: (contents: SidebarContentsType) => void,
+}
+
+export const SidebarNav = memo((props: SidebarNavProps) => {
+  const { onPrimaryItemHover } = props;
+
+  return (
+    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
+      <PageCreateButton />
+
+      <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
+        <PrimaryItems onItemHover={onPrimaryItemHover} />
+      </div>
+      <div className="grw-sidebar-nav-secondary-container">
+        <SecondaryItems />
+      </div>
+    </div>
+  );
+});

+ 1 - 0
apps/app/src/components/Sidebar/SidebarNav/_variables.scss

@@ -0,0 +1 @@
+$grw-sidebar-primary-button-height: 50px;

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

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

+ 29 - 0
apps/app/src/components/Sidebar/_button-styles.scss

@@ -0,0 +1,29 @@
+@use './variables' as var;
+
+%btn-primary-basis {
+  padding-top: .75rem;
+  padding-bottom: .75rem;
+  line-height: 1em;
+  border: 0;
+  border-radius: 0;
+  box-shadow: none !important;
+}
+
+%btn-primary-color-vars {
+  --bs-btn-bg: transparent;
+  --bs-btn-active-bg: transparent;
+  --bs-btn-hover-color: var(
+    --grw-sidebar-nav-btn-hover-color,
+    var(
+      --grw-sidebar-nav-btn-color,
+      var(--bs-btn-color)
+    ),
+  );
+  --bs-btn-active-color: var(
+    --grw-sidebar-nav-btn-active-color,
+    var(
+      --grw-sidebar-nav-btn-color,
+      var(--bs-btn-color)
+    ),
+  );
+}

+ 1 - 0
apps/app/src/components/Sidebar/_variables.scss

@@ -0,0 +1 @@
+$grw-sidebar-nav-width: 48px;

+ 1 - 2
apps/app/src/interfaces/sidebar-config.ts

@@ -1,5 +1,4 @@
 
 
 export interface ISidebarConfig {
 export interface ISidebarConfig {
-  isSidebarDrawerMode: boolean,
-  isSidebarClosedAtDockMode: boolean
+  isSidebarCollapsedMode: boolean,
 }
 }

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

@@ -1,5 +1,13 @@
 import type { Nullable } from '@growi/core';
 import type { Nullable } from '@growi/core';
 
 
+
+export const SidebarMode = {
+  DRAWER: 'drawer',
+  COLLAPSED: 'collapsed',
+  DOCK: 'dock',
+} as const;
+export type SidebarMode = typeof SidebarMode[keyof typeof SidebarMode];
+
 export const SidebarContentsType = {
 export const SidebarContentsType = {
   CUSTOM: 'custom',
   CUSTOM: 'custom',
   RECENT: 'recent',
   RECENT: 'recent',

+ 1 - 3
apps/app/src/interfaces/user-ui-settings.ts

@@ -1,9 +1,7 @@
 import { SidebarContentsType } from './ui';
 import { SidebarContentsType } from './ui';
 
 
 export interface IUserUISettings {
 export interface IUserUISettings {
-  isSidebarCollapsed: boolean,
   currentSidebarContents: SidebarContentsType,
   currentSidebarContents: SidebarContentsType,
   currentProductNavWidth: number,
   currentProductNavWidth: number,
-  preferDrawerModeByUser: boolean,
-  preferDrawerModeOnEditByUser: boolean,
+  preferCollapsedModeByUser: boolean,
 }
 }

+ 1 - 2
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -89,8 +89,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.isEnabledMarp = configManager.getConfig('crowi', 'customize:isEnabledMarp');
   props.isEnabledMarp = configManager.getConfig('crowi', 'customize:isEnabledMarp');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 1 - 2
apps/app/src/pages/_search.page.tsx

@@ -113,8 +113,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
   props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 1 - 2
apps/app/src/pages/me/[[...path]].page.tsx

@@ -158,8 +158,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 1 - 2
apps/app/src/pages/tags.page.tsx

@@ -134,8 +134,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
   };
   };
 
 
 }
 }

+ 1 - 2
apps/app/src/pages/trash.page.tsx

@@ -114,8 +114,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
   };
   };
 
 
 }
 }

+ 3 - 6
apps/app/src/pages/utils/commons.ts

@@ -14,7 +14,7 @@ import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageDocument } from '~/server/models/page';
 import type { PageDocument } from '~/server/models/page';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import {
 import {
-  useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
+  useCurrentProductNavWidth, useCurrentSidebarContents, usePreferCollapsedMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 export type CommonProps = {
 export type CommonProps = {
@@ -100,8 +100,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     forcedColorScheme,
     forcedColorScheme,
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
     sidebarConfig: {
     sidebarConfig: {
-      isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-      isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+      isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     },
     },
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
   };
@@ -166,9 +165,7 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
 
 
 export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
 export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
   // UserUISettings
   // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? sidebarConfig.isSidebarClosedAtDockMode);
+  usePreferCollapsedMode(userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode);
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 };
 };

+ 1 - 2
apps/app/src/server/models/config.ts

@@ -133,8 +133,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isAllReplyShown': false,
   'customize:isAllReplyShown': false,
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isEnabledMarp': false,
   'customize:isEnabledMarp': false,
-  'customize:isSidebarDrawerMode': false,
-  'customize:isSidebarClosedAtDockMode': false,
+  'customize:isSidebarCollapsedMode': false,
 
 
   'notification:owner-page:isEnabled': false,
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
   'notification:group-page:isEnabled': false,

+ 1 - 3
apps/app/src/server/models/user-ui-settings.ts

@@ -17,15 +17,13 @@ export type UserUISettingsModel = Model<UserUISettingsDocument>
 
 
 const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
 const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
   user: { type: Schema.Types.ObjectId, ref: 'User', unique: true },
   user: { type: Schema.Types.ObjectId, ref: 'User', unique: true },
-  isSidebarCollapsed: { type: Boolean, default: false },
   currentSidebarContents: {
   currentSidebarContents: {
     type: String,
     type: String,
     enum: SidebarContentsType,
     enum: SidebarContentsType,
     default: SidebarContentsType.RECENT,
     default: SidebarContentsType.RECENT,
   },
   },
   currentProductNavWidth: { type: Number },
   currentProductNavWidth: { type: Number },
-  preferDrawerModeByUser: { type: Boolean, default: false },
-  preferDrawerModeOnEditByUser: { type: Boolean, default: true },
+  preferCollapsedModeByUser: { type: Boolean, default: false },
 });
 });
 
 
 
 

+ 5 - 9
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -112,8 +112,7 @@ module.exports = (crowi) => {
       body('theme').isString(),
       body('theme').isString(),
     ],
     ],
     sidebar: [
     sidebar: [
-      body('isSidebarDrawerMode').isBoolean(),
-      body('isSidebarClosedAtDockMode').isBoolean(),
+      body('isSidebarCollapsedMode').isBoolean(),
     ],
     ],
     function: [
     function: [
       body('isEnabledTimeline').isBoolean(),
       body('isEnabledTimeline').isBoolean(),
@@ -342,9 +341,8 @@ module.exports = (crowi) => {
   router.get('/sidebar', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/sidebar', loginRequiredStrictly, adminRequired, async(req, res) => {
 
 
     try {
     try {
-      const isSidebarDrawerMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarDrawerMode');
-      const isSidebarClosedAtDockMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode');
-      return res.apiv3({ isSidebarDrawerMode, isSidebarClosedAtDockMode });
+      const isSidebarCollapsedMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode');
+      return res.apiv3({ isSidebarCollapsedMode });
     }
     }
     catch (err) {
     catch (err) {
       const msg = 'Error occurred in getting sidebar';
       const msg = 'Error occurred in getting sidebar';
@@ -355,15 +353,13 @@ module.exports = (crowi) => {
 
 
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
     const requestParams = {
     const requestParams = {
-      'customize:isSidebarDrawerMode': req.body.isSidebarDrawerMode,
-      'customize:isSidebarClosedAtDockMode': req.body.isSidebarClosedAtDockMode,
+      'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
-        isSidebarDrawerMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-        isSidebarClosedAtDockMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+        isSidebarCollapsedMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
       };
       };
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });

+ 2 - 6
apps/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -16,11 +16,9 @@ module.exports = (crowi) => {
 
 
   const validatorForPut = [
   const validatorForPut = [
     body('settings').exists().withMessage('The body param \'settings\' is required'),
     body('settings').exists().withMessage('The body param \'settings\' is required'),
-    body('settings.isSidebarCollapsed').optional().isBoolean(),
     body('settings.currentSidebarContents').optional().isIn(AllSidebarContentsType),
     body('settings.currentSidebarContents').optional().isIn(AllSidebarContentsType),
     body('settings.currentProductNavWidth').optional().isNumeric(),
     body('settings.currentProductNavWidth').optional().isNumeric(),
-    body('settings.preferDrawerModeByUser').optional().isBoolean(),
-    body('settings.preferDrawerModeOnEditByUser').optional().isBoolean(),
+    body('settings.preferCollapsedModeByUser').optional().isBoolean(),
   ];
   ];
 
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -30,11 +28,9 @@ module.exports = (crowi) => {
 
 
     // extract only necessary params
     // extract only necessary params
     const updateData = {
     const updateData = {
-      isSidebarCollapsed: settings.isSidebarCollapsed,
       currentSidebarContents: settings.currentSidebarContents,
       currentSidebarContents: settings.currentSidebarContents,
       currentProductNavWidth: settings.currentProductNavWidth,
       currentProductNavWidth: settings.currentProductNavWidth,
-      preferDrawerModeByUser: settings.preferDrawerModeByUser,
-      preferDrawerModeOnEditByUser: settings.preferDrawerModeOnEditByUser,
+      preferCollapsedModeByUser: settings.preferCollapsedModeByUser,
     };
     };
 
 
     if (user == null) {
     if (user == null) {

+ 52 - 0
apps/app/src/stores/admin/sidebar-config.tsx

@@ -0,0 +1,52 @@
+import type { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
+
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+
+type SidebarConfigOption = {
+  update: () => Promise<void>,
+  isSidebarCollapsedMode: boolean|undefined,
+  setIsSidebarCollapsedMode: (isSidebarCollapsedMode: boolean) => void,
+}
+
+export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
+  const swrResponse = useSWRImmutable<ISidebarConfig>(
+    '/customize-setting/sidebar',
+    endpoint => apiv3Get<ISidebarConfig>(endpoint).then(result => result.data),
+  );
+  return {
+    ...swrResponse,
+    update: async() => {
+      const { data } = swrResponse;
+
+      if (data == null) {
+        return;
+      }
+
+      const { isSidebarCollapsedMode } = data;
+
+      const updateData = {
+        isSidebarCollapsedMode,
+      };
+
+      // invoke API
+      await apiv3Put('/customize-setting/sidebar', updateData);
+    },
+    isSidebarCollapsedMode: swrResponse.data?.isSidebarCollapsedMode,
+    setIsSidebarCollapsedMode: (isSidebarCollapsedMode) => {
+      const { data, mutate } = swrResponse;
+
+      if (data == null) {
+        return;
+      }
+
+      const updateData = {
+        isSidebarCollapsedMode,
+      };
+
+      // update isSidebarCollapsedMode in cache, not revalidate
+      mutate({ ...data, ...updateData }, false);
+    },
+  };
+};

+ 52 - 115
apps/app/src/stores/ui.tsx

@@ -1,7 +1,9 @@
-import { type RefObject, useCallback, useEffect } from 'react';
+import {
+  type RefObject, useCallback, useEffect,
+} from 'react';
 
 
 import { PageGrant, type Nullable } from '@growi/core';
 import { PageGrant, type Nullable } from '@growi/core';
-import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
+import { type SWRResponseWithUtils, useSWRStatic } from '@growi/core/dist/swr';
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
@@ -13,11 +15,8 @@ import {
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import type { IFocusable } from '~/client/interfaces/focusable';
 import type { IFocusable } from '~/client/interfaces/focusable';
-import { useUserUISettings } from '~/client/services/user-ui-settings';
-import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
-import { SidebarContentsType } from '~/interfaces/ui';
+import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import {
 import {
   useIsNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
   useIsNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
@@ -223,155 +222,93 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
   return useStaticSWR(key);
   return useStaticSWR(key);
 };
 };
 
 
-type PreferDrawerModeByUserUtils = {
-  update: (preferDrawerMode: boolean) => void
-}
-
-export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
-  const { scheduleToPut } = useUserUISettings();
-
-  const swrResponse: SWRResponse<boolean, Error> = useStaticSWR('preferDrawerModeByUser', initialData);
-
-  const utils: PreferDrawerModeByUserUtils = {
-    update: (preferDrawerMode: boolean) => {
-      swrResponse.mutate(preferDrawerMode);
-      scheduleToPut({ preferDrawerModeByUser: preferDrawerMode });
-    },
-  };
-
-  return withUtils<PreferDrawerModeByUserUtils>(swrResponse, utils);
-
-};
-
-export const usePreferDrawerModeOnEditByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('preferDrawerModeOnEditByUser', initialData, { fallbackData: true });
-};
-
-export const useSidebarCollapsed = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isSidebarCollapsed', initialData, { fallbackData: false });
-};
 
 
 export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
 export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
-  return useStaticSWR('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+  return useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
 };
 };
 
 
 export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
 export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
-  return useStaticSWR('productNavWidth', initialData, { fallbackData: 320 });
+  return useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
 };
 };
 
 
 export const useDrawerMode = (): SWRResponse<boolean, Error> => {
 export const useDrawerMode = (): SWRResponse<boolean, Error> => {
-  const { data: preferDrawerModeByUser } = usePreferDrawerModeByUser();
-  const { data: preferDrawerModeOnEditByUser } = usePreferDrawerModeOnEditByUser();
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
 
 
-  const condition = editorMode != null && preferDrawerModeByUser != null && preferDrawerModeOnEditByUser != null && isDeviceSmallerThanMd != null;
+  const condition = editorMode != null && isDeviceSmallerThanMd != null;
 
 
   const calcDrawerMode = (
   const calcDrawerMode = (
-      endpoint: string,
+      _keyString: string,
       editorMode: EditorMode,
       editorMode: EditorMode,
-      preferDrawerModeByUser: boolean,
-      preferDrawerModeOnEditByUser: boolean,
       isDeviceSmallerThanMd: boolean,
       isDeviceSmallerThanMd: boolean,
   ): boolean => {
   ): boolean => {
-    // get preference on view or edit
-    const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
-
-    return isDeviceSmallerThanMd ?? preferDrawerMode ?? false;
+    return isDeviceSmallerThanMd
+      ? true
+      : editorMode === EditorMode.Editor;
   };
   };
 
 
-  const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
-  const isEditModeWithPreferDrawerMode = editorMode !== EditorMode.View && preferDrawerModeOnEditByUser;
-  const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
-
   return useSWRImmutable(
   return useSWRImmutable(
-    condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
+    condition ? ['isDrawerMode', editorMode, isDeviceSmallerThanMd] : null,
     // calcDrawerMode,
     // calcDrawerMode,
     key => calcDrawerMode(...key),
     key => calcDrawerMode(...key),
     condition
     condition
       ? {
       ? {
-        fallbackData: isDrawerModeFixed
-          ? true
-          : calcDrawerMode('isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd),
+        fallbackData: calcDrawerMode('isDrawerMode', editorMode, isDeviceSmallerThanMd),
       }
       }
       : undefined,
       : undefined,
   );
   );
 };
 };
 
 
-type SidebarConfigOption = {
-  update: () => Promise<void>,
-  isSidebarDrawerMode: boolean|undefined,
-  isSidebarClosedAtDockMode: boolean|undefined,
-  setIsSidebarDrawerMode: (isSidebarDrawerMode: boolean) => void,
-  setIsSidebarClosedAtDockMode: (isSidebarClosedAtDockMode: boolean) => void
-}
-
-export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
-  const swrResponse = useSWRImmutable(
-    '/customize-setting/sidebar',
-    endpoint => apiv3Get(endpoint).then(result => result.data),
-  );
-  return {
-    ...swrResponse,
-    update: async() => {
-      const { data } = swrResponse;
-
-      if (data == null) {
-        return;
-      }
-
-      const { isSidebarDrawerMode, isSidebarClosedAtDockMode } = data;
+export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+};
 
 
-      const updateData = {
-        isSidebarDrawerMode,
-        isSidebarClosedAtDockMode,
-      };
+export const usePreferCollapsedMode = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+};
 
 
-      // invoke API
-      await apiv3Put('/customize-setting/sidebar', updateData);
-    },
-    isSidebarDrawerMode: swrResponse.data?.isSidebarDrawerMode,
-    isSidebarClosedAtDockMode: swrResponse.data?.isSidebarClosedAtDockMode,
-    setIsSidebarDrawerMode: (isSidebarDrawerMode) => {
-      const { data, mutate } = swrResponse;
+export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isCollapsedContentsOpened', initialData, { fallbackData: false });
+};
 
 
-      if (data == null) {
-        return;
-      }
+type DetectSidebarModeUtils = {
+  isDrawerMode(): boolean
+  isCollapsedMode(): boolean
+  isDockMode(): boolean
+}
 
 
-      const updateData = {
-        isSidebarDrawerMode,
-      };
+export const useSidebarMode = (): SWRResponseWithUtils<DetectSidebarModeUtils, SidebarMode> => {
+  const { data: isDrawerMode } = useDrawerMode();
+  const { data: isCollapsedModeUnderDockMode } = usePreferCollapsedMode();
 
 
-      // update isSidebarDrawerMode in cache, not revalidate
-      mutate({ ...data, ...updateData }, false);
+  const condition = isDrawerMode != null && isCollapsedModeUnderDockMode != null;
 
 
-    },
-    setIsSidebarClosedAtDockMode: (isSidebarClosedAtDockMode) => {
-      const { data, mutate } = swrResponse;
+  const fetcher = useCallback(([, isDrawerMode, isCollapsedModeUnderDockMode]: [Key, boolean|undefined, boolean|undefined]) => {
+    if (isDrawerMode) {
+      return SidebarMode.DRAWER;
+    }
+    return isCollapsedModeUnderDockMode ? SidebarMode.COLLAPSED : SidebarMode.DOCK;
+  }, []);
 
 
-      if (data == null) {
-        return;
-      }
+  const swrResponse = useSWRImmutable(
+    condition ? ['sidebarMode', isDrawerMode, isCollapsedModeUnderDockMode] : null,
+    // calcDrawerMode,
+    fetcher,
+    { fallbackData: fetcher(['sidebarMode', isDrawerMode, isCollapsedModeUnderDockMode]) },
+  );
 
 
-      const updateData = {
-        isSidebarClosedAtDockMode,
-      };
+  const _isDrawerMode = useCallback(() => swrResponse.data === SidebarMode.DRAWER, [swrResponse.data]);
+  const _isCollapsedMode = useCallback(() => swrResponse.data === SidebarMode.COLLAPSED, [swrResponse.data]);
+  const _isDockMode = useCallback(() => swrResponse.data === SidebarMode.DOCK, [swrResponse.data]);
 
 
-      // update isSidebarClosedAtDockMode in cache, not revalidate
-      mutate({ ...data, ...updateData }, false);
-    },
+  return {
+    ...swrResponse,
+    isDrawerMode: _isDrawerMode,
+    isCollapsedMode: _isCollapsedMode,
+    isDockMode: _isDockMode,
   };
   };
 };
 };
 
 
-export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isDrawerOpened', isOpened, { fallbackData: false });
-};
-
-export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false });
-};
-
 export const useSelectedGrant = (initialData?: Nullable<IPageGrantData>): SWRResponse<Nullable<IPageGrantData>, Error> => {
 export const useSelectedGrant = (initialData?: Nullable<IPageGrantData>): SWRResponse<Nullable<IPageGrantData>, Error> => {
   return useStaticSWR<Nullable<IPageGrantData>, Error>('selectedGrant', initialData, { fallbackData: { grant: PageGrant.GRANT_PUBLIC } });
   return useStaticSWR<Nullable<IPageGrantData>, Error>('selectedGrant', initialData, { fallbackData: { grant: PageGrant.GRANT_PUBLIC } });
 };
 };

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

@@ -9,10 +9,4 @@ $grw-marker-green: #6f6;
 $grw-navbar-bottom-height: 48px;
 $grw-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 
 
-$grw-sidebar-nav-width: 48px;
-$grw-sidebar-nav-height: 50px;
-
-$grw-logo-width: $grw-sidebar-nav-width;
-$grw-logomark-width: 24px;
-
 $grw-scroll-margin-top-in-view: 130px;
 $grw-scroll-margin-top-in-view: 130px;

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

@@ -209,7 +209,7 @@ context('Access to Template Editing Mode', () => {
 
 
     // Open sidebar
     // Open sidebar
     cy.collapseSidebar(false);
     cy.collapseSidebar(false);
-    cy.getByTestid('grw-contextual-navigation-child').should('be.visible');
+    cy.getByTestid('grw-sidebar-contents').should('be.visible');
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
 
 
     // If PageTree is not active when the sidebar is opened, make it active
     // If PageTree is not active when the sidebar is opened, make it active
@@ -221,7 +221,7 @@ context('Access to Template Editing Mode', () => {
       });
       });
 
 
     // Create page (/{parentPath}}/{newPagePath}) from PageTree
     // Create page (/{parentPath}}/{newPagePath}) from PageTree
-    cy.getByTestid('grw-contextual-navigation-child').within(() => {
+    cy.getByTestid('grw-sidebar-contents').within(() => {
       cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
       cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
         cy.get('#page-create-button-in-page-tree').first().click({force: true})
         cy.get('#page-create-button-in-page-tree').first().click({force: true})
       });
       });

+ 14 - 22
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -27,7 +27,7 @@ describe('Access to sidebar', () => {
 
 
       describe('Test show/collapse button', () => {
       describe('Test show/collapse button', () => {
         it('Successfully show sidebar', () => {
         it('Successfully show sidebar', () => {
-          cy.getByTestid('grw-contextual-navigation-child').should('be.visible');
+          cy.getByTestid('grw-sidebar-contents').should('be.visible');
 
 
           cy.waitUntilSkeletonDisappear();
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
           cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
@@ -36,10 +36,11 @@ describe('Access to sidebar', () => {
           });
           });
         });
         });
 
 
+        // TODO: rewrite test case with grw-switch-collapse-button
         it('Successfully collapse sidebar', () => {
         it('Successfully collapse sidebar', () => {
-          cy.getByTestid('grw-navigation-resize-button').click({force: true});
+          cy.getByTestid('grw-switch-collapse-button').click({force: true});
 
 
-          cy.getByTestid('grw-contextual-navigation-child').should('not.be.visible');
+          cy.getByTestid('grw-sidebar-contents').should('not.be.visible');
 
 
           cy.waitUntilSkeletonDisappear();
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
           cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
@@ -61,7 +62,7 @@ describe('Access to sidebar', () => {
         });
         });
 
 
         it('Successfully access to page tree', () => {
         it('Successfully access to page tree', () => {
-          cy.getByTestid('grw-contextual-navigation-child').within(() => {
+          cy.getByTestid('grw-sidebar-contents').within(() => {
             cy.getByTestid('grw-pagetree-item-container').should('be.visible');
             cy.getByTestid('grw-pagetree-item-container').should('be.visible');
 
 
             cy.waitUntilSkeletonDisappear();
             cy.waitUntilSkeletonDisappear();
@@ -70,7 +71,7 @@ describe('Access to sidebar', () => {
         });
         });
 
 
         it('Successfully hide page tree items', () => {
         it('Successfully hide page tree items', () => {
-          cy.getByTestid('grw-contextual-navigation-child').within(() => {
+          cy.getByTestid('grw-sidebar-contents').within(() => {
             cy.get('.grw-pagetree-open').should('be.visible');
             cy.get('.grw-pagetree-open').should('be.visible');
 
 
             // hide page tree tiems
             // hide page tree tiems
@@ -83,7 +84,7 @@ describe('Access to sidebar', () => {
         it('Successfully click Add to Bookmarks button', () => {
         it('Successfully click Add to Bookmarks button', () => {
           cy.waitUntil(() => {
           cy.waitUntil(() => {
             // do
             // do
-            cy.getByTestid('grw-contextual-navigation-child').within(() => {
+            cy.getByTestid('grw-sidebar-contents').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
               });
               });
@@ -101,7 +102,7 @@ describe('Access to sidebar', () => {
           // show dropdown again
           // show dropdown again
           cy.waitUntil(() => {
           cy.waitUntil(() => {
             // do
             // do
-            cy.getByTestid('grw-contextual-navigation-child').within(() => {
+            cy.getByTestid('grw-sidebar-contents').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
               });
               });
@@ -116,7 +117,7 @@ describe('Access to sidebar', () => {
         it('Successfully show duplicate page modal', () => {
         it('Successfully show duplicate page modal', () => {
           cy.waitUntil(() => {
           cy.waitUntil(() => {
             // do
             // do
-            cy.getByTestid('grw-contextual-navigation-child').within(() => {
+            cy.getByTestid('grw-sidebar-contents').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
               });
               });
@@ -141,7 +142,7 @@ describe('Access to sidebar', () => {
         it('Successfully rename page', () => {
         it('Successfully rename page', () => {
           cy.waitUntil(() => {
           cy.waitUntil(() => {
             // do
             // do
-            cy.getByTestid('grw-contextual-navigation-child').within(() => {
+            cy.getByTestid('grw-sidebar-contents').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
               });
               });
@@ -164,7 +165,7 @@ describe('Access to sidebar', () => {
         it('Successfully show delete page modal', () => {
         it('Successfully show delete page modal', () => {
           cy.waitUntil(() => {
           cy.waitUntil(() => {
             // do
             // do
-            cy.getByTestid('grw-contextual-navigation-child').within(() => {
+            cy.getByTestid('grw-sidebar-contents').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
               cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
                 cy.getByTestid('open-page-item-control-btn').find('button').first().invoke('css','display','block').click()
               });
               });
@@ -196,7 +197,7 @@ describe('Access to sidebar', () => {
         });
         });
 
 
         it('Successfully access to custom sidebar', () => {
         it('Successfully access to custom sidebar', () => {
-          cy.getByTestid('grw-contextual-navigation-child').within(() => {
+          cy.getByTestid('grw-sidebar-contents').within(() => {
             cy.get('.grw-sidebar-content-header > h3').find('a');
             cy.get('.grw-sidebar-content-header > h3').find('a');
 
 
             cy.waitUntilSkeletonDisappear();
             cy.waitUntilSkeletonDisappear();
@@ -243,18 +244,9 @@ describe('Access to sidebar', () => {
           cy.get('.list-group-item').should('be.visible');
           cy.get('.list-group-item').should('be.visible');
 
 
           // The scope of the screenshot is not narrowed because the blackout is shifted
           // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-1-access-to-recent-changes`, { blackout: blackoutOverride });
+          cy.screenshot(`${ssPrefix}recent-changes-access-to-recent-changes`, { blackout: blackoutOverride });
         });
         });
 
 
-        it('Successfully switch content size', () => {
-          cy.get('#grw-sidebar-contents-wrapper').within(() => {
-            cy.get('#recentChangesResize').click({force: true});
-            cy.get('.list-group-item').should('be.visible');
-          });
-
-          // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-2-switch-content-size`, { blackout: blackoutOverride });
-        });
       });
       });
 
 
       describe('Test tags tab', () => {
       describe('Test tags tab', () => {
@@ -269,7 +261,7 @@ describe('Access to sidebar', () => {
         });
         });
 
 
         it('Successfully access to tags', () => {
         it('Successfully access to tags', () => {
-          cy.getByTestid('grw-contextual-navigation-child').within(() => {
+          cy.getByTestid('grw-sidebar-contents').within(() => {
             cy.getByTestid('grw-tags-list').should('be.visible');
             cy.getByTestid('grw-tags-list').should('be.visible');
 
 
             cy.screenshot(`${ssPrefix}tags-1-access-to-tags`, { blackout: blackoutOverride });
             cy.screenshot(`${ssPrefix}tags-1-access-to-tags`, { blackout: blackoutOverride });

+ 4 - 4
apps/app/test/cypress/support/commands.ts

@@ -81,14 +81,14 @@ Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving =
 
 
   cy.getByTestid('grw-sidebar').should('be.visible').within(() => {
   cy.getByTestid('grw-sidebar').should('be.visible').within(() => {
 
 
-    const isSidebarContextualNavigationHidden = isHiddenByTestId('grw-contextual-navigation-child');
-    if (isSidebarContextualNavigationHidden === isCollapsed) {
+    const isSidebarContentsHidden = isHiddenByTestId('grw-sidebar-contents');
+    if (isSidebarContentsHidden === isCollapsed) {
       return;
       return;
     }
     }
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.getByTestid("grw-navigation-resize-button").click({force: true});
+      cy.getByTestid("grw-switch-collapse-button").click({force: true});
       // wait until saving UserUISettings
       // wait until saving UserUISettings
       if (waitUntilSaving) {
       if (waitUntilSaving) {
         // eslint-disable-next-line cypress/no-unnecessary-waiting
         // eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -96,7 +96,7 @@ Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving =
       }
       }
 
 
       // wait until
       // wait until
-      return cy.getByTestid('grw-contextual-navigation-child').then($contents => isHidden($contents) === isCollapsed);
+      return cy.getByTestid('grw-sidebar-contents').then($contents => isHidden($contents) === isCollapsed);
     });
     });
   });
   });