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

Merge branch 'dev/7.0.x' into imprv/141062-141361-truncate-page-path-when-editing

reiji-h 2 лет назад
Родитель
Сommit
4489f7ebb3
82 измененных файлов с 463 добавлено и 266 удалено
  1. 0 1
      apps/app/next-env.d.ts
  2. 10 10
      apps/app/resource/locales/en_US/sandbox-bootstrap5.md
  3. 2 2
      apps/app/resource/locales/ja_JP/sandbox.md
  4. 1 1
      apps/app/src/components/Admin/App/QuestionnaireSettings.tsx
  5. 3 2
      apps/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx
  6. 1 1
      apps/app/src/components/Admin/AuditLogManagement.tsx
  7. 1 1
      apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  8. 1 1
      apps/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  9. 2 1
      apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx
  10. 3 3
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  11. 1 1
      apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  12. 8 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  13. 1 1
      apps/app/src/components/Admin/Users/PasswordResetModal.jsx
  14. 1 1
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  15. 0 17
      apps/app/src/components/Common/PageViewLayout.module.scss
  16. 3 4
      apps/app/src/components/Common/PageViewLayout.tsx
  17. 1 1
      apps/app/src/components/DescendantsPageList.tsx
  18. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationList.tsx
  19. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationPage.tsx
  20. 1 1
      apps/app/src/components/InfiniteScroll.tsx
  21. 1 1
      apps/app/src/components/InstallerForm.tsx
  22. 1 2
      apps/app/src/components/InvitedForm.tsx
  23. 1 1
      apps/app/src/components/Layout/AdminLayout.tsx
  24. 0 7
      apps/app/src/components/LoadingSpinner.tsx
  25. 2 2
      apps/app/src/components/LoginForm.tsx
  26. 7 7
      apps/app/src/components/Me/AssociateModal.tsx
  27. 1 2
      apps/app/src/components/Me/QuestionnaireSettings.tsx
  28. 4 0
      apps/app/src/components/Navbar/GroundGlassBar.module.scss
  29. 17 0
      apps/app/src/components/Navbar/GroundGlassBar.tsx
  30. 0 5
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  31. 4 2
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  32. 0 8
      apps/app/src/components/Navbar/GrowiNavbarBottom.module.scss
  33. 4 2
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  34. 7 4
      apps/app/src/components/Page/DisplaySwitcher.tsx
  35. 1 2
      apps/app/src/components/Page/RevisionLoader.tsx
  36. 1 1
      apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx
  37. 20 8
      apps/app/src/components/PageAlert/OldRevisionAlert.tsx
  38. 1 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  39. 1 1
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  40. 8 7
      apps/app/src/components/PageControls/PageControls.tsx
  41. 1 1
      apps/app/src/components/PageEditor/DrawioModal.tsx
  42. 1 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss
  43. 6 37
      apps/app/src/components/PageEditor/PageEditor.tsx
  44. 62 0
      apps/app/src/components/PageEditor/PageEditorReadOnly.tsx
  45. 44 2
      apps/app/src/components/PageEditor/ScrollSyncHelper.tsx
  46. 1 1
      apps/app/src/components/PageList/PageList.tsx
  47. 1 1
      apps/app/src/components/PagePresentationModal.tsx
  48. 1 1
      apps/app/src/components/PrivateLegacyPages.tsx
  49. 1 1
      apps/app/src/components/SavePageControls.tsx
  50. 27 9
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  51. 1 1
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  52. 1 1
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  53. 1 1
      apps/app/src/components/TreeItem/SimpleItem.tsx
  54. 2 2
      apps/app/src/pages/_private-legacy-pages.page.tsx
  55. 6 4
      apps/app/src/pages/_search.page.tsx
  56. 2 2
      apps/app/src/pages/forgot-password-errors.page.tsx
  57. 6 7
      apps/app/src/pages/forgot-password.page.tsx
  58. 1 1
      apps/app/src/pages/maintenance.page.tsx
  59. 10 11
      apps/app/src/pages/me/[[...path]].page.tsx
  60. 2 2
      apps/app/src/pages/reset-password.page.tsx
  61. 0 2
      apps/app/src/pages/share/[[...path]].page.tsx
  62. 34 26
      apps/app/src/pages/tags.page.tsx
  63. 12 13
      apps/app/src/pages/trash.page.tsx
  64. 20 0
      apps/app/src/styles/_layout.scss
  65. 2 0
      apps/app/src/styles/vendor.scss
  66. 2 0
      apps/app/turbo.json
  67. 1 0
      packages/custom-icons/svg/facebook.svg
  68. 1 0
      packages/custom-icons/svg/github.svg
  69. 1 0
      packages/custom-icons/svg/google.svg
  70. 1 0
      packages/custom-icons/svg/slack.svg
  71. 9 5
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  72. 39 0
      packages/editor/src/components/CodeMirrorEditorReadOnly.tsx
  73. 1 0
      packages/editor/src/components/index.ts
  74. 1 0
      packages/editor/src/consts/global-code-mirror-editor-key.ts
  75. 17 15
      packages/editor/src/services/paste-util/paste-markdown-util.ts
  76. 3 1
      packages/remark-attachment-refs/package.json
  77. 2 2
      packages/remark-attachment-refs/src/client/components/AttachmentList.tsx
  78. 3 1
      packages/remark-lsx/src/client/components/Lsx.tsx
  79. 3 0
      packages/ui/package.json
  80. 1 0
      packages/ui/src/components/LoadingSpinner.module.scss
  81. 9 0
      packages/ui/src/components/LoadingSpinner.tsx
  82. 1 0
      packages/ui/src/components/index.ts

+ 0 - 1
apps/app/next-env.d.ts

@@ -1,6 +1,5 @@
 /// <reference types="next" />
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
 /// <reference types="next/image-types/global" />
-/// <reference types="next/navigation-types/compat/navigation" />
 
 
 // NOTE: This file should not be edited
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/basic-features/typescript for more information.
 // see https://nextjs.org/docs/basic-features/typescript for more information.

+ 10 - 10
apps/app/resource/locales/en_US/sandbox-bootstrap5.md

@@ -120,19 +120,19 @@
 
 
 
 
 # 4. Colors
 # 4. Colors
-## 背景颜色
+## Contextual colors
 <p class="text-primary">Look, I'm in a well!</p>
 <p class="text-primary">Look, I'm in a well!</p>
 <p class="text-warning">Look, I'm in a well!</p>
 <p class="text-warning">Look, I'm in a well!</p>
 <p class="text-danger">Look, I'm in a well!</p>
 <p class="text-danger">Look, I'm in a well!</p>
 
 
-## 背景情况
+## Contextual backgrounds
 <p class="text-danger bg-primary">Look, I'm in a well!</p>
 <p class="text-danger bg-primary">Look, I'm in a well!</p>
 <p class="text-primary bg-warning">Look, I'm in a well!</p>
 <p class="text-primary bg-warning">Look, I'm in a well!</p>
 <p class="text-warning bg-danger">Look, I'm in a well!</p>
 <p class="text-warning bg-danger">Look, I'm in a well!</p>
 
 
 
 
 # 5. Collapse
 # 5. Collapse
-## 显示内容
+## Displaying content
 <a class="btn btn-primary text-white" data-bs-toggle="collapse" href="#collapse-1">
 <a class="btn btn-primary text-white" data-bs-toggle="collapse" href="#collapse-1">
   Show content
   Show content
 </a>
 </a>
@@ -146,7 +146,7 @@
   </div>
   </div>
 </div>
 </div>
 
 
-## 隐藏内容
+## Hiding content
 <a class="btn btn-secondary text-white" data-bs-toggle="collapse" href="#collapse-2">
 <a class="btn btn-secondary text-white" data-bs-toggle="collapse" href="#collapse-2">
   Hide content
   Hide content
 </a>
 </a>
@@ -161,9 +161,9 @@
 </div>
 </div>
 
 
 
 
