Bladeren bron

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

reiji-h 2 jaren geleden
bovenliggende
commit
4955bf16ee
51 gewijzigde bestanden met toevoegingen van 609 en 310 verwijderingen
  1. 1 0
      .devcontainer/Dockerfile
  2. 0 1
      apps/app/package.json
  3. 3 0
      apps/app/resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2
  4. 1 1
      apps/app/src/client/services/page-operation.ts
  5. 3 4
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  6. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  7. 4 21
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  8. 23 0
      apps/app/src/components/FontFamily/GlobalFonts.tsx
  9. 1 0
      apps/app/src/components/FontFamily/types.d.ts
  10. 20 0
      apps/app/src/components/FontFamily/use-lato.tsx
  11. 18 0
      apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx
  12. 23 0
      apps/app/src/components/FontFamily/use-source-han-code-jp.tsx
  13. 9 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  14. 9 5
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  15. 7 4
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  16. 32 24
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  17. 58 0
      apps/app/src/components/Navbar/hooks.tsx
  18. 0 3
      apps/app/src/components/Page/DisplaySwitcher.tsx
  19. 17 10
      apps/app/src/components/PageControls/BookmarkButtons.module.scss
  20. 5 3
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  21. 16 10
      apps/app/src/components/PageControls/LikeButtons.module.scss
  22. 3 3
      apps/app/src/components/PageControls/LikeButtons.tsx
  23. 11 23
      apps/app/src/components/PageControls/PageControls.module.scss
  24. 16 11
      apps/app/src/components/PageControls/SeenUserInfo.module.scss
  25. 2 4
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  26. 12 10
      apps/app/src/components/PageControls/SubscribeButton.module.scss
  27. 3 1
      apps/app/src/components/PageControls/SubscribeButton.tsx
  28. 34 0
      apps/app/src/components/PageControls/_button-styles.scss
  29. 42 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss
  30. 8 4
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  31. 46 39
      apps/app/src/components/PageEditor/PageEditor.tsx
  32. 30 0
      apps/app/src/components/PageEditor/Preview.module.scss
  33. 6 1
      apps/app/src/components/PageEditor/Preview.tsx
  34. 43 7
      apps/app/src/components/Sidebar/PageCreateButton.tsx
  35. 1 1
      apps/app/src/components/TreeItem/SimpleItem.tsx
  36. 1 0
      apps/app/src/interfaces/page-operation.ts
  37. 2 26
      apps/app/src/pages/_app.page.tsx
  38. 31 3
      apps/app/src/server/routes/apiv3/pages.js
  39. 0 72
      apps/app/src/styles/_editor.scss
  40. 23 0
      apps/app/src/styles/_fonts.scss
  41. 1 0
      apps/app/src/styles/_mixins.scss
  42. 0 2
      apps/app/src/styles/font-icons.scss
  43. 14 0
      apps/app/src/styles/mixins/_editing.scss
  44. 1 0
      apps/app/src/styles/style-app.scss
  45. 1 0
      packages/editor/index.html
  46. 0 1
      packages/editor/package.json
  47. 4 0
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.module.scss
  48. 9 4
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  49. 14 2
      packages/editor/src/components/CodeMirrorEditor/Toolbar/scss/toolbar-button.scss
  50. 0 4
      packages/editor/src/main.scss
  51. 0 5
      yarn.lock

+ 1 - 0
.devcontainer/Dockerfile

@@ -50,6 +50,7 @@ RUN apt-get update \
     && rm -rf /var/lib/apt/lists/*
 ENV DEBIAN_FRONTEND=dialog
 
+RUN git-lfs pull
 RUN yarn global add turbo
 RUN yarn global add node-gyp
 

+ 0 - 1
apps/app/package.json

@@ -212,7 +212,6 @@
     "@growi/ui": "link:../../packages/ui",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
-    "@material-symbols/font-300": "^0.13.1",
     "@next/bundle-analyzer": "^13.2.3",
     "@swc-node/jest": "^1.6.2",
     "@swc/jest": "^0.2.24",

+ 3 - 0
apps/app/resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b607eb2ff757a116a1bf6bfac3702b38c8d5b2d20caa36654c31e8116c299ee7
+size 868836

+ 1 - 1
apps/app/src/client/services/page-operation.ts

@@ -88,7 +88,7 @@ export const resumeRenameOperation = async(pageId: string): Promise<void> => {
 };
 
 // TODO: define return type
-const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
+export const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
   // clone
   const params = Object.assign(tmpParams, {
     path: pagePath,

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

@@ -16,10 +16,8 @@ type Props = {
 
 const CustomizePresentationSetting = (props: Props): JSX.Element => {
   const { adminCustomizeContainer } = props;
-
-  console.log(adminCustomizeContainer);
-
   const { t } = useTranslation();
+
   const onClickSubmit = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizePresentation();
@@ -28,7 +26,8 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
     catch (err) {
       toastError(err);
     }
-  }, [adminCustomizeContainer]);
+  }, [adminCustomizeContainer, t]);
+
   return (
     <React.Fragment>
       <h2 className="admin-setting-header">{t('admin:customize_settings.custom_presentation')}</h2>

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -233,7 +233,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
                 onClick={loadChildFolder}
               >
                 <div className="d-flex justify-content-center">
-                  <span className="material-symbols-rounded">arrow_right</span>
+                  <span className="material-symbols-outlined">arrow_right</span>
                 </div>
               </button>
             )}

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

@@ -266,7 +266,6 @@ PageItemControlDropdownMenu.displayName = 'PageItemControl';
 
 type PageItemControlSubstanceProps = CommonProps & {
   pageId: string,
-  fetchOnInit?: boolean,
   children?: React.ReactNode,
   operationProcessData?: IPageOperationProcessData,
 }
@@ -274,12 +273,12 @@ type PageItemControlSubstanceProps = CommonProps & {
 export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
 
   const {
-    pageId, pageInfo: presetPageInfo, fetchOnInit, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
+    pageId, pageInfo: presetPageInfo, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
     onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
   } = props;
 
   const [isOpen, setIsOpen] = useState(false);
-  const [shouldFetch, setShouldFetch] = useState(fetchOnInit ?? false);
+  const [shouldFetch, setShouldFetch] = useState(false);
 
   const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
 
@@ -336,10 +335,10 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 
   return (
     <NotAvailableForGuest>
-      <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
+      <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} className="grw-page-item-control" data-testid="open-page-item-control-btn">
         { children ?? (
           <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
-            <i className="icon-options"></i>
+            <span className="material-symbols-outlined">more_vert</span>
           </DropdownToggle>
         ) }
 
@@ -377,19 +376,3 @@ export const PageItemControl = (props: PageItemControlProps): JSX.Element => {
 
   return <PageItemControlSubstance pageId={pageId} {...props} />;
 };
-
-
-type AsyncPageItemControlProps = Omit<CommonProps, 'pageInfo'> & {
-  pageId?: string,
-  children?: React.ReactNode,
-}
-
-export const AsyncPageItemControl = (props: AsyncPageItemControlProps): JSX.Element => {
-  const { pageId } = props;
-
-  if (pageId == null) {
-    return <></>;
-  }
-
-  return <PageItemControlSubstance pageId={pageId} fetchOnInit {...props} />;
-};

+ 23 - 0
apps/app/src/components/FontFamily/GlobalFonts.tsx

@@ -0,0 +1,23 @@
+import { memo } from 'react';
+
+import { useLatoFontFamily } from './use-lato';
+import { useMaterialSymbolsOutlined } from './use-material-symbols-outlined';
+import { useSourceHanCodeJP } from './use-source-han-code-jp';
+
+/**
+ * Define prefixed by '--grw-font-family'
+ */
+export const GlobalFonts = memo((): JSX.Element => {
+
+  const latoFontFamily = useLatoFontFamily();
+  const sourceHanCodeJPFontFamily = useSourceHanCodeJP();
+  const materialSymbolsOutlinedFontFamily = useMaterialSymbolsOutlined();
+
+  return (
+    <>
+      {latoFontFamily}
+      {sourceHanCodeJPFontFamily}
+      {materialSymbolsOutlinedFontFamily}
+    </>
+  );
+});

