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

Merge branch 'dev/7.0.x' into imprv/131554-131771-file-drop-overlay

reiji-h 2 лет назад
Родитель
Сommit
fbaed464d1
100 измененных файлов с 1230 добавлено и 548 удалено
  1. 18 1
      CHANGELOG.md
  2. 1 1
      apps/app/docker/README.md
  3. 2 0
      apps/app/package.json
  4. 2 3
      apps/app/src/client/services/user-ui-settings.ts
  5. 1 1
      apps/app/src/client/util/apiv1-client.ts
  6. 1 1
      apps/app/src/client/util/apiv3-client.ts
  7. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.module.scss
  8. 1 1
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx
  9. 18 0
      apps/app/src/components/Common/DrawerToggler/DrawerToggler.module.scss
  10. 36 0
      apps/app/src/components/Common/DrawerToggler/DrawerToggler.tsx
  11. 1 0
      apps/app/src/components/Common/DrawerToggler/index.ts
  12. 1 0
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  13. 37 0
      apps/app/src/components/Common/PageViewLayout.module.scss
  14. 3 3
      apps/app/src/components/Common/PageViewLayout.tsx
  15. 1 6
      apps/app/src/components/IdenticalPathPage.module.scss
  16. 0 70
      apps/app/src/components/InAppNotification/InAppNotificationElm.tsx
  17. 45 0
      apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx
  18. 23 28
      apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  19. 18 22
      apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx
  20. 99 0
      apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts
  21. 1 1
      apps/app/src/components/Layout/BasicLayout.tsx
  22. 0 3
      apps/app/src/components/Layout/PageViewLayout.module.scss
  23. 0 31
      apps/app/src/components/Navbar/DrawerToggler.tsx
  24. 18 19
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  25. 21 5
      apps/app/src/components/Navbar/GrowiNavbarBottom.module.scss
  26. 29 17
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  27. 11 10
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  28. 4 4
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  29. 1 1
      apps/app/src/components/Page/PageView.tsx
  30. 6 0
      apps/app/src/components/PageComment/CommentEditor.tsx
  31. 3 1
      apps/app/src/components/PageControls/BookmarkButtons.module.scss
  32. 3 1
      apps/app/src/components/PageControls/LikeButtons.module.scss
  33. 3 1
      apps/app/src/components/PageControls/PageControls.module.scss
  34. 3 1
      apps/app/src/components/PageControls/SeenUserInfo.module.scss
  35. 3 1
      apps/app/src/components/PageControls/SubscribeButton.module.scss
  36. 0 17
      apps/app/src/components/PageControls/_button-styles.scss
  37. 7 18
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  38. 8 12
      apps/app/src/components/PageEditor/PageEditor.tsx
  39. 1 1
      apps/app/src/components/PageList/PageList.module.scss
  40. 7 8
      apps/app/src/components/PageList/PageListItemL.tsx
  41. 1 2
      apps/app/src/components/PageList/PageListItemS.tsx
  42. 2 2
      apps/app/src/components/PageRenameModal.tsx
  43. 30 0
      apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss
  44. 42 0
      apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx
  45. 1 4
      apps/app/src/components/PageSideContents/PageSideContents.module.scss
  46. 27 41
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  47. 3 1
      apps/app/src/components/PageTags/PageTags.tsx
  48. 1 1
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  49. 1 1
      apps/app/src/components/SavePageControls.tsx
  50. 1 1
      apps/app/src/components/SearchPage/SearchPageBase.module.scss
  51. 1 2
      apps/app/src/components/SearchTypeahead.tsx
  52. 1 1
      apps/app/src/components/ShareLinkPageView.tsx
  53. 20 3
      apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss
  54. 25 6
      apps/app/src/components/Sidebar/Sidebar.module.scss
  55. 23 11
      apps/app/src/components/Sidebar/Sidebar.tsx
  56. 10 2
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss
  57. 9 6
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx
  58. 1 3
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  59. 1 1
      apps/app/src/components/UsersHomepageFooter.module.scss
  60. 1 1
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  61. 1 1
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  62. 1 1
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  63. 34 0
      apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js
  64. 8 8
      apps/app/src/pages/[[...path]].page.tsx
  65. 18 4
      apps/app/src/pages/me/[[...path]].page.tsx
  66. 14 52
      apps/app/src/server/routes/attachment.js
  67. 2 0
      apps/app/src/stores/page-listing.tsx
  68. 90 29
      apps/app/src/stores/ui.tsx
  69. 0 16
      apps/app/src/styles/_layout.scss
  70. 1 1
      apps/app/src/styles/_variables.scss
  71. 0 16
      apps/app/src/styles/molecules/_page-accessories-control.scss
  72. 2 2
      apps/app/src/utils/logger/index.ts
  73. 8 1
      apps/app/tsconfig.json
  74. 4 2
      apps/slackbot-proxy/package.json
  75. 8 1
      apps/slackbot-proxy/tsconfig.json
  76. 1 1
      package.json
  77. 1 1
      packages/core/src/remark-plugins/util/option-parser.ts
  78. 0 7
      packages/core/src/utils/objectid-utils.ts
  79. 4 4
      packages/core/src/utils/page-utils.ts
  80. 1 0
      packages/editor/package.json
  81. 1 0
      packages/editor/src/@types/emoji-mart.d.ts
  82. 3 0
      packages/editor/src/@types/scss.d.ts
  83. 1 1
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  84. 146 4
      packages/editor/src/components/CodeMirrorEditor/Toolbar/EmojiButton.tsx
  85. 8 3
      packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx
  86. 1 1
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  87. 9 2
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  88. 75 0
      packages/editor/src/services/extensions/emojiAutocompletionSettings.ts
  89. 1 0
      packages/editor/src/stores/index.ts
  90. 27 0
      packages/editor/src/stores/use-resolved-theme.ts
  91. 3 0
      packages/editor/vite.config.ts
  92. 1 1
      packages/pluginkit/src/v4/server/utils/template/scan.ts
  93. 4 1
      packages/presentation/tsconfig.json
  94. 75 1
      packages/preset-themes/src/styles/default.scss
  95. 8 1
      packages/remark-attachment-refs/tsconfig.json
  96. 8 1
      packages/remark-drawio/tsconfig.json
  97. 1 1
      packages/remark-lsx/src/client/components/LsxPageList/LsxListView.module.scss
  98. 12 3
      packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx
  99. 1 1
      packages/remark-lsx/src/client/stores/lsx/lsx.ts
  100. 8 1
      packages/remark-lsx/tsconfig.json

+ 18 - 1
CHANGELOG.md