-# 官方文件
-- [点击此处了解徽章详情](https://getbootstrap.com/docs/5.3/components/badge/)
-- [单击此处了解警报详情](https://getbootstrap.com/docs/5.3/components/alerts/)
-- [点击此处了解贺卡详情](https://getbootstrap.com/docs/5.3/components/card/)
-- [点击此处了解颜色详情](https://getbootstrap.com/docs/5.3/utilities/colors/)
-- [点击此处查看折叠详情](https://getbootstrap.com/docs/5.3/components/collapse/)
+# Official docs
+- [Click here for Badges details](https://getbootstrap.jp/docs/5.3/components/badge/)
+- [Click here for Alerts details](https://getbootstrap.jp/docs/5.3/components/alerts/)
+- [Click here for Cards details](https://getbootstrap.jp/docs/5.3/components/card/)
+- [Click here for Colors details](https://getbootstrap.jp/docs/5.3/utilities/colors/)
+- [Click here for Collapse details](https://getbootstrap.jp/docs/5.3/components/collapse/)

+ 2 - 2
apps/app/resource/locales/ja_JP/sandbox.md

@@ -166,7 +166,7 @@
 - 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます
 - 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます
 
 
 #### 活用例
 #### 活用例
-Bootstrap によるページの装飾方法の記述方法は [[こちらをご確認ください>./1. ページの装飾方法(Bootstrap)]]
+Bootstrap によるページの装飾方法の記述方法は [[こちらをご確認ください>./Bootstrap5]]
 
 
 
 
 # :memo:画像の挿入
 # :memo:画像の挿入
@@ -297,6 +297,6 @@ ___
 # :memo:さらに応用的な表現
 # :memo:さらに応用的な表現
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 
 
-- [図形の表現方法(Daigrams)](/Sandbox/Daigrams)
+- [図形の表現方法(Diagrams)](/Sandbox/Diagrams)
 
 
 - [数式の表現方法(Math)](/Sandbox/Math)
 - [数式の表現方法(Math)](/Sandbox/Math)

+ 1 - 1
apps/app/src/components/Admin/App/QuestionnaireSettings.tsx

@@ -2,11 +2,11 @@ import {
   useState, useCallback, useEffect,
   useState, useCallback, useEffect,
 } from 'react';
 } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 import { useSWRxAppSettings } from '~/stores/admin/app-settings';
 import { useSWRxAppSettings } from '~/stores/admin/app-settings';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';

+ 3 - 2
apps/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
@@ -6,7 +7,7 @@ export const AuditLogDisableMode: FC = () => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   return (
   return (
-    <div id="content-main" className="content-main container-lg">
+    <div className="ccontainer-lg">
       <div className="container">
       <div className="container">
         <div className="row justify-content-md-center">
         <div className="row justify-content-md-center">
           <div className="col-md-6 mt-5">
           <div className="col-md-6 mt-5">

+ 1 - 1
apps/app/src/components/Admin/AuditLogManagement.tsx

@@ -1,6 +1,7 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { useState, useCallback, useRef } from 'react';
 import React, { useState, useCallback, useRef } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
@@ -10,7 +11,6 @@ import type { SupportedActionType } from '~/interfaces/activity';
 import { useSWRxActivity } from '~/stores/activity';
 import { useSWRxActivity } from '~/stores/activity';
 import { useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
 import { useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 
 import { ActivityTable } from './AuditLog/ActivityTable';
 import { ActivityTable } from './AuditLog/ActivityTable';

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -2,10 +2,10 @@ import React, {
   useCallback, useEffect, useState,
   useCallback, useEffect, useState,
 } from 'react';
 } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 
 

+ 1 - 1
apps/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
 import React from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 
 
 type Props = {
 type Props = {
   isEnabled?: boolean,
   isEnabled?: boolean,

+ 2 - 1
apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -1,8 +1,9 @@
 import React, { type ComponentPropsWithoutRef } from 'react';
 import React, { type ComponentPropsWithoutRef } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
+
 import { G2G_PROGRESS_STATUS, type G2GProgressStatus } from '~/interfaces/g2g-transfer';
 import { G2G_PROGRESS_STATUS, type G2GProgressStatus } from '~/interfaces/g2g-transfer';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 /**
 /**
  * Props for {@link G2GDataTransferStatusIcon}
  * Props for {@link G2GDataTransferStatusIcon}

+ 3 - 3
apps/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -46,15 +46,15 @@ const SecurityManagementContents = () => {
         i18n: 'OIDC',
         i18n: 'OIDC',
       },
       },
       passport_google: {
       passport_google: {
-        Icon: () => <i className="fa fa-google" />,
+        Icon: () => <span className="growi-custom-icons align-bottom">google</span>,
         i18n: 'Google',
         i18n: 'Google',
       },
       },
       passport_github: {
       passport_github: {
-        Icon: () => <i className="fa fa-github" />,
+        Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
         i18n: 'GitHub',
         i18n: 'GitHub',
       },
       },
       // passport_facebook: {
       // passport_facebook: {
-      //   Icon: () => <i className="fa fa-facebook" />,
+      //   Icon: () => <span className="growi-custom-icons align-bottom">facebook</span>,
       //   i18n: '(TBD) Facebook',
       //   i18n: '(TBD) Facebook',
       // },
       // },
     };
     };

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 
 
@@ -8,7 +9,6 @@ import {
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 
 
 import BotTypeCard from './BotTypeCard';
 import BotTypeCard from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';

+ 8 - 1
apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -162,7 +162,14 @@ export const UserGroupTable: FC<Props> = ({
                 {isExternalGroup && <td>{(group as IExternalUserGroupHasId).provider}</td>}
                 {isExternalGroup && <td>{(group as IExternalUserGroupHasId).provider}</td>}
                 {isAclEnabled
                 {isAclEnabled
                   ? (
                   ? (
-                    <td><Link href={`/admin/user-group-detail/${group._id}?isExternalGroup=${isExternalGroup}`}>{group.name}</Link></td>
+                    <td>
+                      <Link
+                        className="link-opacity-75-hover"
+                        href={`/admin/user-group-detail/${group._id}?isExternalGroup=${isExternalGroup}`}
+                      >
+                        {group.name}
+                      </Link>
+                    </td>
                   )
                   )
                   : (
                   : (
                     <td>{group.name}</td>
                     <td>{group.name}</td>

+ 1 - 1
apps/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import React from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -10,7 +11,6 @@ import {
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 import { useIsMailerSetup } from '~/stores/context';
 import { useIsMailerSetup } from '~/stores/context';
 
 
 class PasswordResetModal extends React.Component {
 class PasswordResetModal extends React.Component {

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

@@ -5,12 +5,12 @@ import React, {
 import {
 import {
   type IPageInfoAll, isIPageInfoForOperation,
   type IPageInfoAll, isIPageInfoForOperation,
 } from '@growi/core';
 } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import type { IPageOperationProcessData } from '~/interfaces/page-operation';
 import type { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';

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

@@ -12,23 +12,6 @@ $page-view-layout-margin-top: 32px;
   min-height: calc(100vh - #{$subnavigation-height + $page-view-layout-margin-top + $page-content-footer-min-heigh});
   min-height: calc(100vh - #{$subnavigation-height + $page-view-layout-margin-top + $page-content-footer-min-heigh});
 }
 }
 
 
-// md/lg layout padding
-.page-view-layout :global {
-  @include bs.media-breakpoint-between(md, xl) {
-    padding-left: var.$grw-sidebar-nav-width;
-  }
-}
-
-// container padding
-.page-view-layout :global,
-.footer-layout :global {
-  @include bs.media-breakpoint-up(lg) {
-    .container-lg {
-      --bs-gutter-x: 3rem;
-    }
-  }
-}
-
 // fluid layout
 // fluid layout
 .fluid-layout :global {
 .fluid-layout :global {
   .grw-container-convertible {
   .grw-container-convertible {

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

@@ -3,7 +3,6 @@ import type { ReactNode } from 'react';
 import styles from './PageViewLayout.module.scss';
 import styles from './PageViewLayout.module.scss';
 
 
 const pageViewLayoutClass = styles['page-view-layout'] ?? '';
 const pageViewLayoutClass = styles['page-view-layout'] ?? '';
-const footerLayoutClass = styles['footer-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 
 
 type Props = {
 type Props = {
@@ -24,8 +23,8 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <div id="main" className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert`}>
-        <div id="content-main" className="content-main container-lg grw-container-convertible flex-expand-vert">
+      <div className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert ps-sidebar`}>
+        <div className="container-lg wide-gutter-x-lg grw-container-convertible flex-expand-vert">
           { headerContents != null && headerContents }
           { headerContents != null && headerContents }
           { sideContents != null
           { sideContents != null
             ? (
             ? (
@@ -48,7 +47,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
       </div>
       </div>
 
 
       { footerContents != null && (
       { footerContents != null && (
-        <footer className={`footer d-edit-none ${footerLayoutClass} ${fluidLayoutClass}`}>
+        <footer className={`footer d-edit-none wide-gutter-x-lg ${fluidLayoutClass}`}>
           {footerContents}
           {footerContents}
         </footer>
         </footer>
       ) }
       ) }

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

@@ -5,6 +5,7 @@ import type {
   IPageHasId,
   IPageHasId,
   IPageInfoForOperation,
   IPageInfoForOperation,
 } from '@growi/core';
 } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
@@ -17,7 +18,6 @@ import {
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 
 
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
-import { LoadingSpinner } from './LoadingSpinner';
 import PageList from './PageList/PageList';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 
 

+ 1 - 1
apps/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -2,10 +2,10 @@ import type { FC } from 'react';
 import React from 'react';
 import React from 'react';
 
 
 import type { HasObjectId } from '@growi/core';
 import type { HasObjectId } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 
 
 import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 import InAppNotificationElm from './InAppNotificationElm';
 import InAppNotificationElm from './InAppNotificationElm';
 
 

+ 1 - 1
apps/app/src/components/InAppNotification/InAppNotificationPage.tsx

@@ -1,6 +1,7 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
@@ -10,7 +11,6 @@ import loggerFactory from '~/utils/logger';
 
 
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '../../stores/in-app-notification';
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '../../stores/in-app-notification';
 import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
 import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
-import { LoadingSpinner } from '../LoadingSpinner';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 
 import InAppNotificationList from './InAppNotificationList';
 import InAppNotificationList from './InAppNotificationList';

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

@@ -1,9 +1,9 @@
 import type { Ref } from 'react';
 import type { Ref } from 'react';
 import React, { useEffect, useState } from 'react';
 import React, { useEffect, useState } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 
 
-import { LoadingSpinner } from './LoadingSpinner';
 
 
 type Props<T> = {
 type Props<T> = {
   swrInifiniteResponse: SWRInfiniteResponse<T>
   swrInifiniteResponse: SWRInfiniteResponse<T>

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

@@ -2,6 +2,7 @@ import type { FormEventHandler } from 'react';
 import { memo, useCallback, useState } from 'react';
 import { memo, useCallback, useState } from 'react';
 
 
 import { Lang, AllLang } from '@growi/core';
 import { Lang, AllLang } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
@@ -10,7 +11,6 @@ import { i18n as i18nConfig } from '^/config/next-i18next.config';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 
 
-import { LoadingSpinner } from './LoadingSpinner';
 
 
 const InstallerForm = memo((): JSX.Element => {
 const InstallerForm = memo((): JSX.Element => {
   const { t, i18n } = useTranslation();
   const { t, i18n } = useTranslation();

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

@@ -1,5 +1,6 @@
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
@@ -7,8 +8,6 @@ import { apiv3Post } from '~/client/util/apiv3-client';
 
 
 import { useCurrentUser } from '../stores/context';
 import { useCurrentUser } from '../stores/context';
 
 
-import { LoadingSpinner } from './LoadingSpinner';
-
 
 
 export type InvitedFormProps = {
 export type InvitedFormProps = {
   invitedFormUsername: string,
   invitedFormUsername: string,

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

@@ -39,7 +39,7 @@ const AdminLayout = ({
             {componentTitle}
             {componentTitle}
           </h1>
           </h1>
         </header>
         </header>
-        <div id="main" className="main">
+        <div className="main">
           <div className="container-fluid">
           <div className="container-fluid">
             <div className="row">
             <div className="row">
               <div className="col-lg-3">
               <div className="col-lg-3">

+ 0 - 7
apps/app/src/components/LoadingSpinner.tsx

@@ -1,7 +0,0 @@
-import React, { type ComponentPropsWithoutRef } from 'react';
-
-import styles from './LoadingSpinner.module.scss';
-
-export const LoadingSpinner = ({ className = '' }: ComponentPropsWithoutRef<'span'>): JSX.Element => (
-  <span className={`material-symbols-outlined pb-0 ${styles.spinner} ${className}`}>progress_activity</span>
-);

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

@@ -2,6 +2,7 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 import ReactCardFlip from 'react-card-flip';
@@ -14,7 +15,6 @@ import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { CompleteUserRegistration } from './CompleteUserRegistration';
 import { CompleteUserRegistration } from './CompleteUserRegistration';
-import { LoadingSpinner } from './LoadingSpinner';
 
 
 import styles from './LoginForm.module.scss';
 import styles from './LoginForm.module.scss';
 
 
@@ -277,7 +277,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={handleLoginWithExternalAuth}>
         <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={handleLoginWithExternalAuth}>
           <div className="eff"></div>
           <div className="eff"></div>
           <span className="btn-label">
           <span className="btn-label">
-            <i className={`fa fa-${authIconNames[auth]}`}></i>
+            <span className="growi-custom-icons align-bottom">{authIconNames[auth]}</span>
           </span>
           </span>
           <span className="btn-label-text">{t('Sign in')}</span>
           <span className="btn-label-text">{t('Sign in')}</span>
         </button>
         </button>

+ 7 - 7
apps/app/src/components/Me/AssociateModal.tsx

@@ -63,28 +63,28 @@ const AssociateModal = (props: Props): JSX.Element => {
         <div>
         <div>
           <Nav tabs className="mb-2">
           <Nav tabs className="mb-2">
             <NavLink
             <NavLink
-              className={activeTab === 1 ? 'active' : ''}
+              className={`${activeTab === 1 ? 'active' : ''} d-flex gap-1 align-items-center`}
               onClick={() => setActiveTab(1)}
               onClick={() => setActiveTab(1)}
             >
             >
               <span className="material-symbols-outlined fs-5">network_node</span> LDAP
               <span className="material-symbols-outlined fs-5">network_node</span> LDAP
             </NavLink>
             </NavLink>
             <NavLink
             <NavLink
-              className={activeTab === 2 ? 'active' : ''}
+              className={`${activeTab === 2 ? 'active' : ''} d-flex gap-1 align-items-center`}
               onClick={() => setActiveTab(2)}
               onClick={() => setActiveTab(2)}
             >
             >
-              <i className="fa fa-github"></i> (TBD) GitHub
+              <span className="growi-custom-icons">github</span> (TBD) GitHub
             </NavLink>
             </NavLink>
             <NavLink
             <NavLink
-              className={activeTab === 3 ? 'active' : ''}
+              className={`${activeTab === 3 ? 'active' : ''} d-flex gap-1 align-items-center`}
               onClick={() => setActiveTab(3)}
               onClick={() => setActiveTab(3)}
             >
             >
-              <i className="fa fa-google"></i> (TBD) Google OAuth
+              <span className="growi-custom-icons">google</span> (TBD) Google OAuth
             </NavLink>
             </NavLink>
             {/* <NavLink
             {/* <NavLink
-              className={activeTab === 4 ? 'active' : ''}
+              className={`${activeTab === 4 ? 'active' : ''} d-flex gap-1 align-items-center`}
               onClick={() => setActiveTab(4)}
               onClick={() => setActiveTab(4)}
             >
             >
-              <i className="fa fa-facebook"></i> (TBD) Facebook
+              <span className="growi-custom-icons">facebook</span> (TBD) Facebook
             </NavLink> */}
             </NavLink> */}
           </Nav>
           </Nav>
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>

+ 1 - 2
apps/app/src/components/Me/QuestionnaireSettings.tsx

@@ -1,5 +1,6 @@
 import { useCallback, useEffect, useState } from 'react';
 import { useCallback, useEffect, useState } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
@@ -8,8 +9,6 @@ import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
 import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
-
 
 
 export const QuestionnaireSettings = (): JSX.Element => {
 export const QuestionnaireSettings = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();

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

@@ -0,0 +1,4 @@
+.ground-glass-bar {
+  background-color: rgba(var(--bs-body-bg-rgb), 0.7);
+  backdrop-filter: blur(35px);
+}

+ 17 - 0
apps/app/src/components/Navbar/GroundGlassBar.tsx

@@ -0,0 +1,17 @@
+import type { DetailedHTMLProps } from 'react';
+
+import styles from './GroundGlassBar.module.scss';
+
+const moduleClass = styles['ground-glass-bar'];
+
+type Props = DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
+
+export const GroundGlassBar = (props: Props): JSX.Element => {
+  const { className, children, ...rest } = props;
+
+  return (
+    <div className={`${moduleClass} ${className ?? ''}`} {...rest}>
+      {children}
+    </div>
+  );
+};

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

@@ -1,10 +1,5 @@
 @use '~/styles/mixins';
 @use '~/styles/mixins';
 
 
-.grw-contextual-sub-navigation :global {
-  background-color: rgba(var(--bs-body-bg-rgb), 0.7);
-  backdrop-filter: blur(35px);
-}
-
 @include mixins.editing() {
 @include mixins.editing() {
   .grw-contextual-sub-navigation {
   .grw-contextual-sub-navigation {
     position: fixed;
     position: fixed;

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

@@ -36,6 +36,8 @@ import { CreateTemplateModal } from '../CreateTemplateModal';
 import { NotAvailable } from '../NotAvailable';
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 import { Skeleton } from '../Skeleton';
 
 
+import { GroundGlassBar } from './GroundGlassBar';
+
 import styles from './GrowiContextualSubNavigation.module.scss';
 import styles from './GrowiContextualSubNavigation.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
@@ -288,7 +290,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   return (
   return (
     <>
     <>
-      <div
+      <GroundGlassBar
         className={`${styles['grw-contextual-sub-navigation']}
         className={`${styles['grw-contextual-sub-navigation']}
           d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
           d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
         `}
         `}
@@ -332,7 +334,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             </Link>
             </Link>
           </div>
           </div>
         ) }
         ) }
-      </div>
+      </GroundGlassBar>
 
 
       {path != null && currentUser != null && !isReadOnlyUser && (
       {path != null && currentUser != null && !isReadOnlyUser && (
         <CreateTemplateModal
         <CreateTemplateModal

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

@@ -22,11 +22,3 @@
     align-items: center;
     align-items: center;
   }
   }
 }
 }
-
-// == Colors
-.grw-navbar-bottom :global {
-  .navbar {
-    background-color: rgba(var(--bs-body-bg-rgb), 0.7);
-    backdrop-filter: blur(35px);
-  }
-}

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

@@ -6,6 +6,8 @@ import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { useDrawerOpened } from '~/stores/ui';
 import { useDrawerOpened } from '~/stores/ui';
 
 
+import { GroundGlassBar } from './GroundGlassBar';
+
 import styles from './GrowiNavbarBottom.module.scss';
 import styles from './GrowiNavbarBottom.module.scss';
 
 
 
 
@@ -18,7 +20,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   const { open: openSearchModal } = useSearchModal();
   const { open: openSearchModal } = useSearchModal();
 
 
   return (
   return (
-    <div className={`
+    <GroundGlassBar className={`
       ${styles['grw-navbar-bottom']}
       ${styles['grw-navbar-bottom']}
       ${isDrawerOpened ? styles['grw-navbar-bottom-drawer-opened'] : ''}
       ${isDrawerOpened ? styles['grw-navbar-bottom-drawer-opened'] : ''}
       d-md-none d-edit-none d-print-none fixed-bottom`}
       d-md-none d-edit-none d-print-none fixed-bottom`}
@@ -73,6 +75,6 @@ export const GrowiNavbarBottom = (): JSX.Element => {
         </ul>
         </ul>
       </div>
       </div>
 
 
-    </div>
+    </GroundGlassBar>
   );
   );
 };
 };

+ 7 - 4
apps/app/src/components/Page/DisplaySwitcher.tsx

@@ -2,17 +2,16 @@ import React from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { useIsEditable } from '~/stores/context';
 import { useIsEditable } from '~/stores/context';
+import { useIsLatestRevision } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 import { LazyRenderer } from '../Common/LazyRenderer';
 
 
-
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
-
+const PageEditorReadOnly = dynamic(() => import('../PageEditor/PageEditorReadOnly').then(mod => mod.PageEditorReadOnly), { ssr: false });
 
 
 type Props = {
 type Props = {
   pageView: JSX.Element,
   pageView: JSX.Element,
@@ -23,6 +22,7 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
 
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
+  const { data: isLatestRevision } = useIsLatestRevision();
 
 
   usePageUpdatedEffect();
   usePageUpdatedEffect();
   useHashChangedEffect();
   useHashChangedEffect();
@@ -34,7 +34,10 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
       { isViewMode && pageView }
       { isViewMode && pageView }
 
 
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
-        <PageEditor />
+        { isLatestRevision
+          ? <PageEditor />
+          : <PageEditorReadOnly />
+        }
       </LazyRenderer>
       </LazyRenderer>
     </>
     </>
   );
   );

+ 1 - 2
apps/app/src/components/Page/RevisionLoader.tsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
 
 
 import type { Ref, IRevision, IRevisionHasId } from '@growi/core';
 import type { Ref, IRevision, IRevisionHasId } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
@@ -8,8 +9,6 @@ import { useSWRxPageRevision } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
-
 import RevisionRenderer from './RevisionRenderer';
 import RevisionRenderer from './RevisionRenderer';
 
 
 export const ROOT_ELEM_ID = 'revision-loader' as const;
 export const ROOT_ELEM_ID = 'revision-loader' as const;

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

@@ -3,13 +3,13 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import type { IAttachmentHasId } from '@growi/core';
 import type { IAttachmentHasId } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 
 
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 import { PageAttachmentList } from '../PageAttachment/PageAttachmentList';
 import { PageAttachmentList } from '../PageAttachment/PageAttachmentList';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 

+ 20 - 8
apps/app/src/components/PageAlert/OldRevisionAlert.tsx

@@ -1,27 +1,39 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
-import Link from 'next/link';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRMUTxCurrentPage, useIsLatestRevision } from '~/stores/page';
 
 
 export const OldRevisionAlert = (): JSX.Element => {
 export const OldRevisionAlert = (): JSX.Element => {
-
+  const router = useRouter();
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: isLatestRevision } = useIsLatestRevision();
+
+  const { data: isOldRevisionPage } = useIsLatestRevision();
   const { data: page } = useSWRxCurrentPage();
   const { data: page } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
+  const onClickShowLatestButton = useCallback(async() => {
+    if (page == null) {
+      return;
+    }
+
+    const url = returnPathForURL(page.path, page._id);
+    await router.push(url);
+    mutateCurrentPage();
+  }, [mutateCurrentPage, page, router]);
 
 
-  if (page == null || isLatestRevision == null || isLatestRevision) {
+  if (page == null || isOldRevisionPage) {
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
     <div className="alert alert-warning">
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
-      <Link href={returnPathForURL(page.path, page._id)}>
+      <button type="button" className="btn btn-outline-natural-secondary" onClick={onClickShowLatestButton}>
         <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
         <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
-      </Link>
+      </button>
     </div>
     </div>
   );
   );
 };
 };

+ 1 - 2
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -3,7 +3,7 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
+import { UserPicture, LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
@@ -13,7 +13,6 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 import { Username } from '../User/Username';
 import { Username } from '../User/Username';
 
 
 import styles from './DeleteAttachmentModal.module.scss';
 import styles from './DeleteAttachmentModal.module.scss';

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

@@ -1,6 +1,7 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import DropdownToggle from 'reactstrap/esm/DropdownToggle';
 import DropdownToggle from 'reactstrap/esm/DropdownToggle';
 import Popover from 'reactstrap/esm/Popover';
 import Popover from 'reactstrap/esm/Popover';
@@ -12,7 +13,6 @@ import { useIsGuestUser } from '~/stores/context';
 
 
 import { BookmarkFolderMenu } from '../Bookmarks/BookmarkFolderMenu';
 import { BookmarkFolderMenu } from '../Bookmarks/BookmarkFolderMenu';
 import UserPictureList from '../Common/UserPictureList';
 import UserPictureList from '../Common/UserPictureList';
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 import styles from './BookmarkButtons.module.scss';
 import styles from './BookmarkButtons.module.scss';
 import popoverStyles from './user-list-popover.module.scss';
 import popoverStyles from './user-list-popover.module.scss';

+ 8 - 7
apps/app/src/components/PageControls/PageControls.tsx

@@ -235,7 +235,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     return wideviewMenuItemRenderer;
     return wideviewMenuItemRenderer;
   }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
   }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
 
 
-  if (!isIPageInfoForOperation(pageInfo)) {
+  if (!isIPageInfoForEntity(pageInfo)) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -249,6 +249,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     MenuItemType.REVERT,
     MenuItemType.REVERT,
   ];
   ];
 
 
+  const _isIPageInfoForOperation = isIPageInfoForOperation(pageInfo);
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return (
   return (
@@ -256,18 +257,18 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
       { isDeviceLargerThanMd && (
       { isDeviceLargerThanMd && (
         <SearchButton />
         <SearchButton />
       )}
       )}
-      {revisionId != null && !isViewMode && (
+      {revisionId != null && !isViewMode && _isIPageInfoForOperation && (
         <Tags
         <Tags
           onClickEditTagsButton={onClickEditTagsButton}
           onClickEditTagsButton={onClickEditTagsButton}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <SubscribeButton
         <SubscribeButton
           status={pageInfo.subscriptionStatus}
           status={pageInfo.subscriptionStatus}
           onClick={subscribeClickhandler}
           onClick={subscribeClickhandler}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <LikeButtons
         <LikeButtons
           onLikeClicked={likeClickhandler}
           onLikeClicked={likeClickhandler}
           sumOfLikers={sumOfLikers}
           sumOfLikers={sumOfLikers}
@@ -275,7 +276,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
           likers={likers}
           likers={likers}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <BookmarkButtons
         <BookmarkButtons
           pageId={pageId}
           pageId={pageId}
           isBookmarked={pageInfo.isBookmarked}
           isBookmarked={pageInfo.isBookmarked}
@@ -289,7 +290,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
           disabled={disableSeenUserInfoPopover}
           disabled={disableSeenUserInfoPopover}
         />
         />
       ) }
       ) }
-      { showPageControlDropdown && (
+      { showPageControlDropdown && _isIPageInfoForOperation && (
         <PageItemControl
         <PageItemControl
           alignEnd
           alignEnd
           pageId={pageId}
           pageId={pageId}
@@ -337,7 +338,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
-  if (!isIPageInfoForOperation(pageInfo)) {
+  if (!isIPageInfoForEntity(pageInfo)) {
     return <></>;
     return <></>;
   }
   }
 
 

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

@@ -6,6 +6,7 @@ import React, {
 
 
 import { useCodeMirrorEditorIsolated } from '@growi/editor';
 import { useCodeMirrorEditorIsolated } from '@growi/editor';
 import { useDrawioModalForEditor } from '@growi/editor/src/stores/use-drawio';
 import { useDrawioModalForEditor } from '@growi/editor/src/stores/use-drawio';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import {
 import {
   Modal,
   Modal,
   ModalBody,
   ModalBody,
@@ -18,7 +19,6 @@ import { useDrawioModal } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 import { type DrawioConfig, DrawioCommunicationHelper } from './DrawioCommunicationHelper';
 import { type DrawioConfig, DrawioCommunicationHelper } from './DrawioCommunicationHelper';
 
 

+ 1 - 0
apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss

@@ -5,6 +5,7 @@
 @include mixins.editing() {
 @include mixins.editing() {
   .grw-editor-navbar-bottom :global {
   .grw-editor-navbar-bottom :global {
     .grw-grant-selector {
     .grw-grant-selector {
+      max-width: 250px;
       .material-symbols-outlined  {
       .material-symbols-outlined  {
         padding-bottom: 2px;
         padding-bottom: 2px;
         font-size: 19px;
         font-size: 19px;

+ 6 - 37
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -51,7 +51,7 @@ import loggerFactory from '~/utils/logger';
 import { EditorNavbar } from './EditorNavbar';
 import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import Preview from './Preview';
-import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
+import { useScrollSync } from './ScrollSyncHelper';
 import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 
 
 import '@growi/editor/dist/style.css';
 import '@growi/editor/dist/style.css';
@@ -65,10 +65,6 @@ declare global {
   var globalEmitter: EventEmitter;
   var globalEmitter: EventEmitter;
 }
 }
 
 
-// for scrolling
-let isOriginOfScrollSyncEditor = false;
-let isOriginOfScrollSyncPreview = false;
-
 export type SaveOptions = {
 export type SaveOptions = {
   slackChannels: string,
   slackChannels: string,
   overwriteScopesOfDescendants?: boolean
   overwriteScopesOfDescendants?: boolean
@@ -163,6 +159,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
 
+  const { scrollEditorHandler, scrollPreviewHandler } = useScrollSync(GlobalCodeMirrorEditorKey.MAIN, previewRef);
+
+  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
+  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
+
   const save: Save = useCallback(async(revisionId, markdown, opts, onConflict) => {
   const save: Save = useCallback(async(revisionId, markdown, opts, onConflict) => {
     if (pageId == null || grantData == null) {
     if (pageId == null || grantData == null) {
       logger.error('Some materials to save are invalid', {
       logger.error('Some materials to save are invalid', {
@@ -272,38 +273,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   }, [codeMirrorEditor, pageId]);
   }, [codeMirrorEditor, pageId]);
 
 
-  const scrollEditorHandler = useCallback(() => {
-    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
-      return;
-    }
-
-    if (isOriginOfScrollSyncPreview) {
-      isOriginOfScrollSyncPreview = false;
-      return;
-    }
-
-    isOriginOfScrollSyncEditor = true;
-    scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor]);
-
-  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
-
-  const scrollPreviewHandler = useCallback(() => {
-    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
-      return;
-    }
-
-    if (isOriginOfScrollSyncEditor) {
-      isOriginOfScrollSyncEditor = false;
-      return;
-    }
-
-    isOriginOfScrollSyncPreview = true;
-    scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor]);
-
-  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
-
   // initial caret line
   // initial caret line
   useEffect(() => {
   useEffect(() => {
     codeMirrorEditor?.setCaretLine();
     codeMirrorEditor?.setCaretLine();

+ 62 - 0
apps/app/src/components/PageEditor/PageEditorReadOnly.tsx

@@ -0,0 +1,62 @@
+import react, { useMemo, useRef } from 'react';
+
+import { CodeMirrorEditorReadOnly, GlobalCodeMirrorEditorKey } from '@growi/editor';
+import { throttle } from 'throttle-debounce';
+
+import { useShouldExpandContent } from '~/client/services/layout';
+import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import { usePreviewOptions } from '~/stores/renderer';
+
+import { EditorNavbar } from './EditorNavbar';
+import Preview from './Preview';
+import { useScrollSync } from './ScrollSyncHelper';
+
+type Props = {
+  visibility?: boolean,
+}
+
+export const PageEditorReadOnly = react.memo(({ visibility }: Props): JSX.Element => {
+  const previewRef = useRef<HTMLDivElement>(null);
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: rendererOptions } = usePreviewOptions();
+  const { data: isLatestRevision } = useIsLatestRevision();
+  const shouldExpandContent = useShouldExpandContent(currentPage);
+
+  const { scrollEditorHandler, scrollPreviewHandler } = useScrollSync(GlobalCodeMirrorEditorKey.READONLY, previewRef);
+  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
+  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
+
+  const revisionBody = currentPage?.revision?.body;
+
+  if (rendererOptions == null || isLatestRevision) {
+    return <></>;
+  }
+
+  return (
+    <div id="page-editor" className={`flex-expand-vert ${visibility ? '' : 'd-none'}`}>
+      <EditorNavbar />
+
+      <div className="flex-expand-horiz">
+        <div className="page-editor-editor-container flex-expand-vert border-end">
+          <CodeMirrorEditorReadOnly
+            markdown={revisionBody}
+            onScroll={scrollEditorHandlerThrottle}
+          />
+        </div>
+        <div
+          ref={previewRef}
+          onScroll={scrollPreviewHandlerThrottle}
+          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
+        >
+          <Preview
+            markdown={revisionBody}
+            pagePath={currentPage?.path}
+            rendererOptions={rendererOptions}
+            expandContentWidth={shouldExpandContent}
+          />
+        </div>
+      </div>
+    </div>
+  );
+});

+ 44 - 2
apps/app/src/components/PageEditor/ScrollSyncHelper.ts → apps/app/src/components/PageEditor/ScrollSyncHelper.tsx

@@ -1,3 +1,7 @@
+import { useCallback, type RefObject, useRef } from 'react';
+
+import { useCodeMirrorEditorIsolated, type GlobalCodeMirrorEditorKey } from '@growi/editor';
+
 let defaultTop = 0;
 let defaultTop = 0;
 const padding = 5;
 const padding = 5;
 
 
@@ -88,7 +92,7 @@ const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: T
 };
 };
 
 
 
 
-export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
 
 
   setDefaultTop(editorRootElement.getBoundingClientRect().top);
   setDefaultTop(editorRootElement.getBoundingClientRect().top);
 
 
@@ -120,7 +124,7 @@ export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement:
 
 
 };
 };
 
 
-export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
 
 
   setDefaultTop(previewRootElement.getBoundingClientRect().y);
   setDefaultTop(previewRootElement.getBoundingClientRect().y);
 
 
@@ -150,3 +154,41 @@ export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement
   editorRootElement.scrollTop = newScrollTop;
   editorRootElement.scrollTop = newScrollTop;
 
 
 };
 };
+
+// eslint-disable-next-line max-len
+export const useScrollSync = (codeMirrorKey: GlobalCodeMirrorEditorKey, previewRef: RefObject<HTMLDivElement>): { scrollEditorHandler: () => void; scrollPreviewHandler: () => void } => {
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(codeMirrorKey);
+
+  const isOriginOfScrollSyncEditor = useRef(false);
+  const isOriginOfScrollSyncPreview = useRef(false);
+
+  const scrollEditorHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
+      return;
+    }
+
+    if (isOriginOfScrollSyncPreview.current) {
+      isOriginOfScrollSyncPreview.current = false;
+      return;
+    }
+
+    isOriginOfScrollSyncEditor.current = true;
+    scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, isOriginOfScrollSyncPreview, previewRef]);
+
+  const scrollPreviewHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
+      return;
+    }
+
+    if (isOriginOfScrollSyncEditor.current) {
+      isOriginOfScrollSyncEditor.current = false;
+      return;
+    }
+
+    isOriginOfScrollSyncPreview.current = true;
+    scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, isOriginOfScrollSyncEditor, previewRef]);
+
+  return { scrollEditorHandler, scrollPreviewHandler };
+};

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

@@ -1,12 +1,12 @@
 import React from 'react';
 import React from 'react';
 
 
 import type { IPageInfoForEntity, IPageWithMeta } from '@growi/core';
 import type { IPageInfoForEntity, IPageWithMeta } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 import { PageListItemL } from './PageListItemL';
 import { PageListItemL } from './PageListItemL';
 
 

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

@@ -1,6 +1,7 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import type { PresentationProps } from '@growi/presentation';
 import type { PresentationProps } from '@growi/presentation';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
@@ -14,7 +15,6 @@ import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
 import { usePresentationViewOptions } from '~/stores/renderer';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 
 
-import { LoadingSpinner } from './LoadingSpinner';
 
 
 import styles from './PagePresentationModal.module.scss';
 import styles from './PagePresentationModal.module.scss';
 
 

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

@@ -3,6 +3,7 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useGlobalSocket } from '@growi/core/dist/swr';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalHeader, ModalBody, ModalFooter,
   UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalHeader, ModalBody, ModalFooter,
@@ -25,7 +26,6 @@ import {
 } from '~/stores/search';
 } from '~/stores/search';
 
 
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
-import { LoadingSpinner } from './LoadingSpinner';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';

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

@@ -3,6 +3,7 @@ import React, { useCallback, useState, useEffect } from 'react';
 import type EventEmitter from 'events';
 import type EventEmitter from 'events';
 
 
 import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledButtonDropdown, Button,
   UncontrolledButtonDropdown, Button,
@@ -28,7 +29,6 @@ import loggerFactory from '~/utils/logger';
 
 
 import { unpublish } from '../client/services/page-operation';
 import { unpublish } from '../client/services/page-operation';
 
 
-import { LoadingSpinner } from './LoadingSpinner';
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { SlackNotification } from './SlackNotification';
 import { SlackNotification } from './SlackNotification';
 
 

+ 27 - 9
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
 import {
 import {
   PageGrant, isPopulated, GroupType, type IGrantedGroup,
   PageGrant, isPopulated, GroupType, type IGrantedGroup,
 } from '@growi/core';
 } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledDropdown,
   UncontrolledDropdown,
@@ -11,7 +12,6 @@ import {
   Modal, ModalHeader, ModalBody,
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { LoadingSpinner } from '~/components/LoadingSpinner';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
@@ -144,11 +144,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
           <span className="label">
           <span className="label">
             {userRelatedGrantedGroups.length > 1
             {userRelatedGrantedGroups.length > 1
               ? (
               ? (
+              // substring for group name truncate
                 <span>
                 <span>
-                  {`${userRelatedGrantedGroups[0].name}... `}
-                  <span className="badge badge-purple">+{userRelatedGrantedGroups.length - 1}</span>
+                  {`${userRelatedGrantedGroups[0].name.substring(0, 30)}, ... `}
+                  <span className="badge bg-primary">+{userRelatedGrantedGroups.length - 1}</span>
                 </span>
                 </span>
-              ) : userRelatedGrantedGroups[0].name}
+              ) : userRelatedGrantedGroups[0].name.substring(0, 30)}
           </span>
           </span>
         </span>
         </span>
       );
       );
@@ -162,7 +163,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
     return (
     return (
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
         <UncontrolledDropdown direction={openInModal ? 'down' : 'up'} size="sm">
         <UncontrolledDropdown direction={openInModal ? 'down' : 'up'} size="sm">
-          <DropdownToggle color={dropdownToggleBtnColor} caret className="w-100 d-flex justify-content-between align-items-center" disabled={disabled}>
+          <DropdownToggle
+            color={dropdownToggleBtnColor}
+            caret
+            className="w-100 text-truncate d-flex justify-content-between align-items-center"
+            disabled={disabled}
+          >
             {dropdownToggleLabelElm}
             {dropdownToggleLabelElm}
           </DropdownToggle>
           </DropdownToggle>
           <DropdownMenu container={openInModal ? '' : 'body'}>
           <DropdownMenu container={openInModal ? '' : 'body'}>
@@ -209,13 +215,13 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
           return (
           return (
             <button
             <button
-              className={`btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 ${activeClass}`}
+              className={`btn btn-outline-primary d-flex justify-content-start mb-3 mx-4 align-items-center p-3 ${activeClass}`}
               type="button"
               type="button"
               key={group.item._id}
               key={group.item._id}
               onClick={() => groupListItemClickHandler(group)}
               onClick={() => groupListItemClickHandler(group)}
             >
             >
-              <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
-              <h5 className="d-inline-block ms-3">{group.item.name}</h5>
+              <input type="checkbox" checked={groupIsGranted} />
+              <p className="ms-3 mb-0">{group.item.name}</p>
               {group.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{group.item.provider}</span>}
               {group.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{group.item.provider}</span>}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
             </button>
@@ -227,6 +233,18 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
 
 
+  const renderModalCloseButton = useCallback(() => {
+    return (
+      <button
+        type="button"
+        className="btn border-0 text-muted"
+        onClick={() => setIsSelectGroupModalShown(false)}
+      >
+        <span className="material-symbols-outlined">close</span>
+      </button>
+    );
+  }, [setIsSelectGroupModalShown]);
+
   return (
   return (
     <>
     <>
       { renderGrantSelector() }
       { renderGrantSelector() }
@@ -238,7 +256,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
           toggle={() => setIsSelectGroupModalShown(false)}
           toggle={() => setIsSelectGroupModalShown(false)}
           centered
           centered
         >
         >
-          <ModalHeader tag="h4" toggle={() => setIsSelectGroupModalShown(false)} className="bg-purple text-muted">
+          <ModalHeader tag="p" toggle={() => setIsSelectGroupModalShown(false)} className="fs-5 text-muted fw-bold pb-2" close={renderModalCloseButton()}>
             {t('user_group.select_group')}
             {t('user_group.select_group')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>

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

@@ -3,6 +3,7 @@ import React, {
   forwardRef, useEffect, useImperativeHandle, useRef, useState,
   forwardRef, useEffect, useImperativeHandle, useRef, useState,
 } from 'react';
 } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
@@ -17,7 +18,6 @@ import { usePageDeleteModal } from '~/stores/modal';
 import { mutatePageTree } from '~/stores/page-listing';
 import { mutatePageTree } from '~/stores/page-listing';
 
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 // Do not import with next/dynamic
 // Do not import with next/dynamic
 // see: https://github.com/weseek/growi/pull/7923
 // see: https://github.com/weseek/growi/pull/7923

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

@@ -9,6 +9,7 @@ import { useTemplateModal, type TemplateModalStatus } from '@growi/editor/src/st
 import {
 import {
   extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
   extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
 } from '@growi/pluginkit/dist/v4';
 } from '@growi/pluginkit/dist/v4';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal,
   Modal,
@@ -26,7 +27,6 @@ import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { LoadingSpinner } from '../LoadingSpinner';
 import Preview from '../PageEditor/Preview';
 import Preview from '../PageEditor/Preview';
 
 
 import { useFormatter } from './use-formatter';
 import { useFormatter } from './use-formatter';

+ 1 - 1
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -6,6 +6,7 @@ import React, {
 import nodePath from 'path';
 import nodePath from 'path';
 
 
 import type { Nullable } from '@growi/core';
 import type { Nullable } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
@@ -15,7 +16,6 @@ import { usePageTreeDescCountMap } from '~/stores/ui';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
 
 
 import CountBadge from '../Common/CountBadge';
 import CountBadge from '../Common/CountBadge';
-import { LoadingSpinner } from '../LoadingSpinner';
 
 
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import { useNewPageInput } from './NewPageInput';
 import { useNewPageInput } from './NewPageInput';

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

@@ -48,8 +48,8 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/_private-legacy-pages');
   useCurrentPathname('/_private-legacy-pages');
 
 

+ 6 - 4
apps/app/src/pages/_search.page.tsx

@@ -4,6 +4,7 @@ import type { IUser } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
@@ -16,8 +17,6 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 
-import { SearchPage } from '../components/SearchPage';
-
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import type { CommonProps } from './utils/commons';
 import {
 import {
@@ -25,6 +24,9 @@ import {
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
+const SearchPage = dynamic(() => import('../components/SearchPage').then(mod => mod.SearchPage), { ssr: false });
+
+
 type Props = CommonProps & {
 type Props = CommonProps & {
   currentUser: IUser,
   currentUser: IUser,
 
 
@@ -52,8 +54,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/_search');
   useCurrentPathname('/_search');
 
 

+ 2 - 2
apps/app/src/pages/forgot-password-errors.page.tsx

@@ -19,8 +19,8 @@ const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
   const { errorCode } = props;
   const { errorCode } = props;
 
 
   return (
   return (
-    <div id="main" className="main">
-      <div id="content-main" className="content-main container-lg">
+    <div className="main">
+      <div className="container-lg">
         <div className="container">
         <div className="container">
           <div className="row justify-content-md-center">
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
             <div className="col-md-6 mt-5">

+ 6 - 7
apps/app/src/pages/forgot-password.page.tsx

@@ -1,15 +1,14 @@
 import React from 'react';
 import React from 'react';
 
 
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { useIsMailerSetup } from '~/stores/context';
 import { useIsMailerSetup } from '~/stores/context';
 
 
-import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps,
-} from './utils/commons';
+import type { CommonProps } from './utils/commons';
+import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
 
 
 const PasswordResetRequestForm = dynamic(() => import('~/components/PasswordResetRequestForm'), { ssr: false });
 const PasswordResetRequestForm = dynamic(() => import('~/components/PasswordResetRequestForm'), { ssr: false });
 
 
@@ -21,8 +20,8 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
   useIsMailerSetup(props.isMailerSetup);
   useIsMailerSetup(props.isMailerSetup);
 
 
   return (
   return (
-    <div id="main" className="main">
-      <div id="content-main" className="content-main container-lg">
+    <div className="main">
+      <div className="container-lg">
         <div className="container">
         <div className="container">
           <div className="row justify-content-md-center">
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
             <div className="col-md-6 mt-5">

+ 1 - 1
apps/app/src/pages/maintenance.page.tsx

@@ -32,7 +32,7 @@ const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
   };
   };
 
 
   return (
   return (
-    <div id="content-main" className="content-main container-lg">
+    <div className="container-lg">
       <div className="container">
       <div className="container">
         <div className="row justify-content-md-center">
         <div className="row justify-content-md-center">
           <div className="col-md-6 mt-5">
           <div className="col-md-6 mt-5">

+ 10 - 11
apps/app/src/pages/me/[[...path]].page.tsx

@@ -10,6 +10,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
+import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import {
 import {
@@ -99,8 +100,8 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/me');
   useCurrentPathname('/me');
 
 
@@ -122,18 +123,16 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
       <Head>
       <Head>
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
-      <div className="dynamic-layout-root mx-md-3">
-        <header className="py-3">
-          <div className="container">
-            <h1 className="title fs-3 mt-5">{ targetPage.title }</h1>
-          </div>
-        </header>
+      <div className="dynamic-layout-root">
+        <GroundGlassBar className="sticky-top py-4"></GroundGlassBar>
+
+        <div className="main ps-sidebar">
+          <div className="container-lg wide-gutter-x-lg">
 
 
-        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+            <h1 className="sticky-top py-2 fs-3">{ targetPage.title }</h1>
 
 
-        <div id="main" className="main">
-          <div id="content-main" className="content-main container">
             {targetPage.component}
             {targetPage.component}
+
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 2 - 2
apps/app/src/pages/reset-password.page.tsx

@@ -19,8 +19,8 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   return (
   return (
-    <div id="main" className="main">
-      <div id="content-main" className="content-main container-lg">
+    <div className="main">
+      <div className="container-lg">
         <div className="container">
         <div className="container">
           <div className="row justify-content-md-center">
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
             <div className="col-md-6 mt-5">

+ 0 - 2
apps/app/src/pages/share/[[...path]].page.tsx

@@ -124,8 +124,6 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
           <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
           <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
         </nav>
         </nav>
 
 
-        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
-
         <ShareLinkPageView
         <ShareLinkPageView
           pagePath={pagePath}
           pagePath={pagePath}
           rendererConfig={props.rendererConfig}
           rendererConfig={props.rendererConfig}

+ 34 - 26
apps/app/src/pages/tags.page.tsx

@@ -2,13 +2,14 @@ import type { ReactNode } from 'react';
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
 
 
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { LoadingSpinner } from '~/components/LoadingSpinner';
+import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IDataTagCount } from '~/interfaces/tag';
 import type { IDataTagCount } from '~/interfaces/tag';
@@ -49,8 +50,8 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/tags');
   useCurrentPathname('/tags');
 
 
@@ -83,30 +84,37 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
       <div className="dynamic-layout-root">
       <div className="dynamic-layout-root">
-        <div className="container-lg mb-5 pb-5" data-testid="tags-page">
-          <h2 className="my-3">{`${t('Tags')}(${totalCount})`}</h2>
-          <div className="px-3 mb-5 text-center">
-            <TagCloudBox tags={tagData} minSize={20} />
+        <GroundGlassBar className="sticky-top py-4"></GroundGlassBar>
+
+        <div className="main ps-sidebar" data-testid="tags-page">
+          <div className="container-lg wide-gutter-x-lg">
+
+            <h2 className="sticky-top py-1">
+              {`${t('Tags')}(${totalCount})`}
+            </h2>
+
+            <div className="px-3 mb-5 text-center">
+              <TagCloudBox tags={tagData} minSize={20} />
+            </div>
+            { isLoading
+              ? (
+                <div className="text-muted text-center">
+                  <LoadingSpinner className="mt-3 fs-3" />
+                </div>
+              )
+              : (
+                <div data-testid="grw-tags-list">
+                  <TagList
+                    tagData={tagData}
+                    totalTags={totalCount}
+                    activePage={activePage}
+                    onChangePage={setOffsetByPageNumber}
+                    pagingLimit={PAGING_LIMIT}
+                  />
+                </div>
+              )
+            }
           </div>
           </div>
-          { isLoading
-            ? (
-              <div className="text-muted text-center">
-                <LoadingSpinner className="mt-3 fs-3" />
-              </div>
-            )
-            : (
-              <div data-testid="grw-tags-list">
-                <TagList
-                  tagData={tagData}
-                  totalTags={totalCount}
-                  activePage={activePage}
-                  onChangePage={setOffsetByPageNumber}
-                  pagingLimit={PAGING_LIMIT}
-                />
-              </div>
-            )
-          }
-          <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         </div>
         </div>
       </div>
       </div>
     </>
     </>

+ 12 - 13
apps/app/src/pages/trash.page.tsx

@@ -1,13 +1,14 @@
 import type { ReactNode } from 'react';
 import type { ReactNode } from 'react';
 import React from 'react';
 import React from 'react';
 
 
-import type { IUser, IUserHasId } from '@growi/core';
+import type { IUser } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
 import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import { PagePathNavSticky } from '~/components/Common/PagePathNav';
+import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
@@ -43,8 +44,10 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
+  useCurrentPageId(null);
+  useCurrentPathname('/trash');
 
 
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiCloudUri(props.growiCloudUri);
 
 
@@ -53,8 +56,6 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
 
   useIsSearchPage(false);
   useIsSearchPage(false);
-  useCurrentPageId(null);
-  useCurrentPathname('/trash');
 
 
   // init sidebar config with UserUISettings and sidebarConfig
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
@@ -69,16 +70,14 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
       <div className="dynamic-layout-root">
       <div className="dynamic-layout-root">
-        <nav className="sticky-top">
-          TODO: implement navigation for /trash
-        </nav>
+        <GroundGlassBar className="sticky-top py-4"></GroundGlassBar>
 
 
-        <div className="content-main container-lg mb-5 pb-5">
-          <PagePathNavSticky pagePath="/trash" />
-          <TrashPageList />
+        <div className="main ps-sidebar">
+          <div className="container-lg wide-gutter-x-lg">
+            <PagePathNavSticky pagePath="/trash" />
+            <TrashPageList />
+          </div>
         </div>
         </div>
-
-        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
       </div>
       </div>
     </>
     </>
   );
   );

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

@@ -48,6 +48,26 @@
   }
   }
 }
 }
 
 
+// md/lg layout padding
+.ps-sidebar {
+  @include bs.media-breakpoint-between(md, xl) {
+    padding-left: var.$grw-sidebar-nav-width;
+  }
+}
+
+.wide-gutter-x-lg {
+  @include bs.media-breakpoint-up(lg) {
+    &,
+    .container,
+    .container-fluid,
+    .container-xxl,
+    .container-xl,
+    .container-lg {
+      --bs-gutter-x: 3rem;
+    }
+  }
+}
+
 // printable style
 // printable style
 @media print {
 @media print {
   body {
   body {

+ 2 - 0
apps/app/src/styles/vendor.scss

@@ -1,5 +1,7 @@
 @import '@growi/core/scss/bootstrap/apply';
 @import '@growi/core/scss/bootstrap/apply';
 
 
+@import '@growi/ui/dist/style';
+
 // react-bootstrap-typeahead
 // react-bootstrap-typeahead
 @import 'react-bootstrap-typeahead/css/Typeahead';
 @import 'react-bootstrap-typeahead/css/Typeahead';
 
 

+ 2 - 0
apps/app/turbo.json

@@ -4,6 +4,7 @@
   "pipeline": {
   "pipeline": {
 
 
     "styles-prebuilt": {
     "styles-prebuilt": {
+      "dependsOn": ["@growi/ui#build"],
       "outputs": ["src/styles/prebuilt/**"],
       "outputs": ["src/styles/prebuilt/**"],
       "inputs": [
       "inputs": [
         "src/styles/**/*.scss",
         "src/styles/**/*.scss",
@@ -24,6 +25,7 @@
       "outputMode": "new-only"
       "outputMode": "new-only"
     },
     },
     "dev:styles-prebuilt": {
     "dev:styles-prebuilt": {
+      "dependsOn": ["@growi/ui#dev"],
       "outputs": ["src/styles/prebuilt/**"],
       "outputs": ["src/styles/prebuilt/**"],
       "inputs": [
       "inputs": [
         "src/styles/**/*.scss",
         "src/styles/**/*.scss",

+ 1 - 0
packages/custom-icons/svg/facebook.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:none;}</style></defs><g transform="translate(-372 -420)"><path class="a" d="M20,10A10,10,0,1,0,7.584,19.706v-6.65H5.522V10H7.584V8.683c0-3.4,1.54-4.981,4.882-4.981a11.026,11.026,0,0,1,2.174.248v2.77c-.236-.025-.646-.037-1.155-.037-1.64,0-2.273.621-2.273,2.236V10h3.266l-.561,3.056H11.211v6.871A10,10,0,0,0,20,10" transform="translate(374 422)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/github.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a,.b{fill:none;}.a{fill-rule:evenodd;}</style></defs><g transform="translate(-207 -420)"><path class="a" d="M10.008,0A10.093,10.093,0,0,0,6.843,19.648c.5.1.679-.217.679-.484,0-.234-.016-1.035-.016-1.87-2.784.6-3.364-1.2-3.364-1.2a2.576,2.576,0,0,0-1.11-1.469c-.911-.618.066-.618.066-.618A2.1,2.1,0,0,1,4.64,15.04a2.12,2.12,0,0,0,2.916.835,2.148,2.148,0,0,1,.63-1.352c-2.22-.234-4.557-1.1-4.557-4.975a3.947,3.947,0,0,1,1.027-2.7,3.667,3.667,0,0,1,.1-2.671s.845-.267,2.75,1.035a9.541,9.541,0,0,1,5,0c1.906-1.3,2.751-1.035,2.751-1.035a3.667,3.667,0,0,1,.1,2.671,3.872,3.872,0,0,1,1.027,2.7c0,3.873-2.336,4.724-4.573,4.975a2.4,2.4,0,0,1,.679,1.87c0,1.352-.016,2.437-.016,2.771,0,.267.182.584.679.484A10.093,10.093,0,0,0,10.008,0Z" transform="translate(209 422)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/google.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:none;}</style></defs><g transform="translate(-291 -420)"><path class="a" d="M19.418,8.182H10V12.05h5.382a4.6,4.6,0,0,1-2,3.018A6.026,6.026,0,1,1,10,3.977a5.4,5.4,0,0,1,3.822,1.5l2.869-2.868A9.611,9.611,0,0,0,10,0a10,10,0,1,0,0,20,9.544,9.544,0,0,0,6.618-2.423,9.75,9.75,0,0,0,2.982-7.35,11.507,11.507,0,0,0-.182-2.045" transform="translate(293.2 422)"/></g></svg>

+ 1 - 0
packages/custom-icons/svg/slack.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:none;}</style></defs><g transform="translate(-131 -420)"><g transform="translate(133 422)"><path class="a" d="M4.2,66.8a2.1,2.1,0,1,1-2.1-2.1H4.2Z" transform="translate(0 -54.162)"/><path class="a" d="M32.3,66.8a2.1,2.1,0,1,1,4.2,0v5.261a2.1,2.1,0,1,1-4.2,0V66.8" transform="translate(-27.039 -54.162)"/><path class="a" d="M34.4,4.2a2.1,2.1,0,1,1,2.1-2.1V4.2Z" transform="translate(-27.039)"/><path class="a" d="M7.362,32.3a2.1,2.1,0,1,1,0,4.2H2.1a2.1,2.1,0,1,1,0-4.2H7.362" transform="translate(0 -27.039)"/><path class="a" d="M97,34.4a2.1,2.1,0,1,1,2.1,2.1H97Z" transform="translate(-81.202 -27.039)"/><path class="a" d="M68.9,7.362a2.1,2.1,0,1,1-4.2,0V2.1a2.1,2.1,0,1,1,4.2,0Z" transform="translate(-54.162)"/><path class="a" d="M66.8,97a2.1,2.1,0,1,1-2.1,2.1V97Z" transform="translate(-54.162 -81.201)"/><path class="a" d="M66.8,68.9a2.1,2.1,0,1,1,0-4.2h5.261a2.1,2.1,0,1,1,0,4.2Z" transform="translate(-54.162 -54.162)"/></g></g></svg>

+ 9 - 5
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -41,11 +41,13 @@ export type CodeMirrorEditorProps = {
 
 
 type Props = CodeMirrorEditorProps & {
 type Props = CodeMirrorEditorProps & {
   editorKey: string | GlobalCodeMirrorEditorKey,
   editorKey: string | GlobalCodeMirrorEditorKey,
+  hideToolbar?: boolean,
 }
 }
 
 
 export const CodeMirrorEditor = (props: Props): JSX.Element => {
 export const CodeMirrorEditor = (props: Props): JSX.Element => {
   const {
   const {
     editorKey,
     editorKey,
+    hideToolbar,
     acceptedUploadFileType = AcceptedUploadFileType.NONE,
     acceptedUploadFileType = AcceptedUploadFileType.NONE,
     indentSize,
     indentSize,
     editorSettings,
     editorSettings,
@@ -209,11 +211,13 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
         <FileDropzoneOverlay isEnabled={isDragActive} />
         <FileDropzoneOverlay isEnabled={isDragActive} />
         <CodeMirrorEditorContainer ref={containerRef} />
         <CodeMirrorEditorContainer ref={containerRef} />
       </div>
       </div>
-      <Toolbar
-        editorKey={editorKey}
-        acceptedUploadFileType={acceptedUploadFileType}
-        onUpload={onUpload}
-      />
+      { !hideToolbar && (
+        <Toolbar
+          editorKey={editorKey}
+          acceptedUploadFileType={acceptedUploadFileType}
+          onUpload={onUpload}
+        />
+      ) }
     </div>
     </div>
   );
   );
 };
 };

+ 39 - 0
packages/editor/src/components/CodeMirrorEditorReadOnly.tsx

@@ -0,0 +1,39 @@
+import { useEffect } from 'react';
+
+import { type Extension, EditorState } from '@codemirror/state';
+
+import { GlobalCodeMirrorEditorKey } from '../consts';
+import { setDataLine } from '../services/extensions/setDataLine';
+import { useCodeMirrorEditorIsolated } from '../stores';
+
+import { CodeMirrorEditor } from '.';
+
+const additionalExtensions: Extension[] = [
+  [
+    setDataLine,
+    EditorState.readOnly.of(true),
+  ],
+];
+
+type Props = {
+  markdown?: string,
+  onScroll?: () => void,
+}
+
+export const CodeMirrorEditorReadOnly = ({ markdown, onScroll }: Props): JSX.Element => {
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.READONLY);
+
+  codeMirrorEditor?.initDoc(markdown);
+
+  useEffect(() => {
+    return codeMirrorEditor?.appendExtensions?.(additionalExtensions);
+  }, [codeMirrorEditor]);
+
+  return (
+    <CodeMirrorEditor
+      hideToolbar
+      editorKey={GlobalCodeMirrorEditorKey.READONLY}
+      onScroll={onScroll}
+    />
+  );
+};

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

@@ -1,5 +1,6 @@
 export * from './CodeMirrorEditor';
 export * from './CodeMirrorEditor';
 export * from './CodeMirrorEditorMain';
 export * from './CodeMirrorEditorMain';
+export * from './CodeMirrorEditorReadOnly';
 export * from './CodeMirrorEditorComment';
 export * from './CodeMirrorEditorComment';
 export * from './CodeMirrorEditorDiff';
 export * from './CodeMirrorEditorDiff';
 export * from './MergeViewer';
 export * from './MergeViewer';

+ 1 - 0
packages/editor/src/consts/global-code-mirror-editor-key.ts

@@ -2,5 +2,6 @@ export const GlobalCodeMirrorEditorKey = {
   MAIN: 'main',
   MAIN: 'main',
   COMMENT: 'comment',
   COMMENT: 'comment',
   DIFF: 'diff',
   DIFF: 'diff',
+  READONLY: 'readonly',
 } as const;
 } as const;
 export type GlobalCodeMirrorEditorKey = typeof GlobalCodeMirrorEditorKey[keyof typeof GlobalCodeMirrorEditorKey]
 export type GlobalCodeMirrorEditorKey = typeof GlobalCodeMirrorEditorKey[keyof typeof GlobalCodeMirrorEditorKey]

+ 17 - 15
packages/editor/src/services/paste-util/paste-markdown-util.ts

@@ -20,28 +20,30 @@ export const adjustPasteData = (strFromBol: string, text: string): string => {
 
 
   let adjusted = text;
   let adjusted = text;
 
 
-  if (text.match(indentAndMarkRE)) {
-    const matchResult = strFromBol.match(indentAndMarkRE);
-    const indent = matchResult ? matchResult[1] : '';
+  if (indentAndMarkOnlyRE.test(strFromBol)) {
+    if (text.match(indentAndMarkRE)) {
+      const matchResult = strFromBol.match(indentAndMarkRE);
+      const indent = matchResult ? matchResult[1] : '';
 
 
-    const lines = text.match(/[^\r\n]+/g);
+      const lines = text.match(/[^\r\n]+/g);
 
 
-    const replacedLines = lines?.map((line, index) => {
+      const replacedLines = lines?.map((line, index) => {
 
 
-      if (index === 0 && strFromBol.match(indentAndMarkOnlyRE)) {
-        return line.replace(indentAndMarkRE, '');
-      }
+        if (index === 0 && strFromBol.match(indentAndMarkOnlyRE)) {
+          return line.replace(indentAndMarkRE, '');
+        }
 
 
-      return indent + line;
-    });
+        return indent + line;
+      });
 
 
-    adjusted = replacedLines ? replacedLines.join('\n') : '';
-  }
+      adjusted = replacedLines ? replacedLines.join('\n') : '';
+    }
 
 
-  else if (strFromBol.match(indentAndMarkRE)) {
-    const replacedText = text.replace(/(\r\n|\r|\n)/g, `$1${strFromBol}`);
+    else {
+      const replacedText = text.replace(/(\r\n|\r|\n)/g, `$1${strFromBol}`);
 
 
-    adjusted = replacedText;
+      adjusted = replacedText;
+    }
   }
   }
 
 
   return adjusted;
   return adjusted;

+ 3 - 1
packages/remark-attachment-refs/package.json

@@ -33,7 +33,9 @@
     "dev": "run-p dev:*",
     "dev": "run-p dev:*",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
-    "watch": "yarn dev -w --emptyOutDir=false",
+    "watch": "run-p watch:*",
+    "watch:client": "yarn dev:client -w --emptyOutDir=false",
+    "watch:server": "yarn dev:server -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",
     "lint:styles": "stylelint src/**/*.scss src/**/*.css",
     "lint:styles": "stylelint src/**/*.scss src/**/*.css",
     "lint:typecheck": "tsc",
     "lint:typecheck": "tsc",

+ 2 - 2
packages/remark-attachment-refs/src/client/components/AttachmentList.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
 import type { IAttachmentHasId } from '@growi/core';
 import type { IAttachmentHasId } from '@growi/core';
-import { Attachment } from '@growi/ui/dist/components';
+import { Attachment, LoadingSpinner } from '@growi/ui/dist/components';
 
 
 import { ExtractedAttachments } from './ExtractedAttachments';
 import { ExtractedAttachments } from './ExtractedAttachments';
 import { RefsContext } from './util/refs-context';
 import { RefsContext } from './util/refs-context';
@@ -43,7 +43,7 @@ export const AttachmentList = ({
     if (isLoading) {
     if (isLoading) {
       return (
       return (
         <div className="text-muted">
         <div className="text-muted">
-          <i className="fa fa-spinner fa-pulse me-1"></i>
+          <LoadingSpinner className="me-1" />
           <span className="attachment-refs-blink">{refsContext.toString()}</span>
           <span className="attachment-refs-blink">{refsContext.toString()}</span>
         </div>
         </div>
       );
       );

+ 3 - 1
packages/remark-lsx/src/client/components/Lsx.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback, useMemo } from 'react';
 import React, { useCallback, useMemo } from 'react';
 
 
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
+
 import { useSWRxLsx } from '../stores/lsx';
 import { useSWRxLsx } from '../stores/lsx';
 import { generatePageNodeTree } from '../utils/page-node';
 import { generatePageNodeTree } from '../utils/page-node';
 
 
@@ -71,7 +73,7 @@ const LsxSubstance = React.memo(({
     return (
     return (
       <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
       <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
         <small>
         <small>
-          <i className="fa fa-spinner fa-pulse me-1"></i>
+          <LoadingSpinner className="me-1" />
           {lsxContext.toString()}
           {lsxContext.toString()}
         </small>
         </small>
       </div>
       </div>

+ 3 - 0
packages/ui/package.json

@@ -21,6 +21,9 @@
     "./dist/utils": {
     "./dist/utils": {
       "import": "./dist/utils/index.js"
       "import": "./dist/utils/index.js"
     },
     },
+    "./dist/style": {
+      "import": "./dist/style.css"
+    },
     "./scss/*": "./scss/*.scss"
     "./scss/*": "./scss/*.scss"
   },
   },
   "scripts": {
   "scripts": {

+ 1 - 0
apps/app/src/components/LoadingSpinner.module.scss → packages/ui/src/components/LoadingSpinner.module.scss

@@ -37,3 +37,4 @@
     transform: rotate(360deg);
     transform: rotate(360deg);
   }
   }
 }
 }
+

+ 9 - 0
packages/ui/src/components/LoadingSpinner.tsx

@@ -0,0 +1,9 @@
+import type { ComponentPropsWithoutRef } from 'react';
+
+import styles from './LoadingSpinner.module.scss';
+
+const moduleClass = styles.spinner ?? '';
+
+export const LoadingSpinner = ({ className = '' }: ComponentPropsWithoutRef<'span'>): JSX.Element => (
+  <span className={`material-symbols-outlined pb-0 ${moduleClass} ${className}`}>progress_activity</span>
+);

+ 1 - 0
packages/ui/src/components/index.ts

@@ -1,4 +1,5 @@
 export * from './Attachment';
 export * from './Attachment';
 export * from './FootstampIcon';
 export * from './FootstampIcon';
 export * from './PagePath';
 export * from './PagePath';
+export * from './LoadingSpinner';
 export * from './UserPicture';
 export * from './UserPicture';