+ 1 - 0
apps/app/src/components/FontFamily/types.d.ts

@@ -0,0 +1 @@
+export type DefineStyle = () => JSX.IntrinsicElements.style;

+ 20 - 0
apps/app/src/components/FontFamily/use-lato.tsx

@@ -0,0 +1,20 @@
+import { Lato } from 'next/font/google';
+
+import { DefineStyle } from './types';
+
+const lato = Lato({
+  weight: ['400', '700'],
+  style: ['normal', 'italic'],
+  subsets: ['latin'],
+  display: 'swap',
+});
+
+export const useLatoFontFamily: DefineStyle = () => (
+  <style jsx global>
+    {`
+      :root {
+        --grw-font-family-lato: ${lato.style.fontFamily};
+      }
+    `}
+  </style>
+);

+ 18 - 0
apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx

@@ -0,0 +1,18 @@
+import localFont from 'next/font/local';
+
+import { DefineStyle } from './types';
+
+const materialSymbolsOutlined = localFont({
+  src: '../../../resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2',
+  adjustFontFallback: false,
+});
+
+export const useMaterialSymbolsOutlined: DefineStyle = () => (
+  <style jsx global>
+    {`
+      :root {
+        --grw-font-family-material-symbols-outlined: ${materialSymbolsOutlined.style.fontFamily};
+      }
+    `}
+  </style>
+);

+ 23 - 0
apps/app/src/components/FontFamily/use-source-han-code-jp.tsx

@@ -0,0 +1,23 @@
+import localFont from 'next/font/local';
+
+import { DefineStyle } from './types';
+
+const sourceHanCodeJPSubsetMain = localFont({
+  src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',
+  display: 'optional',
+});
+const sourceHanCodeJPSubsetJis2 = localFont({
+  src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2',
+  display: 'optional',
+});
+
+export const useSourceHanCodeJP: DefineStyle = () => (
+  <style jsx global>
+    {`
+      :root {
+        --grw-font-family-source-han-code-jp-subset-main: ${sourceHanCodeJPSubsetMain.style.fontFamily};
+        --grw-font-family-source-han-code-jp-subset-jis2: ${sourceHanCodeJPSubsetJis2.style.fontFamily};
+      }
+    `}
+  </style>
+);

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

@@ -1,4 +1,13 @@
+@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() {
+  .grw-contextual-sub-navigation {
+    position: fixed;
+    right: 0;
+  }
+}

+ 9 - 5
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -28,6 +28,7 @@ import { mutatePageTree } from '~/stores/page-listing';
 import {
   useEditorMode, useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
+  useSelectedGrant,
 } from '~/stores/ui';
 
 import CreateTemplateModal from '../CreateTemplateModal';
@@ -41,7 +42,6 @@ import { Skeleton } from '../Skeleton';
 import styles from './GrowiContextualSubNavigation.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
-
 const PageEditorModeManager = dynamic(
   () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
@@ -190,13 +190,14 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
 
-  const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+  const { data: editorMode } = useEditorMode();
   const { data: pageId } = useCurrentPageId();
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isContainerFluid } = useIsContainerFluid();
+  const { data: grantData } = useSelectedGrant();
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
@@ -213,6 +214,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId);
 
   const path = currentPage?.path ?? currentPathname;
+  const grant = currentPage?.grant ?? grantData?.grant;
+  const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;
 
   // TODO: implement tags for editor
   // refs: https://redmine.weseek.co.jp/issues/132125
@@ -236,7 +239,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const { isLinkSharingDisabled } = props;
 
-
   // TODO: implement tags for editor
   // refs: https://redmine.weseek.co.jp/issues/132125
   // const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