@@ -1,9 +1,26 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.2.1...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.2.2...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.2.2](https://github.com/weseek/growi/compare/v6.2.1...v6.2.2) - 2023-10-30
+
+### 🚀 Improvement
+
+- imprv: Printing styles (#8195) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Show liker counts in lsx (#8194) @yuki-takei
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump postcss from 8.4.26 to 8.4.31 (#8142) @dependabot
+- ci(deps): bump cypress-io/github-action from 5 to 6 (#8051) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 5.0.2 to 5.3.0 (#8127) @dependabot
+- ci(deps): bump aws-actions/configure-aws-credentials from 2 to 4 (#8128) @dependabot
+
 ## [v6.2.1](https://github.com/weseek/growi/compare/v6.2.0...v6.2.1) - 2023-10-03
 
 ### BREAKING CHANGES

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

@@ -11,7 +11,7 @@ Supported tags and respective Dockerfile links
 ------------------------------------------------
 
 * [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
-* [`6.2.1`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.1/apps/app/docker/Dockerfile)
+* [`6.2.2`, `6.2`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.2/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 

+ 2 - 0
apps/app/package.json

@@ -218,6 +218,8 @@
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
+    "@types/throttle-debounce": "^5.0.1",
+    "@types/url-join": "^4.0.2",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "bootstrap": "^5.3.1",

+ 2 - 3
apps/app/src/client/services/user-ui-settings.ts

@@ -17,12 +17,11 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
 
 const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 
-type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
-export const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
+export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
   settingsForBulk = {
     ...settingsForBulk,
     ...settings,
   };
 
-  return _putUserUISettingsInBulkDebounced();
+  _putUserUISettingsInBulkDebounced();
 };

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

@@ -1,4 +1,4 @@
-import * as urljoin from 'url-join';
+import urljoin from 'url-join';
 
 import axios from '~/utils/axios';
 

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

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

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.module.scss

@@ -1,2 +1,2 @@
-@use '@growi/ui/src/styles/molecules/page_list';
+@use '@growi/ui/scss/molecules/page_list';
 

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

@@ -110,7 +110,7 @@ export const CopyDropdown = (props) => {
 
   return (
     <>
-      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown`} isOpen={dropdownOpen} toggle={toggleDropdown}>
+      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`} isOpen={dropdownOpen} toggle={toggleDropdown}>
         <DropdownToggle
           caret
           className={dropdownToggleClassName}

+ 18 - 0
apps/app/src/components/Common/DrawerToggler/DrawerToggler.module.scss

@@ -0,0 +1,18 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '@growi/ui/scss/atoms/btn-muted';
+
+@use '~/styles/variables' as var;
+
+
+.grw-drawer-toggler :global {
+  .btn {
+    --bs-btn-color: rgba(var(--bs-tertiary-color-rgb), 0.5);
+    --bs-btn-bg: transparent;
+
+    --bs-btn-hover-color: rgba(var(--bs-tertiary-color-rgb), 0.7);
+
+    width: var.$grw-sidebar-nav-width;
+    height: var.$grw-sidebar-nav-width;
+  }
+}

+ 36 - 0
apps/app/src/components/Common/DrawerToggler/DrawerToggler.tsx

@@ -0,0 +1,36 @@
+import { type ReactNode } from 'react';
+
+import { useDrawerOpened } from '~/stores/ui';
+
+
+import styles from './DrawerToggler.module.scss';
+
+const moduleClass = styles['grw-drawer-toggler'];
+
+
+type Props = {
+  className?: string,
+  children?: ReactNode,
+}
+
+export const DrawerToggler = (props: Props): JSX.Element => {
+
+  const { className, children } = props;
+
+  const { data: isOpened, mutate } = useDrawerOpened();
+
+  return (
+    <div className={`${moduleClass} ${className ?? ''}`}>
+      <button
+        className="btn d-flex align-items-center border-0"
+        type="button"
+        aria-expanded="false"
+        aria-label="Toggle navigation"
+        onClick={() => mutate(!isOpened)}
+      >
+        {children}
+      </button>
+    </div>
+  );
+
+};

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

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

+ 1 - 0
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -250,6 +250,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
   return (
     <DropdownMenu
+      className="d-print-none"
       data-testid="page-item-control-menu"
       end={alignEnd}
       container="body"

+ 37 - 0
apps/app/src/components/Common/PageViewLayout.module.scss

@@ -0,0 +1,37 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '~/styles/variables' as var;
+
+
+.page-view-layout :global {
+  min-height: calc(100vh - 48px - 250px); // 100vh - subnavigation height - page-comments-row minimum height
+
+  .grw-side-contents-container {
+    margin-bottom: 1rem;
+
+    @include bs.media-breakpoint-up(lg) {
+      width: 250px;
+      min-width: 250px;
+      margin-left: 30px;
+    }
+  }
+}
+
+// md/lg layout padding
+.page-view-layout :global {
+  @include bs.media-breakpoint-between(md, xl) {
+    padding-left: var.$grw-sidebar-nav-width;
+  }
+}
+
+// sticky side contents
+.page-view-layout :global {
+  .grw-side-contents-sticky-container {
+    position: sticky;
+
+    $subnavigation-height: 50px;
+    $page-view-layout-margin-top: 32px;
+    $page-path-nav-height: 99px;
+    top: calc($subnavigation-height + $page-view-layout-margin-top + $page-path-nav-height + 4px);
+  }
+}

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

@@ -16,16 +16,16 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
   return (
     <>
-      <div id="main" className={`main page-view-layout ${styles['page-view-layout']}`}>
+      <div id="main" className={`main ${styles['page-view-layout']}`}>
         <div id="content-main" className="content-main container-lg grw-container-convertible">
           { headerContents != null && headerContents }
           { sideContents != null
             ? (
-              <div className="d-flex flex-column flex-column-reverse flex-lg-row">
+              <div className="d-flex gap-3">
                 <div className="flex-grow-1 flex-basis-0 mw-0">
                   {children}
                 </div>
-                <div className="grw-side-contents-container d-edit-none" data-vrt-blackout-side-contents>
+                <div className="grw-side-contents-container col-lg-3  d-edit-none d-print-none" data-vrt-blackout-side-contents>
                   <div className="grw-side-contents-sticky-container">
                     {sideContents}
                   </div>

+ 1 - 6
apps/app/src/components/IdenticalPathPage.module.scss

@@ -1,6 +1 @@
-@use '@growi/ui/src/styles/molecules/page_list';
-@use '~/styles/molecules/page-accessories-control';
-
-.grw-page-accessories-control :global {
-  @extend %grw-page-accessories-control;
-}
+@use '@growi/ui/scss/molecules/page_list';

+ 0 - 70
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -86,72 +86,6 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
 
   const actionUsers = getActionUsers();
 
-  const actionType: string = notification.action;
-  let actionMsg: string;
-  let actionIcon: string;
-
-  switch (actionType) {
-    case 'PAGE_LIKE':
-      actionMsg = 'liked';
-      actionIcon = 'icon-like';
-      break;
-    case 'PAGE_BOOKMARK':
-      actionMsg = 'bookmarked on';
-      actionIcon = 'icon-star';
-      break;
-    case 'PAGE_UPDATE':
-      actionMsg = 'updated on';
-      actionIcon = 'ti ti-agenda';
-      break;
-    case 'PAGE_RENAME':
-      actionMsg = 'renamed';
-      actionIcon = 'icon-action-redo';
-      break;
-    case 'PAGE_DUPLICATE':
-      actionMsg = 'duplicated';
-      actionIcon = 'icon-docs';
-      break;
-    case 'PAGE_DELETE':
-      actionMsg = 'deleted';
-      actionIcon = 'icon-trash';
-      break;
-    case 'PAGE_DELETE_COMPLETELY':
-      actionMsg = 'completely deleted';
-      actionIcon = 'icon-fire';
-      break;
-    case 'PAGE_REVERT':
-      actionMsg = 'reverted';
-      actionIcon = 'icon-action-undo';
-      break;
-    case 'PAGE_RECURSIVELY_RENAME':
-      actionMsg = 'renamed under';
-      actionIcon = 'icon-action-redo';
-      break;
-    case 'PAGE_RECURSIVELY_DELETE':
-      actionMsg = 'deleted under';
-      actionIcon = 'icon-trash';
-      break;
-    case 'PAGE_RECURSIVELY_DELETE_COMPLETELY':
-      actionMsg = 'deleted completely under';
-      actionIcon = 'icon-fire';
-      break;
-    case 'PAGE_RECURSIVELY_REVERT':
-      actionMsg = 'reverted under';
-      actionIcon = 'icon-action-undo';
-      break;
-    case 'COMMENT_CREATE':
-      actionMsg = 'commented on';
-      actionIcon = 'icon-bubble';
-      break;
-    case 'USER_REGISTRATION_APPROVAL_REQUEST':
-      actionMsg = 'requested registration approval';
-      actionIcon = 'icon-bubble';
-      break;
-    default:
-      actionMsg = '';
-      actionIcon = '';
-  }
-
   const isDropdownItem = props.type === 'dropdown-item';
 
   // determine tag
@@ -175,8 +109,6 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
           <PageModelNotification
             ref={notificationRef}
             notification={notification}
-            actionMsg={actionMsg}
-            actionIcon={actionIcon}
             actionUsers={actionUsers}
           />
         )}
@@ -184,8 +116,6 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
           <UserModelNotification
             ref={notificationRef}
             notification={notification}
-            actionMsg={actionMsg}
-            actionIcon={actionIcon}
             actionUsers={actionUsers}
           />
         )}

+ 45 - 0
apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx

@@ -0,0 +1,45 @@
+import React, { FC, useImperativeHandle } from 'react';
+
+import type { HasObjectId } from '@growi/core';
+import { PagePathLabel } from '@growi/ui/dist/components';
+
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+import FormattedDistanceDate from '../../FormattedDistanceDate';
+
+type Props = {
+  notification: IInAppNotification & HasObjectId
+  actionMsg: string
+  actionIcon: string
+  actionUsers: string
+  publishOpen:() => void
+  ref: React.ForwardedRef<IInAppNotificationOpenable>
+};
+
+export const ModelNotification: FC<Props> = (props) => {
+  const {
+    notification, actionMsg, actionIcon, actionUsers, publishOpen, ref,
+  } = props;
+
+  useImperativeHandle(ref, () => ({
+    open() {
+      publishOpen();
+    },
+  }));
+
+  return (
+    <div className="p-2 overflow-hidden">
+      <div className="text-truncate">
+        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
+      </div>
+      <i className={`${actionIcon} me-2`} />
+      <FormattedDistanceDate
+        id={notification._id}
+        date={notification.createdAt}
+        isShowTooltip={false}
+        differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
+      />
+    </div>
+  );
+};

+ 23 - 28
apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -1,57 +1,52 @@
 import React, {
-  forwardRef, ForwardRefRenderFunction, useImperativeHandle,
+  forwardRef, ForwardRefRenderFunction,
 } from 'react';
 
 import type { HasObjectId } from '@growi/core';
-import { PagePathLabel } from '@growi/ui/dist/components/PagePath';
 import { useRouter } from 'next/router';
 
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import FormattedDistanceDate from '../../FormattedDistanceDate';
+import { ModelNotification } from './ModelNotification';
+import { useActionMsgAndIconForPageModelNotification } from './useActionAndMsg';
+
 
 interface Props {
   notification: IInAppNotification & HasObjectId
-  actionMsg: string
-  actionIcon: string
   actionUsers: string
 }
 
 const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, Props> = (props: Props, ref) => {
 
   const {
-    notification, actionMsg, actionIcon, actionUsers,
+    notification, actionUsers,
   } = props;
 
+  const { actionMsg, actionIcon } = useActionMsgAndIconForPageModelNotification(notification);
+
   const router = useRouter();
 
   // publish open()
-  useImperativeHandle(ref, () => ({
-    open() {
-      if (notification.target != null) {
-        // jump to target page
-        const targetPagePath = notification.target.path;
-        if (targetPagePath != null) {
-          router.push(targetPagePath);
-        }
+  const publishOpen = () => {
+    if (notification.target != null) {
+      // jump to target page
+      const targetPagePath = notification.target.path;
+      if (targetPagePath != null) {
+        router.push(targetPagePath);
       }
-    },
-  }));
+    }
+  };
 
   return (
-    <div className="p-2 overflow-hidden">
-      <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
-      </div>
-      <i className={`${actionIcon} me-2`} />
-      <FormattedDistanceDate
-        id={notification._id}
-        date={notification.createdAt}
-        isShowTooltip={false}
-        differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
-      />
-    </div>
+    <ModelNotification
+      notification={notification}
+      actionMsg={actionMsg}
+      actionIcon={actionIcon}
+      actionUsers={actionUsers}
+      publishOpen={publishOpen}
+      ref={ref}
+    />
   );
 };
 

+ 18 - 22
apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx

@@ -1,5 +1,5 @@
 import React, {
-  forwardRef, ForwardRefRenderFunction, useImperativeHandle,
+  forwardRef, ForwardRefRenderFunction,
 } from 'react';
 
 import type { HasObjectId } from '@growi/core';
@@ -8,38 +8,34 @@ import { useRouter } from 'next/router';
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import FormattedDistanceDate from '../../FormattedDistanceDate';
+import { ModelNotification } from './ModelNotification';
+import { useActionMsgAndIconForUserModelNotification } from './useActionAndMsg';
+
 
 const UserModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, {
   notification: IInAppNotification & HasObjectId
-  actionMsg: string
-  actionIcon: string
   actionUsers: string
 }> = ({
-  notification, actionMsg, actionIcon, actionUsers,
+  notification, actionUsers,
 }, ref) => {
   const router = useRouter();
 
+  const { actionMsg, actionIcon } = useActionMsgAndIconForUserModelNotification(notification);
+
   // publish open()
-  useImperativeHandle(ref, () => ({
-    open() {
-      router.push('/admin/users');
-    },
-  }));
+  const publishOpen = () => {
+    router.push('/admin/users');
+  };
 
   return (
-    <div className="p-2 overflow-hidden">
-      <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg}
-      </div>
-      <i className={`${actionIcon} me-2`} />
-      <FormattedDistanceDate
-        id={notification._id}
-        date={notification.createdAt}
-        isShowTooltip={false}
-        differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
-      />
-    </div>
+    <ModelNotification
+      notification={notification}
+      actionMsg={actionMsg}
+      actionIcon={actionIcon}
+      actionUsers={actionUsers}
+      publishOpen={publishOpen}
+      ref={ref}
+    />
   );
 };
 

+ 99 - 0
apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts

@@ -0,0 +1,99 @@
+import type { HasObjectId } from '@growi/core';
+
+import { SupportedAction } from '~/interfaces/activity';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+export type ActionMsgAndIconType = {
+  actionMsg: string
+  actionIcon: string
+}
+
+export const useActionMsgAndIconForPageModelNotification = (notification: IInAppNotification & HasObjectId): ActionMsgAndIconType => {
+  const actionType: string = notification.action;
+  let actionMsg: string;
+  let actionIcon: string;
+
+  switch (actionType) {
+    case SupportedAction.ACTION_PAGE_LIKE:
+      actionMsg = 'liked';
+      actionIcon = 'icon-like';
+      break;
+    case SupportedAction.ACTION_PAGE_BOOKMARK:
+      actionMsg = 'bookmarked on';
+      actionIcon = 'icon-star';
+      break;
+    case SupportedAction.ACTION_PAGE_UPDATE:
+      actionMsg = 'updated on';
+      actionIcon = 'ti ti-agenda';
+      break;
+    case SupportedAction.ACTION_PAGE_RENAME:
+      actionMsg = 'renamed';
+      actionIcon = 'icon-action-redo';
+      break;
+    case SupportedAction.ACTION_PAGE_DUPLICATE:
+      actionMsg = 'duplicated';
+      actionIcon = 'icon-docs';
+      break;
+    case SupportedAction.ACTION_PAGE_DELETE:
+      actionMsg = 'deleted';
+      actionIcon = 'icon-trash';
+      break;
+    case SupportedAction.ACTION_PAGE_DELETE_COMPLETELY:
+      actionMsg = 'completely deleted';
+      actionIcon = 'icon-fire';
+      break;
+    case SupportedAction.ACTION_PAGE_REVERT:
+      actionMsg = 'reverted';
+      actionIcon = 'icon-action-undo';
+      break;
+    case SupportedAction.ACTION_PAGE_RECURSIVELY_RENAME:
+      actionMsg = 'renamed under';
+      actionIcon = 'icon-action-redo';
+      break;
+    case SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE:
+      actionMsg = 'deleted under';
+      actionIcon = 'icon-trash';
+      break;
+    case SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY:
+      actionMsg = 'deleted completely under';
+      actionIcon = 'icon-fire';
+      break;
+    case SupportedAction.ACTION_PAGE_RECURSIVELY_REVERT:
+      actionMsg = 'reverted under';
+      actionIcon = 'icon-action-undo';
+      break;
+    case SupportedAction.ACTION_COMMENT_CREATE:
+      actionMsg = 'commented on';
+      actionIcon = 'icon-bubble';
+      break;
+    default:
+      actionMsg = '';
+      actionIcon = '';
+  }
+
+  return {
+    actionMsg,
+    actionIcon,
+  };
+};
+
+export const useActionMsgAndIconForUserModelNotification = (notification: IInAppNotification & HasObjectId): ActionMsgAndIconType => {
+  const actionType: string = notification.action;
+  let actionMsg: string;
+  let actionIcon: string;
+
+  switch (actionType) {
+    case SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST:
+      actionMsg = 'requested registration approval';
+      actionIcon = 'icon-bubble';
+      break;
+    default:
+      actionMsg = '';
+      actionIcon = '';
+  }
+
+  return {
+    actionMsg,
+    actionIcon,
+  };
+};

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

@@ -33,7 +33,7 @@ type Props = {
 
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
-    <RawLayout className={className ?? ''}>
+    <RawLayout className={`${className ?? ''}`}>
       <DndProvider backend={HTML5Backend}>
 
         <div className="page-wrapper flex-row">

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

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

+ 0 - 31
apps/app/src/components/Navbar/DrawerToggler.tsx

@@ -1,31 +0,0 @@
-import React from 'react';
-
-import { useDrawerOpened } from '~/stores/ui';
-
-type Props = {
-  iconClass?: string,
-}
-
-const DrawerToggler = (props: Props): JSX.Element => {
-
-  const { data: isOpened, mutate } = useDrawerOpened();
-
-  const iconClass = props.iconClass ?? isOpened
-    ? 'icon-arrow-left'
-    : 'icon-arrow-right';
-
-  return (
-    <button
-      className="grw-drawer-toggler btn btn-secondary"
-      type="button"
-      aria-expanded="false"
-      aria-label="Toggle navigation"
-      onClick={() => mutate(!isOpened)}
-    >
-      <i className={iconClass}></i>
-    </button>
-  );
-
-};
-
-export default DrawerToggler;

+ 18 - 19
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -331,28 +331,27 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     <>
       <div
         className={`${styles['grw-contextual-sub-navigation']}
-          d-flex align-items-center justify-content-end px-2 py-1 gap-2 gap-md-4
+          d-flex align-items-center justify-content-end px-2 py-1 gap-2 gap-md-4 d-print-none
         `}
         data-testid="grw-contextual-sub-nav"
       >
-        <div className="h-50">
-          {pageId != null && (
-            <PageControls
-              pageId={pageId}
-              revisionId={revisionId}
-              shareLinkId={shareLinkId}
-              path={path ?? currentPathname} // If the page is empty, "path" is undefined
-              expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
-              disableSeenUserInfoPopover={isSharedUser}
-              showPageControlDropdown={isAbleToShowPageManagement}
-              additionalMenuItemRenderer={additionalMenuItemsRenderer}
-              onClickDuplicateMenuItem={duplicateItemClickedHandler}
-              onClickRenameMenuItem={renameItemClickedHandler}
-              onClickDeleteMenuItem={deleteItemClickedHandler}
-              onClickSwitchContentWidth={switchContentWidthHandler}
-            />
-          )}
-        </div>
+        {pageId != null && (
+          <PageControls
+            pageId={pageId}
+            revisionId={revisionId}
+            shareLinkId={shareLinkId}
+            path={path ?? currentPathname} // If the page is empty, "path" is undefined
+            expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
+            disableSeenUserInfoPopover={isSharedUser}
+            showPageControlDropdown={isAbleToShowPageManagement}
+            additionalMenuItemRenderer={additionalMenuItemsRenderer}
+            onClickDuplicateMenuItem={duplicateItemClickedHandler}
+            onClickRenameMenuItem={renameItemClickedHandler}
+            onClickDeleteMenuItem={deleteItemClickedHandler}
+            onClickSwitchContentWidth={switchContentWidthHandler}
+          />
+        )}
+
         {isAbleToChangeEditorMode && (
           <PageEditorModeManager
             editorMode={editorMode}

+ 21 - 5
apps/app/src/components/Navbar/GrowiNavbarBottom.module.scss

@@ -2,15 +2,31 @@
 @use '~/styles/mixins';
 
 .grw-navbar-bottom :global {
-  height: var.$grw-navbar-bottom-height;
-
   // apply transition
   transition-property: bottom;
   @include mixins.apply-navigation-transition();
+
+  .navbar {
+    height: var.$grw-navbar-bottom-height;
+  }
+}
+
+.grw-navbar-bottom-drawer-opened {
+  bottom: #{-1 * var.$grw-navbar-bottom-height};
 }
 
-.grw-navbar-bottom {
-  &:global(.grw-navbar-bottom-drawer-opened) {
-    bottom: #{-1 * var.$grw-navbar-bottom-height};
+// centering icons
+.grw-navbar-bottom :global {
+  .nav-link {
+    display: flex;
+    align-items: center;
+  }
+}
+
+// == Colors
+.grw-navbar-bottom :global {
+  .navbar {
+    background-color: rgba(var(--bs-body-bg-rgb), 0.7);
+    backdrop-filter: blur(35px);
   }
 }

+ 29 - 17
apps/app/src/components/Navbar/GrowiNavbarBottom.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { useIsSearchPage } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
-import { useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
+import { useIsDeviceLargerThanMd, useDrawerOpened } from '~/stores/ui';
 
 import { GlobalSearch } from './GlobalSearch';
 
@@ -13,20 +13,19 @@ import styles from './GrowiNavbarBottom.module.scss';
 export const GrowiNavbarBottom = (): JSX.Element => {
 
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
   const { open: openCreateModal } = usePageCreateModal();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSearchPage } = useIsSearchPage();
 
-  const additionalClasses = ['grw-navbar-bottom', styles['grw-navbar-bottom']];
-  if (isDrawerOpened) {
-    additionalClasses.push('grw-navbar-bottom-drawer-opened');
-  }
-
   return (
-    <div className="d-md-none d-edit-none fixed-bottom">
+    <div className={`
+      ${styles['grw-navbar-bottom']}
+      ${isDrawerOpened ? styles['grw-navbar-bottom-drawer-opened'] : ''}
+      d-md-none d-edit-none d-print-none fixed-bottom`}
+    >
 
-      { isDeviceSmallerThanMd && !isSearchPage && (
+      { !isDeviceLargerThanMd && !isSearchPage && (
         <div id="grw-global-search-collapse" className="grw-global-search collapse bg-dark">
           <div className="p-3">
             <GlobalSearch dropup />
@@ -34,18 +33,29 @@ export const GrowiNavbarBottom = (): JSX.Element => {
         </div>
       ) }
 
-      <div className={`navbar navbar-expand navbar-dark bg-primary px-0 ${additionalClasses.join(' ')}`}>
+      <div className="navbar navbar-expand px-4 px-sm-5">
 
-        <ul className="navbar-nav w-100">
-          <li className="nav-item me-auto">
+        <ul className="navbar-nav flex-grow-1 d-flex align-items-center justify-content-between">
+          <li className="nav-item">
             <a
               role="button"
               className="nav-link btn-lg"
               onClick={() => mutateDrawerOpened(true)}
             >
-              <i className="icon-menu"></i>
+              <span className="material-symbols-outlined fs-2">reorder</span>
             </a>
           </li>
+
+          <li className="nav-item">
+            <a
+              role="button"
+              className="nav-link btn-lg"
+              onClick={() => openCreateModal(currentPagePath || '')}
+            >
+              <span className="material-symbols-outlined fs-2">edit</span>
+            </a>
+          </li>
+
           {
             !isSearchPage && (
               <li className="nav-item">
@@ -55,20 +65,22 @@ export const GrowiNavbarBottom = (): JSX.Element => {
                   data-bs-target="#grw-global-search-collapse"
                   data-bs-toggle="collapse"
                 >
-                  <i className="icon-magnifier"></i>
+                  <span className="material-symbols-outlined fs-2">search</span>
                 </a>
               </li>
             )
           }
-          <li className="nav-item ms-auto">
+
+          <li className="nav-item">
             <a
               role="button"
               className="nav-link btn-lg"
-              onClick={() => openCreateModal(currentPagePath || '')}
+              onClick={() => {}}
             >
-              <i className="icon-pencil"></i>
+              <span className="material-symbols-outlined fs-2">notifications</span>
             </a>
           </li>
+
         </ul>
       </div>
 

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

@@ -7,11 +7,12 @@
     --bs-btn-font-size: 13px;
     --bs-btn-border-width: 2px;
 
-    width: 70px;
-    height: 30px;
-    @include bs.media-breakpoint-down(sm) {
-      width: 90px;
-      height: 38px;
+    width: 90px;
+    height: 38px;
+
+    @include bs.media-breakpoint-up(md) {
+      width: 70px;
+      height: 30px;
     }
 
     @include mixins.border-vertical('before', 70%, 1, true);
@@ -19,11 +20,11 @@
 }
 
 .grw-page-editor-mode-manager-skeleton :global {
-  width: 179px;
-  height: 30px;
-  @include bs.media-breakpoint-down(sm) {
-    width: 90px;
-    height: 38px;
+  width: 90px;
+  height: 38px;
+  @include bs.media-breakpoint-up(md) {
+    width: 179px;
+    height: 30px;
   }
 }
 

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

@@ -2,7 +2,7 @@ import React, { type ReactNode, useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
+import { EditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 
 import { useOnPageEditorModeButtonClicked } from './hooks';
 
@@ -61,7 +61,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const [isCreating, setIsCreating] = useState(false);
 
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
 
   const onPageEditorModeButtonClicked = useOnPageEditorModeButtonClicked(setIsCreating, path, grant, grantUserGroupId);
   const _isBtnDisabled = isCreating || isBtnDisabled;
@@ -82,7 +82,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
         aria-label="page-editor-mode-manager"
         id="grw-page-editor-mode-manager"
       >
-        {(!isDeviceSmallerThanMd || editorMode !== EditorMode.View) && (
+        {(isDeviceLargerThanMd || editorMode !== EditorMode.View) && (
           <PageEditorModeButton
             currentEditorMode={editorMode}
             editorMode={EditorMode.View}
@@ -92,7 +92,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
             <span className="material-symbols-outlined fs-4">play_arrow</span>{t('View')}
           </PageEditorModeButton>
         )}
-        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && (
+        {(isDeviceLargerThanMd || editorMode === EditorMode.View) && (
           <PageEditorModeButton
             currentEditorMode={editorMode}
             editorMode={EditorMode.Editor}

+ 1 - 1
apps/app/src/components/Page/PageView.tsx

@@ -18,7 +18,7 @@ import { useIsMobile } from '~/stores/ui';
 
 import type { CommentsProps } from '../Comments';
 import { PagePathNavSticky } from '../Common/PagePathNav';
-import { PageViewLayout } from '../Layout/PageViewLayout';
+import { PageViewLayout } from '../Common/PageViewLayout';
 import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import type { PageSideContentsProps } from '../PageSideContents';

+ 6 - 0
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useState, useRef, useEffect,
 } from 'react';
 
+import { useResolvedThemeForEditor } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -19,6 +20,7 @@ import {
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
@@ -76,6 +78,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     increment: incrementEditingCommentsNum,
     decrement: decrementEditingCommentsNum,
   } = useSWRxEditingCommentsNum();
+  const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
+
+  const { resolvedTheme } = useNextThemes();
+  mutateResolvedTheme(resolvedTheme);
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');

+ 3 - 1
apps/app/src/components/PageControls/BookmarkButtons.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 @use './button-styles';
 
 .btn-group-bookmark :global {
@@ -18,7 +20,7 @@
 // == Colors
 .btn-group-bookmark :global {
   .btn-bookmark {
-    @include button-styles.btn-color(bs.$orange);
+    @include btn-muted.colorize(bs.$orange);
   }
 }
 

+ 3 - 1
apps/app/src/components/PageControls/LikeButtons.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 @use './button-styles';
 
 .btn-group-like :global {
@@ -18,6 +20,6 @@
 // == Colors
 .btn-group-like :global {
   .btn-like {
-    @include button-styles.btn-color(bs.$red);
+    @include btn-muted.colorize(bs.$red);
   }
 }

+ 3 - 1
apps/app/src/components/PageControls/PageControls.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 @use './button-styles';
 
 // PageItemControl styles
@@ -13,6 +15,6 @@
 // PageItemControl colors
 .grw-page-controls :global {
   .btn-page-item-control {
-    @include button-styles.btn-color(bs.$gray-500);
+    @include btn-muted.colorize(bs.$gray-500);
   }
 }

+ 3 - 1
apps/app/src/components/PageControls/SeenUserInfo.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 @use './button-styles';
 
 .grw-seen-user-info :global {
@@ -18,6 +20,6 @@
   $color: #549c79;
 
   .btn-seen-user {
-    @include button-styles.btn-color($color);
+    @include btn-muted.colorize($color);
   }
 }

+ 3 - 1
apps/app/src/components/PageControls/SubscribeButton.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 @use './button-styles';
 
 .btn-subscribe :global {
@@ -12,5 +14,5 @@
 
 // == Colors
 .btn-subscribe {
-  @include button-styles.btn-color(bs.$success);
+  @include btn-muted.colorize(bs.$success);
 }

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

@@ -15,20 +15,3 @@
 %text-total-counts-basis {
   font-size: 13px;
 }
-
-@mixin btn-color($color) {
-  $color-rgb: #{bs.to-rgb($color)};
-
-  --bs-btn-color: var(--bs-tertiary-color);
-  --bs-btn-bg: transparent;
-
-  --bs-btn-hover-color: #{$color};
-  --bs-btn-hover-bg: rgba(#{$color-rgb}, 0.2);
-
-  --bs-btn-active-color: #{$color};
-  --bs-btn-active-bg: transparent;
-
-  &:hover {
-    --bs-btn-active-bg: rgba(#{$color-rgb}, 0.2);
-  }
-}

+ 7 - 18
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -9,7 +9,7 @@ import { useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import {
-  useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
+  useDrawerOpened, useEditorMode, useIsDeviceLargerThanLg, useIsDeviceLargerThanMd,
 } from '~/stores/ui';
 
 
@@ -31,8 +31,8 @@ const EditorNavbarBottom = (): JSX.Element => {
 
   const { data: editorMode } = useEditorMode();
   const { data: isSlackConfigured } = useIsSlackConfigured();
-  const { mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
+  const { data: isDeviceLargerThanLg } = useIsDeviceLargerThanLg();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
 
@@ -58,16 +58,6 @@ const EditorNavbarBottom = (): JSX.Element => {
   }, []);
 
 
-  const renderDrawerButton = () => (
-    <button
-      type="button"
-      className="btn btn-outline-secondary border-0"
-      onClick={() => mutateDrawerOpened(true)}
-    >
-      <i className="icon-menu"></i>
-    </button>
-  );
-
   const renderExpandButton = () => (
     <div className="d-md-none ms-2">
       <button
@@ -80,13 +70,13 @@ const EditorNavbarBottom = (): JSX.Element => {
     </div>
   );
 
-  const isCollapsedOptionsSelectorEnabled = isDeviceSmallerThanMd;
+  const isCollapsedOptionsSelectorEnabled = !isDeviceLargerThanLg;
 
   return (
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
       {/* Collapsed SlackNotification */}
       {isSlackConfigured && (
-        <Collapse isOpen={isSlackExpanded && isDeviceSmallerThanMd === true}>
+        <Collapse isOpen={isSlackExpanded && !isDeviceLargerThanLg}>
           <nav className={`navbar navbar-expand-lg border-top ${moduleClass}`}>
             {isSlackEnabled != null
             && (
@@ -105,13 +95,12 @@ const EditorNavbarBottom = (): JSX.Element => {
       }
       <div className={`flex-expand-horiz align-items-center border-top px-2 px-md-3 ${moduleClass}`}>
         <form>
-          { isDeviceSmallerThanMd && renderDrawerButton() }
-          { !isDeviceSmallerThanMd && <OptionsSelector /> }
+          { isDeviceLargerThanMd && <OptionsSelector /> }
         </form>
         <form className="flex-nowrap ms-auto">
           {/* Responsive Design for the SlackNotification */}
           {/* Button or the normal Slack banner */}
-          {isSlackConfigured && (isDeviceSmallerThanMd ? (
+          {isSlackConfigured && (!isDeviceLargerThanMd ? (
             <Button
               className="grw-btn-slack border me-2"
               onClick={() => (setSlackExpanded(!isSlackExpanded))}

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

@@ -8,7 +8,8 @@ import nodePath from 'path';
 import type { IPageHasId } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
-  CodeMirrorEditorMain, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, AcceptedUploadFileType,
+  CodeMirrorEditorMain, GlobalCodeMirrorEditorKey, AcceptedUploadFileType,
+  useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
@@ -48,6 +49,7 @@ import {
   EditorMode,
   useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
+import { useNextThemes } from '~/stores/use-next-themes';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
@@ -123,9 +125,13 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsConflict } = useIsConflict();
 
+  const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
+
   const saveOrUpdate = useSaveOrUpdate();
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
+  const { resolvedTheme } = useNextThemes();
+  mutateResolvedTheme(resolvedTheme);
 
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923
@@ -336,16 +342,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         // refs: https://redmine.weseek.co.jp/issues/126528
         // editorRef.current.insertText(insertText);
         codeMirrorEditor?.insertText(insertText);
-
-        // when if created newly
-        // Not using 'mutateGrant' to inherit the grant of the parent page
-        if (resAdd.pageCreated) {
-          logger.info('Page is created', resAdd.page._id);
-          mutateIsLatestRevision(true);
-          setCreatedPageRevisionIdWithAttachment(resAdd.page.revision);
-          await mutateCurrentPageId(resAdd.page._id);
-          await mutateCurrentPage();
-        }
       }
       catch (e) {
         logger.error('failed to upload', e);
@@ -358,7 +354,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       }
     });
 
-  }, [codeMirrorEditor, currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateIsLatestRevision, pageId]);
+  }, [codeMirrorEditor, currentPagePath, pageId]);
 
   const acceptedFileType = useMemo(() => {
     if (!isUploadableFile) {

+ 1 - 1
apps/app/src/components/PageList/PageList.module.scss

@@ -1 +1 @@
-@use '@growi/ui/src/styles/molecules/page_list';
+@use '@growi/ui/scss/molecules/page_list';

+ 7 - 8
apps/app/src/components/PageList/PageListItemL.tsx

@@ -9,8 +9,7 @@ import type {
 import { isIPageInfoForListing, isIPageInfoForEntity } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pathUtils } from '@growi/core/dist/utils';
-import { UserPicture } from '@growi/ui/dist/components';
-import { PageListMeta } from '@growi/ui/dist/components/PagePath';
+import { UserPicture, PageListMeta } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
@@ -29,7 +28,7 @@ import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import {
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
 } from '~/stores/modal';
-import { useIsDeviceSmallerThanLg } from '~/stores/ui';
+import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
@@ -83,7 +82,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     },
   }));
 
-  const { data: isDeviceSmallerThanLg } = useIsDeviceSmallerThanLg();
+  const { data: isDeviceLargerThanLg } = useIsDeviceLargerThanLg();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
@@ -117,14 +116,14 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   // click event handler
   const clickHandler = useCallback(() => {
     // do nothing if mobile
-    if (isDeviceSmallerThanLg) {
+    if (!isDeviceLargerThanLg) {
       return;
     }
 
     if (onClickItem != null) {
       onClickItem(pageData._id);
     }
-  }, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
+  }, [isDeviceLargerThanLg, onClickItem, pageData._id]);
 
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
@@ -174,9 +173,9 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler });
   }, [onPagePutBacked, openPutBackPageModal, pageData]);
 
-  const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
+  const styleListGroupItem = (isDeviceLargerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
   // background color of list item changes when class "active" exists under 'list-group-item'
-  const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
+  const styleActive = isDeviceLargerThanLg && isSelected ? 'active' : '';
 
   const shouldDangerouslySetInnerHTMLForPaths = elasticSearchResult != null && elasticSearchResult.highlightedPath != null;
 

+ 1 - 2
apps/app/src/components/PageList/PageListItemS.tsx

@@ -1,8 +1,7 @@
 import React from 'react';
 
 import type { IPageHasId } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
-import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components/PagePath';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 

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

@@ -152,12 +152,12 @@ const PageRenameModal = (): JSX.Element => {
   }, [checkExistPaths]);
 
   const checkIsUsersHomepageDebounce = useMemo(() => {
-    const checkIsPagePathRenameable = () => {
+    const checkIsPagePathRenameable = (pageNameInput: string) => {
       setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
 
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomepage, pageNameInput]);
+  }, [isUsersHomepage]);
 
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {

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

@@ -0,0 +1,30 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.btn-page-accessories :global {
+  display: flex;
+  align-items: center;
+  padding: 6px 8px;
+
+  .grw-labels {
+    flex-grow: 1;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+
+// apply larger font when smaller than lg
+@include bs.media-breakpoint-down(lg) {
+  .btn-page-accessories :global {
+    .material-symbols-outlined {
+      font-size: 2em;
+    }
+  }
+}
+
+// expand when larger than lg
+@include bs.media-breakpoint-up(lg) {
+  .btn-page-accessories :global {
+    flex-grow: 1;
+    padding: 1px 5px 1px 10px;
+  }
+}

+ 42 - 0
apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx

@@ -0,0 +1,42 @@
+import { type ReactNode, memo } from 'react';
+
+import CountBadge from '../Common/CountBadge';
+
+
+import styles from './PageAccessoriesControl.module.scss';
+
+const moduleClass = styles['btn-page-accessories'];
+
+
+type Props = {
+  className?: string,
+  icon: ReactNode,
+  label: ReactNode,
+  count?: number,
+  onClick?: () => void,
+}
+
+export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
+  const {
+    icon, label, count,
+    className,
+    onClick,
+  } = props;
+
+  return (
+    <button
+      type="button"
+      className={`btn btn-sm btn-outline-secondary ${moduleClass} ${className} rounded-pill`}
+      onClick={onClick}
+    >
+      <span className="grw-icon d-flex">{icon}</span>
+      <span className="grw-labels ms-1 d-none d-lg-flex">
+        {label}
+        {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
+        { count != null
+          ? <CountBadge count={count} offset={1} />
+          : <div className="px-2"></div>}
+      </span>
+    </button>
+  );
+});

+ 1 - 4
apps/app/src/components/PageSideContents/PageSideContents.module.scss

@@ -1,5 +1,2 @@
-@use '~/styles/molecules/page-accessories-control';
-
-.grw-page-accessories-control :global {
-  @extend %grw-page-accessories-control;
+.grw-page-accessories-controls :global {
 }

+ 27 - 41
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -4,7 +4,7 @@ import { getIdForRef, type IPageHasId, type IPageInfoForOperation } from '@growi
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
-import { Link } from 'react-scroll';
+import { scroller } from 'react-scroll';
 
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { apiPost } from '~/client/util/apiv1-client';
@@ -14,12 +14,12 @@ import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
 import { useIsAbleToShowTagLabel } from '~/stores/ui';
 
-import CountBadge from '../Common/CountBadge';
 import { ContentLinkButtons } from '../ContentLinkButtons';
-import PageListIcon from '../Icons/PageListIcon';
 import { PageTagsSkeleton } from '../PageTags';
 import TableOfContents from '../TableOfContents';
 
+import { PageAccessoriesControl } from './PageAccessoriesControl';
+
 import styles from './PageSideContents.module.scss';
 
 
@@ -104,48 +104,34 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       {/* Tags */}
       <Tags pageId={page._id} revisionId={getIdForRef(page.revision)} />
 
-      {/* Page list */}
-      <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
+      <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
+        {/* Page list */}
         {!isSharedUser && (
-          <button
-            type="button"
-            className="btn btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
-            onClick={() => openDescendantPageListModal(pagePath)}
-            data-testid="pageListButton"
-          >
-            <div className="grw-page-accessories-control-icon">
-              <PageListIcon />
-            </div>
-            {t('page_list')}
-
-            {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
-            { !isTrash && pageInfo != null
-              ? <CountBadge count={(pageInfo as IPageInfoForOperation).descendantCount} offset={1} />
-              : <div className="px-2"></div>}
-          </button>
+          <div className="d-flex" data-testid="pageListButton">
+            <PageAccessoriesControl
+              icon={<span className="material-symbols-outlined">subject</span>}
+              label={t('page_list')}
+              // Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600
+              count={!isTrash && pageInfo != null ? (pageInfo as IPageInfoForOperation).descendantCount : undefined}
+              onClick={() => openDescendantPageListModal(pagePath)}
+            />
+          </div>
+        )}
+
+        {/* Comments */}
+        {!isTopPagePath && (
+          <div className="d-flex" data-testid="page-comment-button">
+            <PageAccessoriesControl
+              icon={<span className="material-symbols-outlined">chat</span>}
+              label="Comments"
+              count={pageInfo != null ? (pageInfo as IPageInfoForOperation).commentCount : undefined}
+              onClick={() => scroller.scrollTo('comments-container', { smooth: false, offset: -120 })}
+            />
+          </div>
         )}
       </div>
 
-      {/* Comments */}
-      {!isTopPagePath && (
-        <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-          <Link to="page-comments" offset={-120}>
-            <button
-              type="button"
-              className="btn btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
-              data-testid="page-comment-button"
-            >
-              <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
-              <span>Comments</span>
-              { pageInfo != null
-                ? <CountBadge count={(pageInfo as IPageInfoForOperation).commentCount} />
-                : <div className="px-2"></div>}
-            </button>
-          </Link>
-        </div>
-      )}
-
-      <div className="d-none d-lg-block">
+      <div className="d-none d-xl-block">
         <TableOfContents />
         {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
       </div>

+ 3 - 1
apps/app/src/components/PageTags/PageTags.tsx

@@ -34,9 +34,11 @@ export const PageTags:FC<Props> = (props: Props) => {
     return <PageTagsSkeleton />;
   }
 
+  const printNoneClass = tags.length === 0 ? 'd-print-none' : '';
+
   return (
     <>
-      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
+      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center ${printNoneClass}`} data-testid="grw-tag-labels">
         <RenderTagLabels
           tags={tags}
           openEditorModal={openEditorModal}

+ 1 - 1
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -44,7 +44,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
       })}
       <NotAvailableForGuest>
         <NotAvailableForReadOnlyUser>
-          <div id="edit-tags-btn-wrapper-for-tooltip">
+          <div id="edit-tags-btn-wrapper-for-tooltip" className="d-print-none">
             <a
               className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isTagLabelsDisabled && 'disabled'}`}
               onClick={openEditorHandler}

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

@@ -70,7 +70,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const { grant, grantedGroup } = grantData;
 
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
-  const labelSubmitButton = (currentPage != null && !currentPage.isEmpty) ? t('Update') : t('Create');
+  const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
   return (

+ 1 - 1
apps/app/src/components/SearchPage/SearchPageBase.module.scss

@@ -1 +1 @@
-@use '@growi/ui/src/styles/molecules/page_list';
+@use '@growi/ui/scss/molecules/page_list';

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

@@ -3,8 +3,7 @@ import React, {
   KeyboardEvent, useCallback, useRef, useState, MouseEvent, useEffect,
 } from 'react';
 
-import { UserPicture } from '@growi/ui/dist/components';
-import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components/PagePath';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 
 import { IFocusable } from '~/client/interfaces/focusable';

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

@@ -11,7 +11,7 @@ import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 import { PagePathNavSticky } from './Common/PagePathNav';
-import { PageViewLayout } from './Layout/PageViewLayout';
+import { PageViewLayout } from './Common/PageViewLayout';
 import RevisionRenderer from './Page/RevisionRenderer';
 import ShareLinkAlert from './Page/ShareLinkAlert';
 import type { PageSideContentsProps } from './PageSideContents';

+ 20 - 3
apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -27,11 +27,28 @@
 
 // == Location
 .on-subnavigation {
-  $grw-contextual-sub-navigation-width: 500px;
+  top: 0;
 
-  left: var.$grw-sidebar-nav-width;
+  @include bs.media-breakpoint-up(md) {
+    left: var.$grw-sidebar-nav-width;
+  }
+}
+
+
+// == App title truncation
+.on-subnavigation {
   // set width for truncation
-  width: calc(100vw - $grw-contextual-sub-navigation-width);
+  $grw-page-controls-width: 226px;
+  $grw-page-editor-mode-manager-width: 90px;
+  $grw-contextual-subnavigation-padding-right: 8px;
+  $gap: 8px;
+  width: calc(100vw - #{$grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
+
+  @include bs.media-breakpoint-up(md) {
+    $grw-page-editor-mode-manager-width: 140px;
+    $gap: 24px;
+    width: calc(100vw - #{var.$grw-sidebar-nav-width + $grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
+  }
 }
 
 .on-sidebar-head {

+ 25 - 6
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -5,10 +5,6 @@
 
 .grw-sidebar :global {
   top: 0;
-
-  .sidebar-contents-container {
-    backdrop-filter: blur(20px);
-  }
 }
 
 
@@ -69,6 +65,7 @@
         transform: translateX(-100%);
       }
       &.open {
+        z-index: bs.$zindex-modal;
         transform: translateX(0);
       }
     }
@@ -81,7 +78,18 @@
     --bs-border-color: var(--grw-highlight-200);
 
     .sidebar-contents-container {
-      background-color: rgba(var(--grw-highlight-100-rgb), .5);
+      background-color: color-mix(in srgb, var(--grw-highlight-100), var(--bs-body-bg));
+    }
+  }
+  // frosted glass effect in collapsed mode
+  .grw-sidebar {
+    &:global {
+      &.grw-sidebar-collapsed {
+        .sidebar-contents-container {
+          background-color: rgba(var(--grw-highlight-100-rgb), .5);
+          backdrop-filter: blur(20px);
+        }
+      }
     }
   }
 }
@@ -92,7 +100,18 @@
     --bs-border-color: var(--grw-highlight-800);
 
     .sidebar-contents-container {
-      background-color: rgba(var(--grw-highlight-800-rgb), .5);
+      background-color: color-mix(in srgb, var(--grw-highlight-800), var(--bs-body-bg));
+    }
+  }
+  // frosted glass effect in collapsed mode
+  .grw-sidebar {
+    &:global {
+      &.grw-sidebar-collapsed {
+        .sidebar-contents-container {
+          background-color: rgba(var(--grw-highlight-800-rgb), .5);
+          backdrop-filter: blur(20px);
+        }
+      }
     }
   }
 }

+ 23 - 11
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -5,7 +5,6 @@ import React, {
 
 import dynamic from 'next/dynamic';
 
-import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { SidebarMode } from '~/interfaces/ui';
 import {
   useDrawerOpened,
@@ -15,6 +14,8 @@ import {
   useSidebarMode,
 } from '~/stores/ui';
 
+import { DrawerToggler } from '../Common/DrawerToggler';
+
 import { AppTitleOnSidebarHead, AppTitleOnSubnavigation } from './AppTitle/AppTitle';
 import { ResizableArea } from './ResizableArea/ResizableArea';
 import { SidebarHead } from './SidebarHead';
@@ -40,8 +41,8 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
   const { mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { data: currentProductNavWidth, mutateAndSave: mutateProductNavWidth } = useCurrentProductNavWidth();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
@@ -52,13 +53,11 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const resizeDoneHandler = useCallback((newWidth: number) => {
     mutateProductNavWidth(newWidth, false);
-    scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
   }, [mutateProductNavWidth]);
 
   const collapsedByResizableAreaHandler = useCallback(() => {
     mutatePreferCollapsedMode(true);
     mutateCollapsedContentsOpened(false);
-    scheduleToPut({ preferCollapsedModeByUser: true });
   }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
 
@@ -154,21 +153,29 @@ const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
 
   const { className, children } = props;
 
-  const { data: isDrawerOpened } = useDrawerOpened();
+  const { data: isDrawerOpened, mutate } = useDrawerOpened();
 
   const openClass = `${isDrawerOpened ? 'open' : ''}`;
 
   return (
-    <div className={`${className} ${openClass}`}>
-      {children}
-    </div>
+    <>
+      <div className={`${className} ${openClass}`}>
+        {children}
+      </div>
+      { isDrawerOpened && (
+        <div className="modal-backdrop fade show" onClick={() => mutate(false)} />
+      ) }
+    </>
   );
 });
 
 
 export const Sidebar = (): JSX.Element => {
 
-  const { data: sidebarMode, isCollapsedMode } = useSidebarMode();
+  const {
+    data: sidebarMode,
+    isDrawerMode, isCollapsedMode, isDockMode,
+  } = useSidebarMode();
 
   // css styles
   const grwSidebarClass = styles['grw-sidebar'];
@@ -188,7 +195,12 @@ export const Sidebar = (): JSX.Element => {
 
   return (
     <>
-      { sidebarMode != null && isCollapsedMode() && <AppTitleOnSubnavigation /> }
+      { sidebarMode != null && isDrawerMode() && (
+        <DrawerToggler className="position-fixed d-none d-md-block">
+          <span className="material-symbols-outlined">reorder</span>
+        </DrawerToggler>
+      ) }
+      { sidebarMode != null && !isDockMode() && <AppTitleOnSubnavigation /> }
       <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
         <ResizableContainer>
           { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }

+ 10 - 2
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss

@@ -28,6 +28,14 @@
   &:global {
     &.btn.btn-primary {
       @extend %btn-primary-color-vars;
+
+      --bs-btn-hover-color: color-mix(in srgb, var(
+        --grw-sidebar-nav-btn-hover-color,
+        var(
+          --grw-sidebar-nav-btn-color,
+          var(--bs-btn-color)
+        )) 90%,
+        transparent);
     }
   }
 }
@@ -35,7 +43,7 @@
   .btn-toggle-collapse {
     &:global {
       &.btn.btn-primary {
-        --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--bs-gray-500));
+        --bs-btn-color: color-mix(in srgb, var(--grw-sidebar-nav-btn-color, var(--bs-gray-500)) 50%, transparent);
         --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-300));
       }
     }
@@ -45,7 +53,7 @@
   .btn-toggle-collapse {
     &:global {
       &.btn.btn-primary {
-        --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--bs-gray-600));
+        --bs-btn-color: color-mix(in srgb, var(--grw-sidebar-nav-btn-color, var(--bs-gray-600)) 50%, transparent);
         --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
       }
     }

+ 9 - 6
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx

@@ -1,4 +1,4 @@
-import { memo, useCallback } from 'react';
+import { memo, useCallback, useMemo } from 'react';
 
 import {
   useCollapsedContentsOpened, usePreferCollapsedMode, useDrawerOpened, useSidebarMode,
@@ -10,9 +10,9 @@ import styles from './ToggleCollapseButton.module.scss';
 
 export const ToggleCollapseButton = memo((): JSX.Element => {
 
-  const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
+  const { isDrawerMode, isCollapsedMode } = useSidebarMode();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const toggleDrawer = useCallback(() => {
@@ -25,9 +25,12 @@ export const ToggleCollapseButton = memo((): JSX.Element => {
   }, [isCollapsedMode, mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
   const rotationClass = isCollapsedMode() ? 'rotate180' : '';
-  const icon = isDrawerMode() || isDockMode()
-    ? 'first_page'
-    : 'keyboard_double_arrow_left';
+  const icon = useMemo(() => {
+    if (isCollapsedMode()) {
+      return 'keyboard_double_arrow_left';
+    }
+    return 'first_page';
+  }, [isCollapsedMode]);
 
   return (
     <button

+ 1 - 3
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -2,7 +2,6 @@ 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';
 
@@ -41,13 +40,12 @@ const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
     onHover,
   } = props;
 
-  const { data: currentContents, mutate: mutateContents } = useCurrentSidebarContents();
+  const { data: currentContents, mutateAndSave: mutateContents } = useCurrentSidebarContents();
 
   const indicatorClass = useIndicator(sidebarMode, contents === currentContents);
 
   const selectThisItem = useCallback(() => {
     mutateContents(contents, false);
-    scheduleToPut({ currentSidebarContents: contents });
   }, [contents, mutateContents]);
 
   const itemClickedHandler = useCallback(() => {

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

@@ -1,4 +1,4 @@
-@use '@growi/ui/src/styles/molecules/page_list';
+@use '@growi/ui/scss/molecules/page_list';
 $grw-sidebar-content-header-height: 58px;
 $grw-sidebar-content-footer-height: 50px;
 

+ 1 - 1
apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-basic-auth-related-config');
+const logger = loggerFactory('growi:migrate:remove-basic-auth-related-config');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-presentation-configurations');
+const logger = loggerFactory('growi:migrate:remove-presentation-configurations');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -4,7 +4,7 @@ import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoos
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:migration:add-installed-date-to-config');
+const logger = loggerFactory('growi:migrate:add-installed-date-to-config');
 
 const mongoose = require('mongoose');
 

+ 34 - 0
apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js

@@ -0,0 +1,34 @@
+// eslint-disable-next-line import/no-named-as-default
+import UserUISettings from '~/server/models/user-ui-settings';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:clean-user-ui-settings-collection');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await UserUISettings.updateMany(
+      {},
+      {
+        $unset: {
+          isSidebarCollapsed: '',
+          preferDrawerModeByUser: '',
+          preferDrawerModeOnEditByUser: '',
+        },
+      },
+      { strict: false },
+    );
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // No rollback
+  },
+};

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

@@ -20,7 +20,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useLayoutFluidClassNameByPage, useEditorModeClassName } from '~/client/services/layout';
+import { useEditorModeClassName, useLayoutFluidClassNameByPage } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript'; import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -343,21 +343,21 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+
+const BasicLayoutWithEditor = ({ children }: { children?: ReactNode }): JSX.Element => {
+  const editorModeClassName = useEditorModeClassName();
+  return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
+};
+
 type LayoutProps = Props & {
   children?: ReactNode
 }
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
-  const className = useEditorModeClassName();
-
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
+  return <BasicLayoutWithEditor>{children}</BasicLayoutWithEditor>;
 };
 
 Page.getLayout = function getLayout(page: React.ReactElement<Props>) {

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

@@ -1,7 +1,7 @@
-import React, { useMemo } from 'react';
+import React, { type ReactNode, useMemo } from 'react';
 
 import type { IUserHasId } from '@growi/core';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
@@ -135,12 +135,26 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-MePage.getLayout = function getLayout(page) {
+
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
   return (
-    <BasicLayout>{page}</BasicLayout>
+    <BasicLayout>
+      {children}
+    </BasicLayout>
   );
 };
 
+MePage.getLayout = function getLayout(page) {
+  return <Layout {...page.props}>{page}</Layout>;
+};
+
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;

+ 14 - 52
apps/app/src/server/routes/attachment.js

@@ -454,10 +454,8 @@ module.exports = function(crowi, app) {
    * @apiParam {File} file
    */
   api.add = async function(req, res) {
-    let pageId = req.body.page_id || null;
+    const pageId = req.body.page_id || null;
     const pagePath = req.body.path || null;
-    const pageBody = req.body.page_body || null;
-    let pageCreated = false;
 
     // check params
     if (pageId == null && pagePath == null) {
@@ -469,67 +467,31 @@ module.exports = function(crowi, app) {
 
     const file = req.file;
 
-    let page;
-    if (pageId == null) {
-      logger.debug('Create page before file upload');
-
-      if (!isCreatablePage(pagePath)) {
-        return res.json(ApiResponse.error(`Could not use the path '${pagePath}'`));
-      }
-
-      if (isUserPage(pagePath)) {
-        const isExistUser = await User.isExistUserByUserPagePath(pagePath);
-        if (!isExistUser) {
-          return res.json(ApiResponse.error("Unable to create a page under a non-existent user's user page"));
-        }
-      }
-
-      const isAclEnabled = crowi.aclService.isAclEnabled();
-      const grant = isAclEnabled ? Page.GRANT_OWNER : Page.GRANT_PUBLIC;
-
-      page = await crowi.pageService.create(pagePath, pageBody ?? '', req.user, { grant });
-      pageCreated = true;
-      pageId = page._id;
-    }
-    else {
-      page = await Page.findById(pageId);
+    try {
+      const page = await Page.findById(pageId);
 
       // check the user is accessible
       const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
       if (!isAccessible) {
         return res.json(ApiResponse.error(`Forbidden to access to the page '${page.id}'`));
       }
-    }
 
-    let attachment;
-    try {
-      attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
+      const attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
+
+      const result = {
+        page: serializePageSecurely(page),
+        revision: serializeRevisionSecurely(page.revision),
+        attachment: attachment.toObject({ virtuals: true }),
+      };
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
+
+      res.json(ApiResponse.success(result));
     }
     catch (err) {
       logger.error(err);
       return res.json(ApiResponse.error(err.message));
     }
-
-    const result = {
-      page: serializePageSecurely(page),
-      revision: serializeRevisionSecurely(page.revision),
-      attachment: attachment.toObject({ virtuals: true }),
-      pageCreated,
-    };
-
-    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
-
-    res.json(ApiResponse.success(result));
-
-    if (pageCreated) {
-      // global notification
-      try {
-        await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, page, req.user);
-      }
-      catch (err) {
-        logger.error('Create notification failed', err);
-      }
-    }
   };
 
   /**

+ 2 - 0
apps/app/src/stores/page-listing.tsx

@@ -204,6 +204,8 @@ export const useSWRxPageChildren = (
     }),
     {
       keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnRecconect: false,
     },
   );
 };

+ 90 - 29
apps/app/src/stores/ui.tsx

@@ -3,18 +3,19 @@ import {
 } from 'react';
 
 import { PageGrant, type Nullable } from '@growi/core';
-import { type SWRResponseWithUtils, useSWRStatic } from '@growi/core/dist/swr';
+import { type SWRResponseWithUtils, useSWRStatic, withUtils } from '@growi/core/dist/swr';
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
 import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, type SWRResponse, type Key,
+  useSWRConfig, type SWRResponse, type Key, KeyedMutator, MutatorOptions,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import type { IFocusable } from '~/client/interfaces/focusable';
+import { scheduleToPut } from '~/client/services/user-ui-settings';
 import type { IPageGrantData } from '~/interfaces/page';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
@@ -95,8 +96,6 @@ export const EditorModeHash = {
 } as const;
 export type EditorModeHash = typeof EditorModeHash[keyof typeof EditorModeHash];
 
-export const isEditorModeHash = (hash: string): hash is EditorModeHash => Object.values<string>(EditorModeHash).includes(hash);
-
 const updateHashByEditorMode = (newEditorMode: EditorMode) => {
   const { pathname, search } = window.location;
 
@@ -166,8 +165,8 @@ export const useEditorMode = (): SWRResponseWithUtils<EditorModeUtils, EditorMod
   });
 };
 
-export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
-  const key: Key = isClient() ? 'isDeviceSmallerThanMd' : null;
+export const useIsDeviceLargerThanMd = (): SWRResponse<boolean, Error> => {
+  const key: Key = isClient() ? 'isDeviceLargerThanMd' : null;
 
   const { cache, mutate } = useSWRConfig();
 
@@ -176,13 +175,13 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
       const mdOrAvobeHandler = function(this: MediaQueryList): void {
         // sm -> md: matches will be true
         // md -> sm: matches will be false
-        mutate(key, !this.matches);
+        mutate(key, this.matches);
       };
       const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
 
       // initialize
       if (cache.get(key)?.data == null) {
-        cache.set(key, { ...cache.get(key), data: !mql.matches });
+        cache.set(key, { ...cache.get(key), data: mql.matches });
       }
 
       return () => {
@@ -191,11 +190,11 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
     }
   }, [cache, key, mutate]);
 
-  return useStaticSWR(key);
+  return useSWRStatic(key);
 };
 
-export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
-  const key: Key = isClient() ? 'isDeviceSmallerThanLg' : null;
+export const useIsDeviceLargerThanLg = (): SWRResponse<boolean, Error> => {
+  const key: Key = isClient() ? 'isDeviceLargerThanLg' : null;
 
   const { cache, mutate } = useSWRConfig();
 
@@ -204,13 +203,13 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
       const lgOrAvobeHandler = function(this: MediaQueryList): void {
         // md -> lg: matches will be true
         // lg -> md: matches will be false
-        mutate(key, !this.matches);
+        mutate(key, this.matches);
       };
       const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
 
       // initialize
       if (cache.get(key)?.data == null) {
-        cache.set(key, { ...cache.get(key), data: !mql.matches });
+        cache.set(key, { ...cache.get(key), data: mql.matches });
       }
 
       return () => {
@@ -219,30 +218,92 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
     }
   }, [cache, key, mutate]);
 
-  return useStaticSWR(key);
+  return useSWRStatic(key);
 };
 
+export const useIsDeviceLargerThanXl = (): SWRResponse<boolean, Error> => {
+  const key: Key = isClient() ? 'isDeviceLargerThanXl' : null;
+
+  const { cache, mutate } = useSWRConfig();
 
-export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
-  return useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+  useEffect(() => {
+    if (key != null) {
+      const xlOrAvobeHandler = function(this: MediaQueryList): void {
+        // lg -> xl: matches will be true
+        // xl -> lg: matches will be false
+        mutate(key, this.matches);
+      };
+      const mql = addBreakpointListener(Breakpoint.XL, xlOrAvobeHandler);
+
+      // initialize
+      if (cache.get(key)?.data == null) {
+        cache.set(key, { ...cache.get(key), data: mql.matches });
+      }
+
+      return () => {
+        cleanupBreakpointListener(mql, xlOrAvobeHandler);
+      };
+    }
+  }, [cache, key, mutate]);
+
+  return useSWRStatic(key);
 };
 
-export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
-  return useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+
+type MutateAndSaveUserUISettings<Data> = (data: Data, opts?: boolean | MutatorOptions<Data>) => Promise<Data | undefined>;
+type MutateAndSaveUserUISettingsUtils<Data> = {
+  mutateAndSave: MutateAndSaveUserUISettings<Data>;
+}
+
+export const useCurrentSidebarContents = (
+    initialData?: SidebarContentsType,
+): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<SidebarContentsType>, SidebarContentsType> => {
+  const swrResponse = useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<SidebarContentsType> = useCallback((data, opts?) => {
+    scheduleToPut({ currentSidebarContents: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+export const useCurrentProductNavWidth = (initialData?: number): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
+  const swrResponse = useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback((data, opts?) => {
+    scheduleToPut({ currentProductNavWidth: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const usePreferCollapsedMode = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+export const usePreferCollapsedMode = (initialData?: boolean): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<boolean>, boolean> => {
+  const swrResponse = useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<boolean> = useCallback((data, opts?) => {
+    scheduleToPut({ preferCollapsedModeByUser: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean, Error> => {
+export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean> => {
   return useSWRStatic('isCollapsedContentsOpened', initialData, { fallbackData: false });
 };
 
+export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+};
+
 type DetectSidebarModeUtils = {
   isDrawerMode(): boolean
   isCollapsedMode(): boolean
@@ -250,28 +311,28 @@ type DetectSidebarModeUtils = {
 }
 
 export const useSidebarMode = (): SWRResponseWithUtils<DetectSidebarModeUtils, SidebarMode> => {
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isDeviceLargerThanXl } = useIsDeviceLargerThanXl();
   const { data: editorMode } = useEditorMode();
   const { data: isCollapsedModeUnderDockMode } = usePreferCollapsedMode();
 
-  const condition = isDeviceSmallerThanMd != null && editorMode != null && isCollapsedModeUnderDockMode != null;
+  const condition = isDeviceLargerThanXl != null && editorMode != null && isCollapsedModeUnderDockMode != null;
 
   const isEditorMode = editorMode === EditorMode.Editor;
 
   const fetcher = useCallback((
-      [, isDeviceSmallerThanMd, isEditorMode, isCollapsedModeUnderDockMode]: [Key, boolean|undefined, boolean|undefined, boolean|undefined],
+      [, isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode]: [Key, boolean|undefined, boolean|undefined, boolean|undefined],
   ) => {
-    if (isDeviceSmallerThanMd) {
+    if (!isDeviceLargerThanXl) {
       return SidebarMode.DRAWER;
     }
     return isEditorMode || isCollapsedModeUnderDockMode ? SidebarMode.COLLAPSED : SidebarMode.DOCK;
   }, []);
 
   const swrResponse = useSWRImmutable(
-    condition ? ['sidebarMode', isDeviceSmallerThanMd, isEditorMode, isCollapsedModeUnderDockMode] : null,
+    condition ? ['sidebarMode', isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode] : null,
     // calcDrawerMode,
     fetcher,
-    { fallbackData: fetcher(['sidebarMode', isDeviceSmallerThanMd, isEditorMode, isCollapsedModeUnderDockMode]) },
+    { fallbackData: fetcher(['sidebarMode', isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode]) },
   );
 
   const _isDrawerMode = useCallback(() => swrResponse.data === SidebarMode.DRAWER, [swrResponse.data]);

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

@@ -63,22 +63,6 @@
   }
 }
 
-.grw-side-contents-container {
-  margin-bottom: 1rem;
-
-  @include bs.media-breakpoint-up(lg) {
-    width: 250px;
-    min-width: 250px;
-    margin-left: 30px;
-  }
-}
-
-.grw-side-contents-sticky-container {
-  position: sticky;
-  // growisubnavigation + grw-navbar-boder + some spacing
-  top: calc(100px + 4px + 20px);
-}
-
 // printable style
 @media print {
   body {

+ 1 - 1
apps/app/src/styles/_variables.scss

@@ -8,7 +8,7 @@ $grw-marker-green: #6f6;
 //== Layout
 $grw-sidebar-nav-width: 48px;
 
-$grw-navbar-bottom-height: 48px;
+$grw-navbar-bottom-height: 62px;
 $grw-editor-navbar-bottom-height: 48px;
 
 $grw-scroll-margin-top-in-view: 130px;

+ 0 - 16
apps/app/src/styles/molecules/_page-accessories-control.scss

@@ -1,16 +0,0 @@
-%grw-page-accessories-control {
-  .grw-btn-page-accessories {
-    padding-right: 1rem;
-    padding-left: 1rem;
-
-    svg {
-      width: 16px;
-      height: 16px;
-    }
-  }
-  .grw-page-accessories-control-icon {
-    display: flex;
-    justify-content: center;
-    width: 20px;
-  }
-}

+ 2 - 2
apps/app/src/utils/logger/index.ts

@@ -1,11 +1,11 @@
 import Logger from 'bunyan';
-import { createLogger } from 'universal-bunyan';
+import { createLogger, type UniversalBunyanConfig } from 'universal-bunyan';
 
 import configForDev from '^/config/logger/config.dev';
 import configForProd from '^/config/logger/config.prod';
 
 const isProduction = process.env.NODE_ENV === 'production';
-const config = isProduction ? configForProd : configForDev;
+const config = (isProduction ? configForProd : configForDev) as UniversalBunyanConfig;
 
 const loggerFactory = function(name: string): Logger {
   return createLogger({

+ 8 - 1
apps/app/tsconfig.json

@@ -15,7 +15,14 @@
       "~/*": ["./src/*"],
       "^/*": ["./*"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "next-env.d.ts",

+ 4 - 2
apps/slackbot-proxy/package.json

@@ -17,8 +17,10 @@
     "start:prod": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/index.js",
     "postbuild": "yarn cp:public && yarn cp:views && yarn cp:bootstrap",
     "predev": "yarn cp:bootstrap:dev",
-    "lint": "yarn eslint src --ext .ts",
-    "lint:fix": "yarn eslint src --ext .ts --fix",
+    "lint:js": "yarn eslint src/**/*.{js,ts}",
+    "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
+    "lint:typecheck": "tsc",
+    "lint": "run-p lint:*",
     "version": "yarn version --no-git-tag-version --preid=slackbot-proxy"
   },
   "// comments for dependencies": {

+ 8 - 1
apps/slackbot-proxy/tsconfig.json

@@ -7,7 +7,14 @@
     "baseUrl": ".",
     "paths": {
       "~/*": ["./src/*"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 1 - 1
package.json

@@ -75,7 +75,7 @@
     "glob": "^8.1.0",
     "mock-require": "^3.0.3",
     "path-browserify": "^1.0.1",
-    "postcss": "^8.4.5",
+    "postcss": "^8.4.31",
     "postcss-scss": "^4.0.3",
     "reg-keygen-git-hash-plugin": "^0.11.1",
     "reg-notify-github-plugin": "^0.11.1",

+ 1 - 1
packages/core/src/remark-plugins/util/option-parser.ts

@@ -36,7 +36,7 @@ export class OptionParser {
 
     // determine start
     let start;
-    let end;
+    let end = -1;
 
     // has operator
     if (match[3] != null) {

+ 0 - 7
packages/core/src/utils/objectid-utils.ts

@@ -1,12 +1,5 @@
 import ObjectId from 'bson-objectid';
 
-import { isServer } from './browser-utils';
-
-// Workaround to avoid https://github.com/williamkapke/bson-objectid/issues/50
-if (isServer()) {
-  global._Buffer = Buffer;
-}
-
 export function isValidObjectId(id: string | ObjectId | null | undefined): boolean {
   if (id == null) {
     return false;

+ 4 - 4
packages/core/src/utils/page-utils.ts

@@ -1,3 +1,5 @@
+import { IPage } from '..';
+
 import { isTopPage } from './page-path-utils/is-top-page';
 
 // const GRANT_PUBLIC = 1;
@@ -14,8 +16,7 @@ const STATUS_DELETED = 'deleted';
  * @param page Page
  * @returns boolean
  */
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const isOnTree = (page): boolean => {
+export const isOnTree = (page: IPage): boolean => {
   const { path, parent } = page;
 
   if (isTopPage(path)) {
@@ -38,8 +39,7 @@ export const isOnTree = (page): boolean => {
  * @param page PageDocument
  * @returns boolean
  */
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const isPageNormalized = (page): boolean => {
+export const isPageNormalized = (page: IPage): boolean => {
   const { grant, status } = page;
 
   if (grant === GRANT_RESTRICTED || grant === GRANT_SPECIFIED) {

+ 1 - 0
packages/editor/package.json

@@ -32,6 +32,7 @@
     "@uiw/react-codemirror": "^4.21.8",
     "bootstrap": "^5.3.1",
     "codemirror": "^6.0.1",
+    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",
     "react-dropzone": "^14.2.3",
     "react-hook-form": "^7.45.4",

+ 1 - 0
packages/editor/src/@types/emoji-mart.d.ts

@@ -0,0 +1 @@
+declare module 'emoji-mart';

+ 3 - 0
packages/editor/src/@types/declaration.d.ts → packages/editor/src/@types/scss.d.ts

@@ -1,2 +1,5 @@
 // prevent TS2307: Cannot find module './xxx.module.scss' or its corresponding type declarations.
 declare module '*.scss';
+
+// prevent TS7016: Could not find a declaration file for module 'emoji-mart'.
+declare module 'emoji-mart';

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

@@ -124,7 +124,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
       <div {...getRootProps()} className={`dropzone ${fileUploadState} flex-expand-vert`}>
         {renderOverlay()}
         <CodeMirrorEditorContainer ref={containerRef} />
-        <Toolbar onFileOpen={open} acceptedFileType={acceptedFileType} />
+        <Toolbar editorKey={editorKey} onFileOpen={open} acceptedFileType={acceptedFileType} />
       </div>
     </div>
   );

+ 146 - 4
packages/editor/src/components/CodeMirrorEditor/Toolbar/EmojiButton.tsx

@@ -1,7 +1,149 @@
-export const EmojiButton = (): JSX.Element => {
+import {
+  FC, useState, useCallback, CSSProperties,
+} from 'react';
+
+import { Picker } from 'emoji-mart';
+import i18n from 'i18next';
+import { Modal } from 'reactstrap';
+
+import { useCodeMirrorEditorIsolated, useResolvedThemeForEditor } from '../../../stores';
+
+import 'emoji-mart/css/emoji-mart.css';
+
+type Props = {
+  editorKey: string,
+}
+
+type Translation = {
+  search: string
+  clear: string
+  notfound: string
+  skintext: string
+  categories: object
+  categorieslabel: string
+  skintones: object
+  title: string
+}
+
+// TODO: https://redmine.weseek.co.jp/issues/133681
+const getEmojiTranslation = (): Translation => {
+
+  const categories: { [key: string]: string } = {};
+  [
+    'search',
+    'recent',
+    'smileys',
+    'people',
+    'nature',
+    'foods',
+    'activity',
+    'places',
+    'objects',
+    'symbols',
+    'flags',
+    'custom',
+  ].forEach((category) => {
+    categories[category] = i18n.t(`emoji.categories.${category}`);
+  });
+
+  const skintones: { [key: string]: string} = {};
+  (Array.from(Array(6).keys())).forEach((tone) => {
+    skintones[tone + 1] = i18n.t(`emoji.skintones.${tone + 1}`);
+  });
+
+  const translation = {
+    search: i18n.t('emoji.search'),
+    clear: i18n.t('emoji.clear'),
+    notfound: i18n.t('emoji.notfound'),
+    skintext: i18n.t('emoji.skintext'),
+    categories,
+    categorieslabel: i18n.t('emoji.categorieslabel'),
+    skintones,
+    title: i18n.t('emoji.title'),
+  };
+
+  return translation;
+};
+
+const translation = getEmojiTranslation();
+
+export const EmojiButton: FC<Props> = (props) => {
+  const { editorKey } = props;
+
+  const [isOpen, setIsOpen] = useState(false);
+
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
+  const { data: resolvedTheme } = useResolvedThemeForEditor();
+
+  const view = codeMirrorEditor?.view;
+  const cursorIndex = view?.state.selection.main.head;
+  const toggle = () => setIsOpen(!isOpen);
+
+  const selectEmoji = useCallback((emoji: { colons: string }): void => {
+
+    if (cursorIndex == null || !isOpen) {
+      return;
+    }
+
+    view?.dispatch({
+      changes: {
+        from: cursorIndex,
+        insert: emoji.colons,
+      },
+    });
+
+    toggle();
+  }, [cursorIndex, isOpen, toggle, view]);
+
+  const setStyle = useCallback((): CSSProperties => {
+    if (view == null || cursorIndex == null || !isOpen) {
+      return {};
+    }
+
+    const offset = 20;
+    const emojiPickerHeight = 420;
+    const cursorRect = view.coordsAtPos(cursorIndex);
+    const editorRect = view.dom.getBoundingClientRect();
+
+    if (cursorRect == null) {
+      return {};
+    }
+
+    // Emoji Picker bottom position exceed editor's bottom position
+    if (cursorRect.bottom + emojiPickerHeight > editorRect.bottom) {
+      return {
+        top: editorRect.bottom - emojiPickerHeight,
+        left: cursorRect.left + offset,
+        position: 'fixed',
+      };
+    }
+    return {
+      top: cursorRect.top + offset,
+      left: cursorRect.left + offset,
+      position: 'fixed',
+    };
+  }, [cursorIndex, isOpen, view]);
+
   return (
-    <button type="button" className="btn btn-toolbar-button">
-      <span className="material-symbols-outlined fs-5">emoji_emotions</span>
-    </button>
+    <>
+      <button type="button" className="btn btn-toolbar-button" onClick={toggle}>
+        <span className="material-symbols-outlined fs-5">emoji_emotions</span>
+      </button>
+      { isOpen
+      && (
+        <div className="mb-2 d-none d-md-block">
+          <Modal isOpen={isOpen} toggle={toggle} backdropClassName="emoji-picker-modal" fade={false}>
+            <Picker
+              onSelect={selectEmoji}
+              i18n={translation}
+              title={translation.title}
+              emojiTooltip
+              style={setStyle()}
+              theme={resolvedTheme}
+            />
+          </Modal>
+        </div>
+      )}
+    </>
   );
 };

+ 8 - 3
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -1,5 +1,7 @@
 import { memo } from 'react';
 
+import { AcceptedUploadFileType } from '../../../consts';
+
 import { AttachmentsDropup } from './AttachmentsDropup';
 import { DiagramButton } from './DiagramButton';
 import { EmojiButton } from './EmojiButton';
@@ -7,23 +9,26 @@ import { TableButton } from './TableButton';
 import { TemplateButton } from './TemplateButton';
 import { TextFormatTools } from './TextFormatTools';
 
-import { AcceptedUploadFileType } from 'src/consts';
 
 import styles from './Toolbar.module.scss';
 
 type Props = {
+  editorKey: string,
   onFileOpen: () => void,
   acceptedFileType: AcceptedUploadFileType
 }
 
 export const Toolbar = memo((props: Props): JSX.Element => {
 
-  const { onFileOpen, acceptedFileType } = props;
+  const { editorKey, onFileOpen, acceptedFileType } = props;
+
   return (
     <div className={`d-flex gap-2 p-2 codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
       <AttachmentsDropup onFileOpen={onFileOpen} acceptedFileType={acceptedFileType} />
       <TextFormatTools />
-      <EmojiButton />
+      <EmojiButton
+        editorKey={editorKey}
+      />
       <TableButton />
       <DiagramButton />
       <TemplateButton />

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

@@ -13,7 +13,6 @@ const additionalExtensions: Extension[] = [
   scrollPastEnd(),
 ];
 
-
 type Props = {
   onChange?: (value: string) => void,
   onSave?: () => void,
@@ -60,6 +59,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     return cleanupFunction;
   }, [codeMirrorEditor, onSave]);
 
+
   return (
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}

+ 9 - 2
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -10,6 +10,8 @@ import { tags } from '@lezer/highlight';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 import deepmerge from 'ts-deepmerge';
 
+import { emojiAutocompletionSettings } from '../../extensions/emojiAutocompletionSettings';
+
 import { useAppendExtensions, type AppendExtensions } from './utils/append-extensions';
 import { useFocus, type Focus } from './utils/focus';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
@@ -43,20 +45,25 @@ export type UseCodeMirrorEditor = {
 
 
 const defaultExtensions: Extension[] = [
+  EditorView.lineWrapping,
   markdown({ base: markdownLanguage, codeLanguages: languages }),
   keymap.of([indentWithTab]),
   Prec.lowest(keymap.of(defaultKeymap)),
   syntaxHighlighting(markdownHighlighting),
   Prec.lowest(syntaxHighlighting(defaultHighlightStyle)),
+  emojiAutocompletionSettings,
 ];
 
+
 export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor => {
 
-  const mergedProps = useMemo<UseCodeMirror>(() => {
+  const mergedProps = useMemo(() => {
     return deepmerge(
       props ?? {},
       {
-        extensions: defaultExtensions,
+        extensions: [
+          defaultExtensions,
+        ],
         // Reset settings of react-codemirror.
         // Extensions are defined first will be used if they have the same priority.
         // If extensions conflict, disable them here.

+ 75 - 0
packages/editor/src/services/extensions/emojiAutocompletionSettings.ts

@@ -0,0 +1,75 @@
+import { type CompletionContext, type Completion, autocompletion } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
+import { emojiIndex } from 'emoji-mart';
+import emojiData from 'emoji-mart/data/all.json';
+
+const getEmojiDataArray = (): string[] => {
+  const rawEmojiDataArray = emojiData.categories;
+
+  const emojiCategoriesData = [
+    'people',
+    'nature',
+    'foods',
+    'activity',
+    'places',
+    'objects',
+    'symbols',
+    'flags',
+  ];
+
+  const fixedEmojiDataArray: string[] = [];
+
+  emojiCategoriesData.forEach((value) => {
+    const tempArray = rawEmojiDataArray.find(obj => obj.id === value)?.emojis;
+
+    if (tempArray == null) {
+      return;
+    }
+
+    fixedEmojiDataArray.push(...tempArray);
+  });
+
+  return fixedEmojiDataArray;
+};
+
+const emojiDataArray = getEmojiDataArray();
+
+const emojiOptions = emojiDataArray.map(
+  tag => ({ label: `:${tag}:`, type: tag }),
+);
+
+const TWO_OR_MORE_WORD_CHARACTERS_REGEX = /:\w{2,}$/;
+
+
+// EmojiAutocompletion is activated when two characters are entered into the editor.
+const emojiAutocompletion = (context: CompletionContext) => {
+  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+  const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
+  const emojiBefore = TWO_OR_MORE_WORD_CHARACTERS_REGEX.exec(textBefore);
+
+  if (!emojiBefore && !context.explicit) return null;
+
+  return {
+    from: emojiBefore ? nodeBefore.from + emojiBefore.index : context.pos,
+    options: emojiOptions,
+    validFor: TWO_OR_MORE_WORD_CHARACTERS_REGEX,
+  };
+};
+
+export const emojiAutocompletionSettings = autocompletion({
+  addToOptions: [{
+    render: (completion: Completion) => {
+      const emojiName = completion.type ?? '';
+      const emojiData = emojiIndex.emojis[emojiName];
+
+      const emoji = emojiData.native ?? emojiData[1].native;
+
+      const element = document.createElement('span');
+      element.innerHTML = emoji;
+      return element;
+    },
+    position: 20,
+  }],
+  icons: false,
+  override: [emojiAutocompletion],
+});

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

@@ -1 +1,2 @@
 export * from './codemirror-editor';
+export * from './use-resolved-theme';

+ 27 - 0
packages/editor/src/stores/use-resolved-theme.ts

@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import { ColorScheme } from '@growi/core';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
+import { mutate } from 'swr';
+
+type ResolvedThemeStatus = {
+  themeData: ColorScheme,
+}
+
+type ResolvedThemeUtils = {
+  mutateResolvedThemeForEditor(resolvedTheme: ColorScheme): void
+}
+
+export const useResolvedThemeForEditor = (): SWRResponse<ResolvedThemeStatus, Error> & ResolvedThemeUtils => {
+  const swrResponse = useSWRStatic<ResolvedThemeStatus, Error>('resolvedTheme');
+
+  const mutateResolvedThemeForEditor = useCallback((resolvedTheme: ColorScheme) => {
+    mutate('resolvedTheme', { themeData: resolvedTheme });
+  }, []);
+
+  return {
+    ...swrResponse,
+    mutateResolvedThemeForEditor,
+  };
+};

+ 3 - 0
packages/editor/vite.config.ts

@@ -50,6 +50,9 @@ export default defineConfig({
         preserveModules: true,
         preserveModulesRoot: 'src',
       },
+      external: [
+        'emoji-mart/css/emoji-mart.css',
+      ],
     },
   },
 });

+ 1 - 1
packages/pluginkit/src/v4/server/utils/template/scan.ts

@@ -52,7 +52,7 @@ export const scanTemplate = async(
         id: templateId,
         locale,
         isValid: false,
-        invalidReason: err.message,
+        invalidReason: (err as Error).message,
       });
     }
   }

+ 4 - 1
packages/presentation/tsconfig.json

@@ -6,7 +6,10 @@
 
     "baseUrl": ".",
     "paths": {
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "noImplicitAny": false
   },
   "include": [
     "src"

+ 75 - 1
packages/preset-themes/src/styles/default.scss

@@ -1,4 +1,78 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
+:root[data-bs-theme='light'] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
+
+  $primary: #007eb0;
+  $highlight: #c4c2bd;
+
+  @include generate-color-palette('primary', $primary, #000010, white, 22%, 22%);
+  @include generate-color-palette('highlight', $highlight);
+
+  $body-color:                #223246;
+  $body-bg:                   white;
+
+  $body-secondary-color:      rgba($body-color, .75);
+  $body-secondary-bg:         $gray-200;
+
+  $body-tertiary-color:       rgba($body-color, .5);
+  $body-tertiary-bg:          $gray-100;
+
+  $border-color:              var(--grw-highlight-200);
+
+  $link-color:                #434240;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/root';
+  @import '@growi/core/scss/bootstrap/theming/root-light';
+  @import '@growi/core/scss/bootstrap/theming/apply';
+
+  --grw-wiki-link-color-rgb: var(--grw-highlight-800-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-highlight-900-rgb);
+  --grw-sidebar-nav-btn-color: var(--grw-highlight-600);
+}
+
+:root[data-bs-theme='dark'] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
+
+  $primary: #007eb0;
+  $highlight: #c4c2bd;
+
+  @include generate-color-palette('primary', $primary, #000010, white, 22%, 22%);
+  @include generate-color-palette('highlight', $highlight, black, white);
+
+  $body-color-dark:                   $gray-300;
+  $body-bg-dark:                      #1c1a1a;
+
+  $body-secondary-color-dark:         rgba($body-color-dark, .75);
+  $body-secondary-bg-dark:            $gray-800;
+
+  $body-tertiary-color-dark:          rgba($body-color-dark, .5);
+  $body-tertiary-bg-dark:             mix($gray-800, $gray-900, 50%);
+
+  $border-color-dark:                 var(--grw-highlight-200);
+
+  $link-color-dark:                   $gray-500;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/root';
+  @import '@growi/core/scss/bootstrap/theming/root-dark';
+  @import '@growi/core/scss/bootstrap/theming/apply';
+
+  --grw-wiki-link-color-rgb: var(--grw-highlight-500-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-highlight-300-rgb);
+  --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
+}
 
 // @use './variables' as var;
 // @use './theme/mixins/page-editor-mode-manager';

+ 8 - 1
packages/remark-attachment-refs/tsconfig.json

@@ -6,7 +6,14 @@
     "baseUrl": ".",
     "paths": {
       "~/*": ["./src/*"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 8 - 1
packages/remark-drawio/tsconfig.json

@@ -2,7 +2,14 @@
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "jsx": "react-jsx"
+    "jsx": "react-jsx",
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 1 - 1
packages/remark-lsx/src/client/components/LsxPageList/LsxListView.module.scss

@@ -1,4 +1,4 @@
-@use '@growi/ui/src/styles/molecules/page_list';
+@use '@growi/ui/scss/molecules/page_list';
 
 .page-list :global {
   .page-list-ul > li > a:not(:hover) {

+ 12 - 3
packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 
 import { pathUtils } from '@growi/core/dist/utils';
-import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components/PagePath';
+import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import Link from 'next/link';
 
 import type { PageNode } from '../../../interfaces/page-node';
@@ -97,8 +97,17 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
     if (pageNode.page == null) {
       return <></>;
     }
-    return <PageListMeta page={pageNode.page} basisViewersCount={basisViewersCount} />;
-  }, [basisViewersCount, pageNode.page]);
+
+    const { page } = pageNode;
+
+    return (
+      <PageListMeta
+        page={page}
+        basisViewersCount={basisViewersCount}
+        likerCount={page.liker.length}
+      />
+    );
+  }, [basisViewersCount, pageNode]);
 
   return (
     <li className={`page-list-li ${styles['page-list-li']}`}>

+ 1 - 1
packages/remark-lsx/src/client/stores/lsx/lsx.ts

@@ -27,7 +27,7 @@ export const useSWRxLsx = (
           : null;
       }
       catch (err) {
-        parseError = err;
+        parseError = err as Error;
       }
 
       // the first loading

+ 8 - 1
packages/remark-lsx/tsconfig.json

@@ -6,7 +6,14 @@
 
     "types": [
       "vitest/globals"
-    ]
+    ],
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

Некоторые файлы не были показаны из-за большого количества измененных файлов