@@ -328,7 +330,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   return (
     <>
       <div
-        className={`grw-contextual-sub-navigation ${styles['grw-contextual-sub-navigation']}
+        className={`${styles['grw-contextual-sub-navigation']}
           d-flex align-items-center justify-content-end px-2 py-1 gap-2 gap-md-4
         `}
         data-testid="grw-contextual-sub-nav"
@@ -355,7 +357,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
           <PageEditorModeManager
             editorMode={editorMode}
             isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
-            onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
+            path={path}
+            grant={grant}
+            grantUserGroupId={grantUserGroupId}
           />
         )}
       </div>

+ 7 - 4
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

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

+ 32 - 24
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -1,26 +1,27 @@
-import React, { type ReactNode, useCallback } from 'react';
+import React, { type ReactNode, useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 
+import { useOnPageEditorModeButtonClicked } from './hooks';
+
 import styles from './PageEditorModeManager.module.scss';
 
 
 type PageEditorModeButtonProps = {
   currentEditorMode: EditorMode,
   editorMode: EditorMode,
-  icon: ReactNode,
-  label: ReactNode,
+  children?: ReactNode,
   isBtnDisabled?: boolean,
   onClick?: (mode: EditorMode) => void,
 }
 const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
   const {
-    currentEditorMode, isBtnDisabled, editorMode, icon, label, onClick,
+    currentEditorMode, isBtnDisabled, editorMode, children, onClick,
   } = props;
 
-  const classNames = ['btn btn-outline-primary px-1'];
+  const classNames = ['btn btn-outline-primary py-1 px-2 d-flex align-items-center justify-content-center'];
   if (currentEditorMode === editorMode) {
     classNames.push('active');
   }
@@ -35,36 +36,43 @@ const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
       onClick={() => onClick?.(editorMode)}
       data-testid={`${editorMode}-button`}
     >
-      <span className="me-1">{icon}</span>
-      <span>{label}</span>
+      {children}
     </button>
   );
 });
 
 type Props = {
   editorMode: EditorMode | undefined,
-  onPageEditorModeButtonClicked?: (editorMode: EditorMode) => void,
-  isBtnDisabled?: boolean,
+  isBtnDisabled: boolean,
+  path?: string,
+  grant?: number,
+  grantUserGroupId?: string
 }
 
 export const PageEditorModeManager = (props: Props): JSX.Element => {
   const {
     editorMode = EditorMode.View,
     isBtnDisabled,
-    onPageEditorModeButtonClicked,
+    path,
+    grant,
+    grantUserGroupId,
   } = props;
 
   const { t } = useTranslation();
+  const [isCreating, setIsCreating] = useState(false);
+
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
 
-  const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
-    if (isBtnDisabled ?? false) {
+  const onPageEditorModeButtonClicked = useOnPageEditorModeButtonClicked(setIsCreating, path, grant, grantUserGroupId);
+  const _isBtnDisabled = isCreating || isBtnDisabled;
+
+  const pageEditorModeButtonClickedHandler = useCallback((viewType: EditorMode) => {
+    if (_isBtnDisabled) {
       return;
     }
-    if (onPageEditorModeButtonClicked != null) {
-      onPageEditorModeButtonClicked(viewType);
-    }
-  }, [isBtnDisabled, onPageEditorModeButtonClicked]);
+
+    onPageEditorModeButtonClicked?.(viewType);
+  }, [_isBtnDisabled, onPageEditorModeButtonClicked]);
 
   return (
     <>
@@ -78,21 +86,21 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
           <PageEditorModeButton
             currentEditorMode={editorMode}
             editorMode={EditorMode.View}
-            isBtnDisabled={isBtnDisabled}
+            isBtnDisabled={_isBtnDisabled}
             onClick={pageEditorModeButtonClickedHandler}
-            icon={<i className="icon-control-play" />}
-            label={t('view')}
-          />
+          >
+            <span className="material-symbols-outlined fs-4">play_arrow</span>{t('View')}
+          </PageEditorModeButton>
         )}
         {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && (
           <PageEditorModeButton
             currentEditorMode={editorMode}
             editorMode={EditorMode.Editor}
-            isBtnDisabled={isBtnDisabled}
+            isBtnDisabled={_isBtnDisabled}
             onClick={pageEditorModeButtonClickedHandler}
-            icon={<i className="icon-note" />}
-            label={t('Edit')}
-          />
+          >
+            <span className="material-symbols-outlined me-1 fs-5">edit_square</span>{t('Edit')}
+          </PageEditorModeButton>
         )}
       </div>
     </>

+ 58 - 0
apps/app/src/components/Navbar/hooks.tsx

@@ -0,0 +1,58 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+
+import { createPage } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/toastr';
+import { useIsNotFound } from '~/stores/page';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
+
+export const useOnPageEditorModeButtonClicked = (
+    setIsCreating:React.Dispatch<React.SetStateAction<boolean>>,
+    path?: string,
+    grant?: number,
+    grantUserGroupId?: string,
+): (editorMode: EditorMode) => Promise<void> => {
+  const router = useRouter();
+  const { t } = useTranslation('commons');
+  const { data: isNotFound } = useIsNotFound();
+  const { mutate: mutateEditorMode } = useEditorMode();
+
+  return useCallback(async(editorMode: EditorMode) => {
+    if (isNotFound == null || path == null || grant == null) {
+      return;
+    }
+
+    if (editorMode === EditorMode.Editor && isNotFound) {
+      try {
+        setIsCreating(true);
+
+        const params = {
+          isSlackEnabled: false,
+          slackChannels: '',
+          grant,
+          pageTags: [],
+          grantUserGroupId,
+        };
+
+        const response = await createPage(path, '', params);
+
+        // Should not mutateEditorMode as it might prevent transitioning during mutation
+        router.push(`${response.page.id}#edit`);
+      }
+      catch (err) {
+        logger.warn(err);
+        toastError(t('toaster.create_failed', { target: path }));
+      }
+      finally {
+        setIsCreating(false);
+      }
+    }
+
+    mutateEditorMode(editorMode);
+  }, [grant, grantUserGroupId, isNotFound, mutateEditorMode, path, router, setIsCreating, t]);
+};

+ 0 - 3
apps/app/src/components/Page/DisplaySwitcher.tsx

@@ -12,7 +12,6 @@ import { LazyRenderer } from '../Common/LazyRenderer';
 
 
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
-const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
 
 
 type Props = {
@@ -37,8 +36,6 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
         <PageEditor />
       </LazyRenderer>
-
-      { isEditable && !isViewMode && <EditorNavbarBottom /> }
     </>
   );
 };

+ 17 - 10
apps/app/src/components/PageControls/BookmarkButtons.module.scss

@@ -1,17 +1,24 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use './button-styles';
+
 .btn-group-bookmark :global {
   .btn-bookmark {
-    box-shadow: none !important;
-
-    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$orange, rgba(lighten(bs.$orange, 20%), 0.5), rgba(lighten(bs.$orange, 20%), 0.5));
+    @extend %btn-basis;
+  }
+  .dropdown .btn-bookmark {
+    padding-right: 1px;
+  }
+  .total-counts {
+    @extend %btn-total-counts-basis;
+    padding-left: 5px;
+  }
+}
 
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: bs.$orange;
-    }
-    &:not(:disabled):not(.disabled):not(:hover) {
-      background-color: transparent;
-    }
+// == Colors
+.btn-group-bookmark :global {
+  .btn-bookmark {
+    @include button-styles.btn-color(bs.$orange);
   }
 }
+

+ 5 - 3
apps/app/src/components/PageControls/BookmarkButtons.tsx

@@ -73,10 +73,12 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
         <DropdownToggle
           id="bookmark-dropdown-btn"
           color="transparent"
-          className={`shadow-none btn btn-bookmark border-0 rounded-end-0
+          className={`btn btn-bookmark rounded-end-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
-          <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
+          <span className={`material-symbols-outlined ${isBookmarked ? 'fill' : ''}`}>
+            bookmark
+          </span>
         </DropdownToggle>
       </BookmarkFolderMenu>
       <UncontrolledTooltip placement="top" data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
@@ -86,7 +88,7 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
       <button
         type="button"
         id="po-total-bookmarks"
-        className={`shadow-none btn btn-bookmark border-0
+        className={`btn btn-bookmark
           total-counts ${isBookmarked ? 'active' : ''}`}
       >
         {bookmarkCount}

+ 16 - 10
apps/app/src/components/PageControls/LikeButtons.module.scss

@@ -1,17 +1,23 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use './button-styles';
+
 .btn-group-like :global {
   .btn-like {
-    box-shadow: none !important;
-
-    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), lighten(bs.$red, 15%), rgba(lighten(bs.$red, 10%), 0.15), rgba(lighten(bs.$red, 10%), 0.5));
+    @extend %btn-basis;
+  }
+  .btn-like#like-button {
+    padding-right: 3px;
+  }
+  .total-counts {
+    @extend %btn-total-counts-basis;
+    padding-left: 5px;
+  }
+}
 
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: lighten(bs.$red, 15%);
-    }
-    &:not(:disabled):not(.disabled):not(:hover) {
-      background-color: transparent;
-    }
+// == Colors
+.btn-group-like :global {
+  .btn-like {
+    @include button-styles.btn-color(bs.$red);
   }
 }

+ 3 - 3
apps/app/src/components/PageControls/LikeButtons.tsx

@@ -46,10 +46,10 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         type="button"
         id="like-button"
         onClick={onLikeClicked}
-        className={`shadow-none btn btn-like border-0
+        className={`btn btn-like
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
-        <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>
+        <span className={`material-symbols-outlined ${isLiked ? 'fill' : ''}`}>favorite</span>
       </button>
 
       <UncontrolledTooltip data-testid="like-button-tooltip" placement="top" target="like-button" autohide={false} fade={false}>
@@ -59,7 +59,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
       <button
         type="button"
         id="po-total-likes"
-        className={`shadow-none btn btn-like border-0
+        className={`btn btn-like
           total-counts ${isLiked ? 'active' : ''}`}
       >
         {sumOfLikers}

+ 11 - 23
apps/app/src/components/PageControls/PageControls.module.scss

@@ -1,30 +1,18 @@
-%page-controls-buttons-height {
-  height: 40px;
-}
-
-.grw-page-controls :global {
-
-  .btn-subscribe {
-    --bs-btn-font-size: 18px;
-    @extend %page-controls-buttons-height;
-  }
-
-  .btn-like,
-  .btn-bookmark,
-  .btn-seen-user {
-    --bs-btn-font-size: 18px;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
-    @extend %page-controls-buttons-height;
-    padding-right: 6px;
-    padding-left: 8px;
-  }
+@use './button-styles';
 
-  .total-counts {
-    font-size: 13px;
+// PageItemControl styles
+.grw-page-controls :global {
+  .btn-page-item-control {
+    @extend %btn-basis;
   }
+}
 
+// == Colors
+// PageItemControl colors
+.grw-page-controls :global {
   .btn-page-item-control {
-    @extend %page-controls-buttons-height;
+    @include button-styles.btn-color(bs.$gray-500);
   }
-
 }

+ 16 - 11
apps/app/src/components/PageControls/SeenUserInfo.module.scss

@@ -1,18 +1,23 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
-@use '~/styles/atoms/mixins/buttons' as mixins-buttons;
+
+@use './button-styles';
 
 .grw-seen-user-info :global {
-  .btn.btn-seen-user {
-    $color-seen-user: #549c79;
+  .btn-seen-user {
+    @extend %btn-basis;
+  }
+  .total-counts {
+    @extend %text-total-counts-basis;
+  }
+}
 
-    @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
 
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: $color-seen-user;
-    }
-    &:not(:disabled):not(.disabled):not(:hover) {
-      background-color: transparent;
-    }
+// == Colors
+
+.grw-seen-user-info :global {
+  $color: #549c79;
+
+  .btn-seen-user {
+    @include button-styles.btn-color($color);
   }
 }

+ 2 - 4
apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -28,10 +28,8 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
 
   return (
     <div className={`grw-seen-user-info ${styles['grw-seen-user-info']}`}>
-      <button type="button" id="btn-seen-user" className="shadow-none btn btn-seen-user border-0">
-        <span className="me-1 footstamp-icon">
-          <FootstampIcon />
-        </span>
+      <button type="button" id="btn-seen-user" className="shadow-none btn btn-seen-user border-0 d-flex align-items-center">
+        <span className="material-symbols-outlined me-1">footprint</span>
         <span className="total-counts">{sumOfSeenUsers || seenUsers.length}</span>
       </button>
       <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-seen-user" toggle={togglePopover} trigger="legacy" disabled={disabled}>

+ 12 - 10
apps/app/src/components/PageControls/SubscribeButton.module.scss

@@ -1,14 +1,16 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
-.btn-subscribe {
-  &:global {
-    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$success, rgba(lighten(bs.$success, 10%), 0.15), rgba(lighten(bs.$success, 10%), 0.5));
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: lighten(bs.$success, 15%);
-    }
-    &:not(:disabled):not(.disabled):not(:hover) {
-      background-color: transparent;
-    }
+@use './button-styles';
+
+.btn-subscribe :global {
+  @extend %btn-basis;
+
+  .total-counts {
+    @extend %btn-total-counts-basis;
   }
 }
+
+// == Colors
+.btn-subscribe {
+  @include button-styles.btn-color(bs.$success);
+}

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

@@ -36,7 +36,9 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         className={`shadow-none btn btn-subscribe ${styles['btn-subscribe']} border-0
           ${isSubscribing ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
-        <i className={`fa ${isSubscribing ? 'fa-bell' : 'fa-bell-slash-o'}`}></i>
+        <span className={`material-symbols-outlined ${isSubscribing ? 'fill' : ''}`}>
+          {isSubscribing ? 'notifications' : 'notifications_off'}
+        </span>
       </button>
 
       <UncontrolledTooltip data-testid="subscribe-button-tooltip" placement="top" target="subscribe-button" fade={false}>

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

@@ -0,0 +1,34 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+%btn-basis {
+  --bs-btn-padding-x: 6px;
+  --bs-btn-padding-y: 8px;
+  --bs-btn-line-height: 1em;
+  --bs-btn-border-width: 0;
+  --bs-btn-box-shadow: none;
+}
+
+%btn-total-counts-basis {
+  --bs-btn-font-size: 13px;
+}
+
+%text-total-counts-basis {
+  font-size: 13px;
+}
+
+@mixin btn-color($color) {
+  $color-rgb: #{bs.to-rgb($color)};
+
+  --bs-btn-color: var(--bs-tertiary-color);
+  --bs-btn-bg: transparent;
+
+  --bs-btn-hover-color: #{$color};
+  --bs-btn-hover-bg: rgba(#{$color-rgb}, 0.2);
+
+  --bs-btn-active-color: #{$color};
+  --bs-btn-active-bg: transparent;
+
+  &:hover {
+    --bs-btn-active-bg: rgba(#{$color-rgb}, 0.2);
+  }
+}

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

@@ -0,0 +1,42 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+@use '~/styles/variables' as var;
+@use '~/styles/mixins';
+
+@include mixins.editing() {
+  .grw-editor-navbar-bottom :global {
+    height: var.$grw-editor-navbar-bottom-height;
+
+    .grw-grant-selector {
+      @include bs.media-breakpoint-down(sm) {
+        .btn .label {
+          display: none;
+        }
+      }
+      @include bs.media-breakpoint-up(md) {
+        .dropdown-toggle {
+          min-width: 100px;
+
+          // caret
+          &::after {
+            margin-left: 1em;
+          }
+        }
+      }
+    }
+
+    .btn-submit {
+      width: 100px;
+    }
+
+    .btn-expand {
+      // rotate icon
+      i {
+        display: inline-block;
+        transition: transform 200ms;
+      }
+      &.expand i {
+        transform: rotate(-180deg);
+      }
+    }
+  }
+}

+ 8 - 4
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -13,6 +13,11 @@ import {
 } from '~/stores/ui';
 
 
+import styles from './EditorNavbarBottom.module.scss';
+
+const moduleClass = styles['grw-editor-navbar-bottom'];
+
+
 const SavePageControls = dynamic<SavePageControlsProps>(() => import('~/components/SavePageControls').then(mod => mod.SavePageControls), { ssr: false });
 const SlackLogo = dynamic(() => import('~/components/SlackLogo').then(mod => mod.SlackLogo), { ssr: false });
 const SlackNotification = dynamic(() => import('~/components/SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
@@ -32,7 +37,6 @@ const EditorNavbarBottom = (): JSX.Element => {
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
 
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
-  const additionalClasses = ['grw-editor-navbar-bottom'];
 
   const [slackChannelsStr, setSlackChannelsStr] = useState<string>('');
 
@@ -83,7 +87,7 @@ const EditorNavbarBottom = (): JSX.Element => {
       {/* Collapsed SlackNotification */}
       {isSlackConfigured && (
         <Collapse isOpen={isSlackExpanded && isDeviceSmallerThanMd === true}>
-          <nav className={`navbar navbar-expand-lg border-top ${additionalClasses.join(' ')}`}>
+          <nav className={`navbar navbar-expand-lg border-top ${moduleClass}`}>
             {isSlackEnabled != null
             && (
               <SlackNotification
@@ -99,7 +103,7 @@ const EditorNavbarBottom = (): JSX.Element => {
         </Collapse>
       )
       }
-      <div className={`flex-expand-horiz align-items-center border-top px-2 px-md-3 ${additionalClasses.join(' ')}`}>
+      <div className={`flex-expand-horiz align-items-center border-top px-2 px-md-3 ${moduleClass}`}>
         <form>
           { isDeviceSmallerThanMd && renderDrawerButton() }
           { !isDeviceSmallerThanMd && <OptionsSelector /> }
@@ -139,7 +143,7 @@ const EditorNavbarBottom = (): JSX.Element => {
       { isCollapsedOptionsSelectorEnabled && (
         <Collapse isOpen={isExpanded}>
           <div className="px-2"> {/* set padding for border-top */}
-            <div className={`navbar navbar-expand border-top px-0 ${additionalClasses.join(' ')}`}>
+            <div className={`navbar navbar-expand border-top px-0 ${moduleClass}`}>
               <form className="ms-auto">
                 <OptionsSelector />
               </form>

+ 46 - 39
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -59,6 +59,7 @@ import Preview from './Preview';
 import scrollSyncHelper from './ScrollSyncHelper';
 
 import '@growi/editor/dist/style.css';
+import EditorNavbarBottom from './EditorNavbarBottom';
 
 
 const logger = loggerFactory('growi:PageEditor');
@@ -566,48 +567,54 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }
 
   return (
-    <div data-testid="page-editor" id="page-editor" className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
-      <div className="page-editor-editor-container flex-expand-vert">
-        {/* <Editor
-          ref={editorRef}
-          value={initialValue}
-          isUploadable={isUploadable}
-          isUploadableFile={isUploadableFile}
-          indentSize={currentIndentSize}
-          onScroll={editorScrolledHandler}
-          onScrollCursorIntoView={editorScrollCursorIntoViewHandler}
-          onChange={markdownChangedHandler}
-          onUpload={uploadHandler}
-          onSave={saveWithShortcut}
-        /> */}
-        <CodeMirrorEditorMain
-          onChange={markdownChangedHandler}
-          onSave={saveWithShortcut}
-          onUpload={uploadHandler}
-          indentSize={currentIndentSize ?? defaultIndentSize}
-          acceptedFileType={acceptedFileType}
-        />
+    <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
+      <div className="flex-expand-vert justify-content-center align-items-center" style={{ minHeight: '72px' }}>
+        <div>Header</div>
       </div>
-      <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
-        <Preview
-          ref={previewRef}
-          rendererOptions={rendererOptions}
-          markdown={markdownToPreview}
-          pagePath={currentPagePath}
-          // TODO: implement
-          // refs: https://redmine.weseek.co.jp/issues/126519
-          // onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
+      <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
+        <div className="page-editor-editor-container flex-expand-vert">
+          {/* <Editor
+            ref={editorRef}
+            value={initialValue}
+            isUploadable={isUploadable}
+            isUploadableFile={isUploadableFile}
+            indentSize={currentIndentSize}
+            onScroll={editorScrolledHandler}
+            onScrollCursorIntoView={editorScrollCursorIntoViewHandler}
+            onChange={markdownChangedHandler}
+            onUpload={uploadHandler}
+            onSave={saveWithShortcut}
+          /> */}
+          <CodeMirrorEditorMain
+            onChange={markdownChangedHandler}
+            onSave={saveWithShortcut}
+            onUpload={uploadHandler}
+            indentSize={currentIndentSize ?? defaultIndentSize}
+            acceptedFileType={acceptedFileType}
+          />
+        </div>
+        <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
+          <Preview
+            ref={previewRef}
+            rendererOptions={rendererOptions}
+            markdown={markdownToPreview}
+            pagePath={currentPagePath}
+            // TODO: implement
+            // refs: https://redmine.weseek.co.jp/issues/126519
+            // onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
+          />
+        </div>
+        {/*
+        <ConflictDiffModal
+          isOpen={conflictDiffModalStatus?.isOpened}
+          onClose={() => closeConflictDiffModal()}
+          markdownOnEdit={markdownToPreview}
+          optionsToSave={optionsToSave}
+          afterResolvedHandler={afterResolvedHandler}
         />
+        */}
       </div>
-      {/*
-      <ConflictDiffModal
-        isOpen={conflictDiffModalStatus?.isOpened}
-        onClose={() => closeConflictDiffModal()}
-        markdownOnEdit={markdownToPreview}
-        optionsToSave={optionsToSave}
-        afterResolvedHandler={afterResolvedHandler}
-      />
-       */}
+      <EditorNavbarBottom />
     </div>
   );
 });

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

@@ -0,0 +1,30 @@
+@use '~/styles/variables' as var;
+@use '~/styles/mixins';
+
+@include mixins.editing(true) {
+  .page-editor-preview-body :global {
+  }
+}
+
+// modify width for fluid layout
+@include mixins.editing(true) {
+  .dynamic-layout-root:not(.growi-layout-fluid) {
+    :local {
+      .page-editor-preview-body :global {
+        .wiki {
+          max-width: 980px;
+          margin: 0 auto;
+        }
+      }
+    }
+  }
+  .dynamic-layout-root.growi-layout-fluid {
+    :local {
+      .page-editor-preview-body :global {
+        .wiki {
+          margin: 0 auto;
+        }
+      }
+    }
+  }
+}

+ 6 - 1
apps/app/src/components/PageEditor/Preview.tsx

@@ -7,6 +7,11 @@ import type { RendererOptions } from '~/interfaces/renderer-options';
 import RevisionRenderer from '../Page/RevisionRenderer';
 
 
+import styles from './Preview.module.scss';
+
+const moduleClass = styles['page-editor-preview-body'];
+
+
 type Props = {
   rendererOptions: RendererOptions,
   markdown?: string,
@@ -23,7 +28,7 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
 
   return (
     <div
-      className={`page-editor-preview-body ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
+      className={`${moduleClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       ref={ref}
       onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
         if (props.onScroll != null) {

+ 43 - 7
apps/app/src/components/Sidebar/PageCreateButton.tsx

@@ -2,10 +2,19 @@ import React, { useCallback, useState } from 'react';
 
 import { useRouter } from 'next/router';
 
+import { createPage } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/toastr';
+import { useSWRxCurrentPage } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:cli:PageCreateButton');
+
 export const PageCreateButton = React.memo((): JSX.Element => {
   const router = useRouter();
+  const { data: currentPage, isLoading } = useSWRxCurrentPage();
 
   const [isHovered, setIsHovered] = useState(false);
+  const [isCreating, setIsCreating] = useState(false);
 
   const onMouseEnterHandler = () => {
     setIsHovered(true);
@@ -15,12 +24,37 @@ export const PageCreateButton = React.memo((): JSX.Element => {
     setIsHovered(false);
   };
 
-  const isSelected = true;
-  // TODO: create page directly
-  // TODO: https://redmine.weseek.co.jp/issues/132680s
-  const onCreateNewPageButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}#edit`);
-  }, [router]);
+  const onCreateNewPageButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const parentPath = currentPage == null
+        ? '/'
+        : currentPage.path;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+        shouldGeneratePath: true,
+      };
+
+      const response = await createPage(parentPath, '', params);
+
+      router.push(`${response.page.id}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
   const onCreateTodaysButtonHandler = useCallback(() => {
     // router.push(`${router.pathname}#edit`);
   }, [router]);
@@ -43,10 +77,11 @@ export const PageCreateButton = React.memo((): JSX.Element => {
     >
       <div className="btn-group">
         <button
-          className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
+          className="d-block btn btn-primary"
           onClick={onCreateNewPageButtonHandler}
           type="button"
           data-testid="grw-sidebar-nav-page-create-button"
+          disabled={isCreating}
         >
           <i className="material-symbols-outlined">edit</i>
         </button>
@@ -65,6 +100,7 @@ export const PageCreateButton = React.memo((): JSX.Element => {
                 className="dropdown-item"
                 onClick={onCreateNewPageButtonHandler}
                 type="button"
+                disabled={isCreating}
               >
                 Create New Page
               </button>

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

@@ -247,7 +247,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
               onClick={onClickLoadChildren}
             >
               <div className="d-flex justify-content-center">
-                <span className="material-symbols-rounded">arrow_right</span>
+                <span className="material-symbols-outlined">arrow_right</span>
               </div>
             </button>
           )}

+ 1 - 0
apps/app/src/interfaces/page-operation.ts

@@ -34,4 +34,5 @@ export type OptionsToSave = {
   pageTags: string[] | null;
   grantUserGroupId?: string | null;
   grantUserGroupName?: string | null;
+  shouldGeneratePath?: boolean | null;
 };

+ 2 - 26
apps/app/src/pages/_app.page.tsx

@@ -3,12 +3,11 @@ import React, { ReactElement, ReactNode, useEffect } from 'react';
 import { NextPage } from 'next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
-import { Lato } from 'next/font/google';
-import localFont from 'next/font/local';
 import { SWRConfig } from 'swr';
 
 import * as nextI18nConfig from '^/config/next-i18next.config';
 
+import { GlobalFonts } from '~/components/FontFamily/GlobalFonts';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
   useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
@@ -25,22 +24,6 @@ import '~/styles/style-app.scss';
 
 const isDev = process.env.NODE_ENV === 'development';
 
-// define fonts
-const lato = Lato({
-  weight: ['400', '700'],
-  style: ['normal', 'italic'],
-  subsets: ['latin'],
-  display: 'swap',
-});
-const sourceHanCodeJPSubsetMain = localFont({
-  src: '../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',
-  display: 'optional',
-});
-const sourceHanCodeJPSubsetJis2 = localFont({
-  src: '../../resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2',
-  display: 'optional',
-});
-
 // eslint-disable-next-line @typescript-eslint/ban-types
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
   getLayout?: (page: ReactElement) => ReactNode,
@@ -74,14 +57,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
 
   return (
     <>
-      <style jsx global>{`
-        :root {
-          --font-family-sans-serif: ${lato.style.fontFamily}, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
-          --font-family-serif: Georgia, 'Times New Roman', Times, serif;
-          --font-family-monospace: monospace, ${sourceHanCodeJPSubsetMain.style.fontFamily}, ${sourceHanCodeJPSubsetJis2.style.fontFamily};
-        }
-      `}
-      </style>
+      <GlobalFonts />
       <SWRConfig value={swrGlobalConfiguration}>
         {getLayout(<Component {...pageProps} />)}
       </SWRConfig>

+ 31 - 3
apps/app/src/server/routes/apiv3/pages.js

@@ -175,6 +175,7 @@ module.exports = (crowi) => {
       body('isSlackEnabled').if(value => value != null).isBoolean().withMessage('isSlackEnabled must be boolean'),
       body('slackChannels').if(value => value != null).isString().withMessage('slackChannels must be string'),
       body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
+      body('shouldGeneratePath').optional().isBoolean().withMessage('shouldGeneratePath is must be boolean or undefined'),
     ],
     renamePage: [
       body('pageId').isMongoId().withMessage('pageId is required'),
@@ -238,6 +239,17 @@ module.exports = (crowi) => {
     return [];
   }
 
+  async function generateUniquePath(basePath, index = 1) {
+    const Page = mongoose.model('Page');
+    const path = basePath + index;
+    const response = await Page.findByPath(path);
+    const isPathExists = response != null;
+    if (isPathExists) {
+      return generateUniquePath(basePath, index + 1);
+    }
+    return path;
+  }
+
   /**
    * @swagger
    *
@@ -266,9 +278,9 @@ module.exports = (crowi) => {
    *                    type: array
    *                    items:
    *                      $ref: '#/components/schemas/Tag'
-   *                  createFromPageTree:
+   *                  shouldGeneratePath:
    *                    type: boolean
-   *                    description: Whether the page was created from the page tree or not
+   *                    description: Determine whether a new path should be generated
    *                required:
    *                  - body
    *                  - path
@@ -295,7 +307,7 @@ module.exports = (crowi) => {
    */
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity, validator.createPage, apiV3FormValidator, async(req, res) => {
     const {
-      body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags,
+      body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
     } = req.body;
 
     let { path } = req.body;
@@ -303,6 +315,22 @@ module.exports = (crowi) => {
     // check whether path starts slash
     path = addHeadingSlash(path);
 
+    if (shouldGeneratePath) {
+      try {
+        const rootPath = '/';
+        const defaultTitle = '/Untitled';
+        const basePath = path === rootPath ? defaultTitle : path + defaultTitle;
+        path = await generateUniquePath(basePath);
+
+        if (!isCreatablePage(path)) {
+          path = await generateUniquePath(defaultTitle);
+        }
+      }
+      catch (err) {
+        return res.apiv3Err(new ErrorV3('Failed to generate unique path'));
+      }
+    }
+
     if (!isCreatablePage(path)) {
       return res.apiv3Err(`Could not use the path '${path}'`);
     }

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

@@ -29,65 +29,7 @@
     width: calc(100vw - var.$grw-sidebar-nav-width);
     height: 100vh;
   }
-  .grw-editor-navbar-bottom {
-    height: var.$grw-editor-navbar-bottom-height;
 
-    .grw-grant-selector {
-      @include bs.media-breakpoint-down(sm) {
-        .btn .label {
-          display: none;
-        }
-      }
-      @include bs.media-breakpoint-up(md) {
-        .dropdown-toggle {
-          min-width: 100px;
-
-          // caret
-          &::after {
-            margin-left: 1em;
-          }
-        }
-      }
-    }
-
-    .btn-submit {
-      width: 100px;
-    }
-
-    .btn-expand {
-      // rotate icon
-      i {
-        display: inline-block;
-        transition: transform 200ms;
-      }
-      &.expand i {
-        transform: rotate(-180deg);
-      }
-    }
-  }
-
-  /*********************
-   * Navigation styles
-   */
-  .grw-subnav {
-    padding-bottom: 0;
-
-    h1 {
-      font-size: 16px;
-    }
-
-    .grw-drawer-toggler {
-      width: 38px;
-      height: 38px;
-      font-size: 18px;
-    }
-  }
-
-  .grw-copy-dropdown {
-    .btn-copy {
-      padding: 3px !important; // overwrite padding
-    }
-  }
 
   &.builtin-editor {
     /*****************
@@ -174,20 +116,6 @@
 
 }
 
-.layout-root.editing {
-  &:not(.growi-layout-fluid) .page-editor-preview-body {
-    .wiki {
-      max-width: 980px;
-      margin: 0 auto;
-    }
-  }
-  &.growi-layout-fluid .page-editor-preview-body {
-    .wiki {
-      margin: 0 auto;
-    }
-  }
-}
-
 // TODO: Never used this id class
 #tag-edit-button-tooltip {
   .tooltip-inner {

+ 23 - 0
apps/app/src/styles/_fonts.scss

@@ -0,0 +1,23 @@
+:root {
+  --font-family-sans-serif: var(--grw-font-family-lato), -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
+  --font-family-serif: Georgia, 'Times New Roman', Times, serif;
+  --font-family-monospace: monospace, var(--grw-font-family-source-han-code-jp-subset-main), var(--grw-font-family-source-han-code-jp-subset-jis2);
+}
+
+.material-symbols-outlined {
+  display: inline-block;
+  font-family: var(--grw-font-family-material-symbols-outlined);
+  font-size: 24px;  /* Preferred icon size */
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  text-transform: none;
+  letter-spacing: normal;
+  word-wrap: normal;
+  white-space: nowrap;
+  direction: ltr;
+
+  &.fill {
+    font-variation-settings: 'FILL' 1;
+  }
+}

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

@@ -1,6 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use './variables' as var;
 
+@import './mixins/editing';
 
 @mixin apply-navigation-transition() {
   transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);

+ 0 - 2
apps/app/src/styles/font-icons.scss

@@ -2,6 +2,4 @@
 // font-familiy used in simple-line-icons has to be prioritized than the one used in font-awesome.
 @import 'font-awesome';
 @import 'simple-line-icons';
-@import '@material-symbols/font-300/outlined';
-@import '@material-symbols/font-300/rounded';
 @import '@icon/themify-icons/themify-icons';

+ 14 - 0
apps/app/src/styles/mixins/_editing.scss

@@ -0,0 +1,14 @@
+@mixin editing($global: false) {
+  :global {
+    .layout-root.editing {
+      @if ($global) {
+        @content;
+      }
+      @else {
+        :local {
+          @content;
+        }
+      }
+    }
+  }
+}

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

@@ -20,6 +20,7 @@
 // // growi component
 // @import 'draft';
 @import 'editor';
+@import 'fonts';
 @import 'layout';
 @import 'mirror_mode';
 @import 'modal';

+ 1 - 0
packages/editor/index.html

@@ -3,6 +3,7 @@
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,300,0..1" rel="stylesheet" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Vite + React + TS</title>
   </head>

+ 0 - 1
packages/editor/package.json

@@ -26,7 +26,6 @@
     "@codemirror/language": "^6.8.0",
     "@codemirror/state": "^6.2.1",
     "@codemirror/view": "^6.15.3",
-    "@material-symbols/font-300": "^0.13.1",
     "@popperjs/core": "^2.11.8",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",

+ 4 - 0
packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.module.scss

@@ -0,0 +1,4 @@
+// == Colors
+.btn-text-format-tools-toggler {
+  --bs-btn-active-bg: var(--bs-secondary-bg);
+}

+ 9 - 4
packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -3,6 +3,11 @@ import { useCallback, useState } from 'react';
 import { Collapse } from 'reactstrap';
 
 
+import styles from './TextFormatTools.module.scss';
+
+const btnTextFormatToolsTogglerClass = styles['btn-text-format-tools-toggler'];
+
+
 type TogglarProps = {
   isOpen: boolean,
   onClick?: () => void,
@@ -10,17 +15,17 @@ type TogglarProps = {
 
 const TextFormatToolsToggler = (props: TogglarProps): JSX.Element => {
 
-  const { onClick } = props;
+  const { isOpen, onClick } = props;
 
-  // TODO: change color by isOpen
+  const activeClass = isOpen ? 'active' : '';
 
   return (
     <button
       type="button"
-      className="btn btn-toolbar-button"
+      className={`btn btn-toolbar-button ${btnTextFormatToolsTogglerClass} ${activeClass}`}
       onClick={onClick}
     >
-      <span className="material-symbols-outlined fs-5">match_case</span>
+      <span className="material-symbols-outlined fs-3">match_case</span>
     </button>
   );
 };

+ 14 - 2
packages/editor/src/components/CodeMirrorEditor/Toolbar/scss/toolbar-button.scss

@@ -1,9 +1,21 @@
+// styles
+.btn-toolbar-button {
+  --bs-btn-border-width: 0;
+}
+
+// set size
 .btn-toolbar-button {
   --bs-btn-padding-x: 0;
   --bs-btn-padding-y: 0;
-  --bs-btn-line-height: 1;
-  --bs-btn-border-width: 0;
 
   width: 24px !important;
   height: 24px !important;
 }
+
+// set icon center
+.btn-toolbar-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+

+ 0 - 4
packages/editor/src/main.scss

@@ -1,8 +1,4 @@
 @import 'bootstrap';
 @import 'react-toastify/scss/main';
 
-$material-symbols-font-path: '@material-symbols/font-300/';
-@import '@material-symbols/font-300/outlined';
-@import '@material-symbols/font-300/rounded';
-
 @import '@growi/core/scss/flex-expand';

+ 0 - 5
yarn.lock

@@ -3229,11 +3229,6 @@
     markdown-it-front-matter "^0.2.3"
     postcss "^8.4.19"
 
-"@material-symbols/font-300@^0.13.1":
-  version "0.13.1"
-  resolved "https://registry.yarnpkg.com/@material-symbols/font-300/-/font-300-0.13.1.tgz#33e1914565a8a8e421cb9de502ec5f6ccdc80256"
-  integrity sha512-3UcU9kw/1hKDyjkeOuv2wx9nwr5XSpbl/GG+o9+TY5xZ3ogeruNQ5aS7mRXqTxQiizLXtmkYeNUcS3N4fLQonQ==
-
 "@microsoft/api-extractor-model@7.27.5":
   version "7.27.5"
   resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.27.5.tgz#2220cf20c8587cd4cf78f82c20c4011a9e36a60f"