Browse Source

Merge pull request #10948 from growilabs/feat/160341-180922-response-fb

feat/160341-180922 resoponse design fb
Yuki Takei 1 month ago
parent
commit
6d7752f311

+ 5 - 7
apps/app/public/static/locales/en_US/translation.json

@@ -1103,7 +1103,6 @@
     "template": "Template",
     "template": "Template",
     "text_formatting": "Text Formatting"
     "text_formatting": "Text Formatting"
   },
   },
-
   "editor_guide": {
   "editor_guide": {
     "title": "Editor Guide",
     "title": "Editor Guide",
     "tabs": {
     "tabs": {
@@ -1128,7 +1127,8 @@
       "link_sandbox": "Sandbox page is here",
       "link_sandbox": "Sandbox page is here",
       "all_important": "This text is all\nimportant",
       "all_important": "This text is all\nimportant",
       "sub_text": "subscript",
       "sub_text": "subscript",
-      "sup_text": "superscript"
+      "sup_text": "superscript",
+      "is_text": "This is {{val}}"
     },
     },
     "layout": {
     "layout": {
       "copy_done": "Copied!",
       "copy_done": "Copied!",
@@ -1164,14 +1164,12 @@
       "copy_done": "Copied to clipboard!",
       "copy_done": "Copied to clipboard!",
       "style": "Style",
       "style": "Style",
       "alert": "Alert",
       "alert": "Alert",
+      "alert_with_custom_title": "Alert with label",
+      "alert_with_custom_title_text": "Custom Title",
+      "alert_unavailable": "Unavailable in this style",
       "badge": "Badge",
       "badge": "Badge",
       "text_color": "Text Color",
       "text_color": "Text Color",
       "back_color": "Background Color",
       "back_color": "Background Color",
-      "alert_block": "Alert Block",
-      "important_label": "Important",
-      "important_text": "Important information",
-      "caution_label": "Caution",
-      "caution_text": "Prohibited items or warnings",
       "placeholder": "Sample text goes here",
       "placeholder": "Sample text goes here",
       "docs_title": "Bootstrap 5 Official Documentation",
       "docs_title": "Bootstrap 5 Official Documentation",
       "docs_badge": "Learn more about Badges",
       "docs_badge": "Learn more about Badges",

+ 6 - 7
apps/app/public/static/locales/fr_FR/translation.json

@@ -1095,7 +1095,6 @@
     "template": "Modèle",
     "template": "Modèle",
     "text_formatting": "Mise en forme du texte"
     "text_formatting": "Mise en forme du texte"
   },
   },
-
   "editor_guide": {
   "editor_guide": {
     "title": "Guide de l'Éditeur",
     "title": "Guide de l'Éditeur",
     "tabs": {
     "tabs": {
@@ -1120,7 +1119,8 @@
       "link_sandbox": "La page bac à sable est ici",
       "link_sandbox": "La page bac à sable est ici",
       "all_important": "Tout ce texte est\nimportant",
       "all_important": "Tout ce texte est\nimportant",
       "sub_text": "indice",
       "sub_text": "indice",
-      "sup_text": "exposant"
+      "sup_text": "exposant",
+      "is_text": "Ceci est du {{val}}"
     },
     },
     "layout": {
     "layout": {
       "copy_done": "Copié !",
       "copy_done": "Copié !",
@@ -1156,14 +1156,13 @@
       "copy_done": "Copié dans le presse-papiers !",
       "copy_done": "Copié dans le presse-papiers !",
       "style": "Style",
       "style": "Style",
       "alert": "Alerte",
       "alert": "Alerte",
+      "alert_with_custom_title": "Alerte avec étiquette",
+      "alert_with_custom_title_text": "Titre personnalisé",
+      "alert_unavailable": "Non disponible pour ce style",
       "badge": "Badge",
       "badge": "Badge",
       "text_color": "Couleur du texte",
       "text_color": "Couleur du texte",
       "back_color": "Couleur d'arrière-plan",
       "back_color": "Couleur d'arrière-plan",
-      "alert_block": "Bloc d'alerte",
-      "important_label": "Important",
-      "important_text": "Informations importantes",
-      "caution_label": "Attention",
-      "caution_text": "Avertissements ou éléments interdits",
+
       "placeholder": "Le texte s'affiche ici",
       "placeholder": "Le texte s'affiche ici",
       "docs_title": "Documentation officielle de Bootstrap 5",
       "docs_title": "Documentation officielle de Bootstrap 5",
       "docs_badge": "En savoir plus sur les Badges",
       "docs_badge": "En savoir plus sur les Badges",

+ 6 - 7
apps/app/public/static/locales/ja_JP/translation.json

@@ -1136,7 +1136,6 @@
     "template": "テンプレート",
     "template": "テンプレート",
     "text_formatting": "テキスト書式"
     "text_formatting": "テキスト書式"
   },
   },
-
   "editor_guide": {
   "editor_guide": {
     "title": "エディターガイド",
     "title": "エディターガイド",
     "tabs": {
     "tabs": {
@@ -1161,7 +1160,8 @@
       "link_sandbox": "砂場ページはこちら",
       "link_sandbox": "砂場ページはこちら",
       "all_important": "このテキストはすべて\n重要です",
       "all_important": "このテキストはすべて\n重要です",
       "sub_text": "下付き",
       "sub_text": "下付き",
-      "sup_text": "上付き"
+      "sup_text": "上付き",
+      "is_text": "これは{{val}}です"
     },
     },
     "layout": {
     "layout": {
       "copy_done": "コピーしました!",
       "copy_done": "コピーしました!",
@@ -1197,14 +1197,13 @@
       "copy_done": "コピーしました!",
       "copy_done": "コピーしました!",
       "style": "スタイル",
       "style": "スタイル",
       "alert": "アラート",
       "alert": "アラート",
+      "alert_with_custom_title": "ラベル付きアラート",
+      "alert_with_custom_title_text": "カスタムタイトル",
+      "alert_unavailable": "このスタイルでは使用できません",
       "badge": "バッジ",
       "badge": "バッジ",
       "text_color": "テキストカラー",
       "text_color": "テキストカラー",
       "back_color": "背景色",
       "back_color": "背景色",
-      "alert_block": "アラートブロック",
-      "important_label": "Important",
-      "important_text": "重要な情報",
-      "caution_label": "Caution",
-      "caution_text": "禁止事項や警告",
+
       "placeholder": "テキストが入ります",
       "placeholder": "テキストが入ります",
       "docs_title": "Bootstrap5 公式ドキュメント",
       "docs_title": "Bootstrap5 公式ドキュメント",
       "docs_badge": "バッジの詳細はこちら",
       "docs_badge": "バッジの詳細はこちら",

+ 6 - 6
apps/app/public/static/locales/ko_KR/translation.json

@@ -1087,7 +1087,8 @@
       "link_sandbox": "연습장 페이지는 여기입니다",
       "link_sandbox": "연습장 페이지는 여기입니다",
       "all_important": "이 텍스트는 모두\n중요합니다",
       "all_important": "이 텍스트는 모두\n중요합니다",
       "sub_text": "아래 첨자",
       "sub_text": "아래 첨자",
-      "sup_text": "위 첨자"
+      "sup_text": "위 첨자",
+      "is_text": "이것은 {{val}}입니다"
     },
     },
     "layout": {
     "layout": {
       "copy_done": "복사되었습니다!",
       "copy_done": "복사되었습니다!",
@@ -1123,14 +1124,13 @@
       "copy_done": "클립보드에 복사되었습니다!",
       "copy_done": "클립보드에 복사되었습니다!",
       "style": "스타일",
       "style": "스타일",
       "alert": "알림",
       "alert": "알림",
+      "alert_with_custom_title": "레이블이 있는 알림",
+      "alert_with_custom_title_text": "사용자 정의 제목",
+      "alert_unavailable": "이 스타일에서는 사용할 수 없습니다",
       "badge": "배지",
       "badge": "배지",
       "text_color": "텍스트 색상",
       "text_color": "텍스트 색상",
       "back_color": "배경 색상",
       "back_color": "배경 색상",
-      "alert_block": "알림 블록",
-      "important_label": "Important",
-      "important_text": "중요한 정보",
-      "caution_label": "Caution",
-      "caution_text": "금지 사항 및 경고",
+
       "placeholder": "텍스트가 입력됩니다",
       "placeholder": "텍스트가 입력됩니다",
       "docs_title": "Bootstrap 5 공식 문서",
       "docs_title": "Bootstrap 5 공식 문서",
       "docs_badge": "배지 상세 정보",
       "docs_badge": "배지 상세 정보",

+ 6 - 7
apps/app/public/static/locales/zh_CN/translation.json

@@ -1108,7 +1108,6 @@
     "template": "模板",
     "template": "模板",
     "text_formatting": "文本格式"
     "text_formatting": "文本格式"
   },
   },
-
   "editor_guide": {
   "editor_guide": {
     "title": "编辑器指南",
     "title": "编辑器指南",
     "tabs": {
     "tabs": {
@@ -1133,7 +1132,8 @@
       "link_sandbox": "沙盒页面在这里",
       "link_sandbox": "沙盒页面在这里",
       "all_important": "这段文字非常\n重要",
       "all_important": "这段文字非常\n重要",
       "sub_text": "下标",
       "sub_text": "下标",
-      "sup_text": "上标"
+      "sup_text": "上标",
+      "is_text": "这是{{val}}效果"
     },
     },
     "layout": {
     "layout": {
       "copy_done": "已复制!",
       "copy_done": "已复制!",
@@ -1169,14 +1169,13 @@
       "copy_done": "已复制到剪贴板!",
       "copy_done": "已复制到剪贴板!",
       "style": "样式",
       "style": "样式",
       "alert": "提示",
       "alert": "提示",
+      "alert_with_custom_title": "带标签的提示",
+      "alert_with_custom_title_text": "自定义标题",
+      "alert_unavailable": "此样式不可用",
       "badge": "徽章",
       "badge": "徽章",
       "text_color": "文本颜色",
       "text_color": "文本颜色",
       "back_color": "背景颜色",
       "back_color": "背景颜色",
-      "alert_block": "警告框",
-      "important_label": "Important",
-      "important_text": "重要信息",
-      "caution_label": "Caution",
-      "caution_text": "禁止事项及警告",
+
       "placeholder": "此处显示文本内容",
       "placeholder": "此处显示文本内容",
       "docs_title": "Bootstrap 5 官方文档",
       "docs_title": "Bootstrap 5 官方文档",
       "docs_badge": "了解更多关于徽章的信息",
       "docs_badge": "了解更多关于徽章的信息",

+ 50 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/EditorGuideModal.module.scss

@@ -0,0 +1,50 @@
+.editor-guide-modal {
+  position: fixed !important;
+  top: var(--egm-top) !important;
+  left: var(--egm-left) !important;
+  display: flex !important;
+  align-items: center;
+  justify-content: center;
+  width: var(--egm-width) !important;
+  height: var(--egm-height) !important;
+  padding: 0 !important;
+  overflow: hidden !important;
+}
+
+.editor-guide-backdrop {
+  position: fixed !important;
+  top: var(--egm-top) !important;
+  left: var(--egm-left) !important;
+  width: var(--egm-width) !important;
+  height: var(--egm-height) !important;
+}
+
+.card-body-scrollable {
+  overflow-y: auto;
+}
+
+.editor-guide-tabs-container {
+  &:not(#_) {
+    .nav-tabs, ul {
+      display: flex;
+      flex-wrap: nowrap;
+      justify-content: space-between;
+      width: 100%;
+      padding: 0;
+      margin: 0;
+      list-style: none;
+    }
+
+    .nav-item, li {
+      flex: 1 1 0;
+      min-width: 100px;
+      text-align: center;
+    }
+
+    .nav-link, button, a {
+      display: block;
+      width: 100%;
+      white-space: nowrap;
+    }
+  }
+}

+ 76 - 77
apps/app/src/client/components/PageEditor/EditorGuideModal/EditorGuideModal.tsx

@@ -1,7 +1,6 @@
 import {
 import {
   type JSX,
   type JSX,
   type RefObject,
   type RefObject,
-  useEffect,
   useLayoutEffect,
   useLayoutEffect,
   useMemo,
   useMemo,
   useState,
   useState,
@@ -10,8 +9,8 @@ import {
   useEditorGuideModalActions,
   useEditorGuideModalActions,
   useEditorGuideModalStatus,
   useEditorGuideModalStatus,
 } from '@growi/editor/dist/states/modal/editor-guide';
 } from '@growi/editor/dist/states/modal/editor-guide';
-import { createPortal } from 'react-dom';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { Card, CardBody, CardHeader, Modal } from 'reactstrap';
 
 
 import { CustomNavTab } from '../../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../../CustomNavigation/CustomNav';
 import CustomTabContent from '../../CustomNavigation/CustomTabContent';
 import CustomTabContent from '../../CustomNavigation/CustomTabContent';
@@ -19,6 +18,13 @@ import { DecorationTab } from './contents/DecorationTab';
 import { LayoutTab } from './contents/LayoutTab';
 import { LayoutTab } from './contents/LayoutTab';
 import { TextStyleTab } from './contents/TextStyleTab';
 import { TextStyleTab } from './contents/TextStyleTab';
 
 
+import styles from './EditorGuideModal.module.scss';
+
+// Bootstrap $spacer (1rem = 16px) * 2 — matches calc(100% - 32px) padding on both sides
+const MODAL_MARGIN_PX = 32;
+// Bootstrap $modal-lg
+const MODAL_MAX_WIDTH = '800px';
+
 const TAB_TYPES = ['textstyle', 'layout', 'decoration'] as const;
 const TAB_TYPES = ['textstyle', 'layout', 'decoration'] as const;
 type TabType = (typeof TAB_TYPES)[number];
 type TabType = (typeof TAB_TYPES)[number];
 type Props = {
 type Props = {
@@ -32,17 +38,12 @@ const TextStyleTabPane = (): React.JSX.Element => <TextStyleTab />;
 const LayoutTabPane = (): React.JSX.Element => <LayoutTab />;
 const LayoutTabPane = (): React.JSX.Element => <LayoutTab />;
 const DecorationTabPane = (): React.JSX.Element => <DecorationTab />;
 const DecorationTabPane = (): React.JSX.Element => <DecorationTab />;
 
 
-/**
- * EditorGuideModal
- *
- * This modal overlays only the preview area (specified by containerRef),
- * not the entire screen. Uses createPortal to render into document.body.
- */
-export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
+export const EditorGuideModal = ({
+  containerRef,
+}: Props): JSX.Element | null => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { isOpened } = useEditorGuideModalStatus();
   const { isOpened } = useEditorGuideModalStatus();
   const { close } = useEditorGuideModalActions();
   const { close } = useEditorGuideModalActions();
-  const [isShown, setIsShown] = useState(false);
   const [rect, setRect] = useState<DOMRect | null>(null);
   const [rect, setRect] = useState<DOMRect | null>(null);
 
 
   const [activeTab, setActiveTab] = useState<TabType>('textstyle');
   const [activeTab, setActiveTab] = useState<TabType>('textstyle');
@@ -63,82 +64,80 @@ export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
     };
     };
   }, [t]);
   }, [t]);
 
 
-  // Get rect on open and on resize
   useLayoutEffect(() => {
   useLayoutEffect(() => {
     if (!isOpened || containerRef.current == null) return;
     if (!isOpened || containerRef.current == null) return;
 
 
-    const updateRect = () =>
-      setRect(containerRef.current?.getBoundingClientRect() ?? null);
+    const updateRect = () => {
+      const r = containerRef.current?.getBoundingClientRect() ?? null;
+      setRect(r);
+      if (r != null) {
+        document.body.style.setProperty('--egm-top', `${r.top}px`);
+        document.body.style.setProperty('--egm-left', `${r.left}px`);
+        document.body.style.setProperty('--egm-width', `${r.width}px`);
+        document.body.style.setProperty('--egm-height', `${r.height}px`);
+      }
+    };
     updateRect();
     updateRect();
     window.addEventListener('resize', updateRect);
     window.addEventListener('resize', updateRect);
-    return () => window.removeEventListener('resize', updateRect);
+    return () => {
+      window.removeEventListener('resize', updateRect);
+      document.body.style.removeProperty('--egm-top');
+      document.body.style.removeProperty('--egm-left');
+      document.body.style.removeProperty('--egm-width');
+      document.body.style.removeProperty('--egm-height');
+    };
   }, [isOpened, containerRef]);
   }, [isOpened, containerRef]);
 
 
-  // Trigger fade-in after mount
-  useEffect(() => {
-    if (!isOpened) {
-      setIsShown(false);
-      return;
-    }
-    const id = requestAnimationFrame(() => setIsShown(true));
-    return () => cancelAnimationFrame(id);
-  }, [isOpened]);
-
-  if (!isOpened || rect == null) return <></>;
-
-  const style = {
-    position: 'fixed' as const,
-    top: rect.top,
-    left: rect.left,
-    width: rect.width,
-    height: rect.height,
-  };
+  if (!isOpened || rect == null) return null;
 
 
-  return createPortal(
-    <>
-      <div
-        className={`modal-backdrop fade z-2 ${isShown ? 'show' : ''}`}
-        style={style}
-        onClick={close}
-        aria-hidden="true"
-      />
-      <div
-        className={`d-flex align-items-center justify-content-center z-3 pe-none fade ${isShown ? 'show' : ''}`}
-        style={style}
+  return (
+    <Modal
+      isOpen={isOpened}
+      toggle={close}
+      keyboard
+      modalClassName={styles['editor-guide-modal']}
+      backdropClassName={styles['editor-guide-backdrop']}
+      contentClassName="bg-transparent border-0 shadow-none"
+      style={{
+        margin: 0,
+        maxWidth: MODAL_MAX_WIDTH,
+        width: `calc(100% - ${MODAL_MARGIN_PX}px)`,
+      }}
+    >
+      <Card
+        className="shadow-lg border-0"
+        style={{ maxHeight: rect.height - MODAL_MARGIN_PX }}
       >
       >
-        <div className="px-3 pe-auto">
-          <div className="card shadow-lg">
-            <div className="card-header d-flex justify-content-between align-items-center">
-              <h5 className="mb-0">Editor Guide</h5>
-              <button
-                type="button"
-                className="btn-close"
-                onClick={close}
-                aria-label="Close"
-              />
-            </div>
-            <div className="mt-2 px-3">
-              <CustomNavTab
-                activeTab={activeTab}
-                navTabMapping={navTabMapping}
-                onNavSelected={(tabKey) => {
-                  if (isTabType(tabKey)) {
-                    setActiveTab(tabKey);
-                  }
-                }}
-                hideBorderBottom
-              />
-            </div>
-            <div className="card-body overflow-auto">
-              <CustomTabContent
-                activeTab={activeTab}
-                navTabMapping={navTabMapping}
-              />
-            </div>
-          </div>
+        <CardHeader className="d-flex justify-content-between align-items-center bg-transparent border-bottom-0 pt-3">
+          <h5 className="mb-0 text-body">{t('editor_guide.title')}</h5>
+          <button
+            type="button"
+            className="btn-close"
+            onClick={close}
+            aria-label="Close"
+          />
+        </CardHeader>
+        <div className={`mt-2 px-3 ${styles['editor-guide-tabs-container']}`}>
+          <CustomNavTab
+            activeTab={activeTab}
+            navTabMapping={navTabMapping}
+            onNavSelected={(tabKey) => {
+              if (isTabType(tabKey)) {
+                setActiveTab(tabKey);
+              }
+            }}
+            hideBorderBottom
+          />
         </div>
         </div>
-      </div>
-    </>,
-    document.body,
+        <CardBody
+          className={`pt-0 flex-fill ${styles['card-body-scrollable']}`}
+        >
+          <CustomTabContent
+            activeTab={activeTab}
+            navTabMapping={navTabMapping}
+          />
+        </CardBody>
+      </Card>
+    </Modal>
   );
   );
 };
 };

+ 21 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/components/GuideRow.module.scss

@@ -0,0 +1,21 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use 'sass:map';
+
+.copyButton {
+  display: block;
+  padding: 0;
+  text-align: left;
+  cursor: pointer;
+  background-color: transparent;
+  border: 0;
+}
+
+.codeBox {
+  width: fit-content;
+}
+
+.copyBadge {
+  top: map.get(bs.$spacers, 1);
+  right: map.get(bs.$spacers, 1);
+  font-size: bs.$badge-font-size;
+}

+ 89 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/components/GuideRow.tsx

@@ -0,0 +1,89 @@
+import type { ReactNode } from 'react';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+
+import styles from './GuideRow.module.scss';
+
+export interface LayoutGuideItem {
+  id: string;
+  title?: string;
+  code?: string;
+  preview?: ReactNode;
+  minWidth?: string;
+  underContent?: ReactNode;
+}
+
+export type GuideRowProps = Omit<LayoutGuideItem, 'id'>;
+
+export const GuideRow = ({
+  title,
+  code,
+  preview,
+  minWidth = '230px',
+  underContent,
+}: GuideRowProps) => {
+  const { t } = useTranslation();
+  const handleCopy = useCallback(async () => {
+    try {
+      if (code == null) return;
+
+      await navigator.clipboard.writeText(code);
+      toastSuccess(t('editor_guide.textstyle.copy_done'));
+    } catch (_err) {
+      toastError(t('common:failed_to_copy'));
+    }
+  }, [code, t]);
+
+  const isFullWidth = minWidth === '100%' || !preview;
+
+  return (
+    <section className={title ? 'mt-4 mb-2' : 'mb-2'}>
+      {title && <h3 className="fw-bold mb-2 fs-5 text-body">{title}</h3>}
+      <div className="d-flex flex-row flex-wrap align-items-center gap-4 py-1">
+        <button
+          type="button"
+          onClick={handleCopy}
+          className={`${styles.copyButton} ${isFullWidth ? 'w-100 flex-grow-1' : 'flex-grow-0 flex-shrink-0'}`}
+          style={{ minWidth: isFullWidth ? '100%' : minWidth }}
+        >
+          {code != null && (
+            <div
+              className={`${styles.codeBox} rounded overflow-hidden position-relative ${isFullWidth ? 'w-100' : ''}`}
+            >
+              <PrismAsyncLight
+                style={oneDark}
+                language="markdown"
+                customStyle={{ margin: 0 }}
+                wrapLongLines={isFullWidth}
+              >
+                {code}
+              </PrismAsyncLight>
+              <small
+                className={`position-absolute badge bg-secondary opacity-50 ${styles.copyBadge}`}
+              >
+                Copy
+              </small>
+            </div>
+          )}
+          {code == null && (
+            <span className="text-secondary">
+              ({t('editor_guide.decoration.alert_unavailable')})
+            </span>
+          )}
+        </button>
+
+        {preview && (
+          <div className="flex-grow-1">
+            <div className="wiki-content small">{preview}</div>
+          </div>
+        )}
+      </div>
+
+      {underContent && <div className="mt-2 w-100">{underContent}</div>}
+    </section>
+  );
+};

+ 21 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/DecorationTab.module.scss

@@ -0,0 +1,21 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use 'sass:map';
+
+.decorationTab {
+  min-width: map.get(bs.$grid-breakpoints, 'sm');
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+// Override the margin of callout in the preview
+.decorationBody {
+  :global(.callout) {
+    margin: 0.2rem 0;
+  }
+}
+
+.dropdownMenu {
+  max-height: 300px;
+  margin-top: bs.$dropdown-spacer;
+  overflow-y: auto;
+}

+ 146 - 223
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/DecorationTab.tsx

@@ -1,256 +1,179 @@
 import type React from 'react';
 import type React from 'react';
 import { useMemo, useState } from 'react';
 import { useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import ReactMarkdown from 'react-markdown';
+import {
+  Dropdown,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+} from 'reactstrap';
 
 
-import { toastSuccess } from '~/client/util/toastr';
+import { usePreviewOptions } from '~/stores/renderer';
 
 
-interface LayoutGuideItem {
-  id: string;
-  title: string;
-  code: string;
-  preview?: React.ReactNode;
-  minWidth?: string;
-  underContent?: React.ReactNode;
-}
-type GuideRowProps = Omit<LayoutGuideItem, 'id'>;
+import type { LayoutGuideItem } from '../components/GuideRow';
+import { GuideRow } from '../components/GuideRow';
 
 
-const GuideRow = ({
-  title,
-  code,
-  preview,
-  minWidth = '230px',
-  underContent,
-}: GuideRowProps) => {
-  const { t } = useTranslation();
-  const handleCopy = async () => {
-    await navigator.clipboard.writeText(code);
-    toastSuccess(t('editor_guide.textstyle.copy_done'));
-  };
-
-  const isFullWidth = minWidth === '100%' || !preview;
-
-  return (
-    <section className={title !== '' ? 'mt-4 mb-2' : 'mb-2'}>
-      {title !== '' && <h3 className="fw-bold mb-2 fs-4 text-body">{title}</h3>}
-      <div className="d-flex flex-row flex-wrap align-items-center gap-4 py-1">
-        <button
-          type="button"
-          onClick={handleCopy}
-          className="flex-grow-0 flex-shrink-0 border-0 p-0 bg-transparent text-start"
-          style={{
-            cursor: 'pointer',
-            flex: isFullWidth ? '1 0 100%' : '0 0 auto',
-            width: isFullWidth ? '100%' : 'fit-content',
-            minWidth: isFullWidth ? '100%' : minWidth,
-            display: 'block',
-          }}
-        >
-          <div
-            className={`text-light p-2 ps-3 pe-5 rounded position-relative ${isFullWidth ? 'w-100' : ''}`}
-            style={{
-              backgroundColor: 'var(--bs-dark)',
-            }}
-          >
-            <pre
-              className="m-0 small font-monospace text-white-50"
-              style={{
-                whiteSpace: isFullWidth ? 'pre-wrap' : 'pre',
-                lineHeight: '1.5',
-              }}
-            >
-              {code}
-            </pre>
-            <small
-              className="position-absolute badge bg-secondary opacity-50"
-              style={{ fontSize: '0.4rem', top: '4px', right: '4px' }}
-            >
-              Click
-            </small>
-          </div>
-        </button>
+import styles from './DecorationTab.module.scss';
 
 
-        {preview && (
-          <div
-            className="flex-grow-0 flex-shrink-0"
-            style={{
-              flexBasis: isFullWidth ? '100%' : 'auto',
-            }}
-          >
-            <div className="wiki-content small">{preview}</div>
-          </div>
-        )}
-      </div>
+const BOOTSTRAP_STYLES = [
+  'primary',
+  'secondary',
+  'info',
+  'success',
+  'warning',
+  'danger',
+] as const;
+type BOOTSTRAP_STYLES = (typeof BOOTSTRAP_STYLES)[number];
 
 
-      {underContent && <div className="mt-2 w-100">{underContent}</div>}
-    </section>
-  );
+const BOOTSTRAP_STYLES_TO_CONFIGS_MAPPINGS: Record<
+  BOOTSTRAP_STYLES,
+  { icon: string; calloutType?: string }
+> = {
+  primary: {
+    icon: 'feedback',
+    calloutType: 'important',
+  },
+  secondary: { icon: 'label' },
+  info: { icon: 'info', calloutType: 'note' },
+  success: { icon: 'lightbulb', calloutType: 'tip' },
+  warning: { icon: 'warning', calloutType: 'warning' },
+  danger: { icon: 'report', calloutType: 'caution' },
 };
 };
 
 
 export const DecorationTab: React.FC = () => {
 export const DecorationTab: React.FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const i18nKey = 'editor_guide.decoration';
   const i18nKey = 'editor_guide.decoration';
-  const [currentStyle, setCurrentStyle] = useState<'primary' | 'danger'>(
-    'primary',
-  );
+  const [currentStyle, setCurrentStyle] = useState<BOOTSTRAP_STYLES>('primary');
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
 
 
-  const styleConfig = useMemo(() => {
-    const isPrimary = currentStyle === 'primary';
-    return {
-      colorName: currentStyle,
-      displayName: isPrimary ? 'Primary' : 'Danger',
-      iconName: isPrimary ? 'chat' : 'error',
-      alertPrefix: isPrimary ? '[!IMPORTANT]' : '[!CAUTION]',
-      alertLabel: isPrimary
-        ? t(`${i18nKey}.important_label`)
-        : t(`${i18nKey}.caution_label`),
-      alertText: isPrimary
-        ? t(`${i18nKey}.important_text`)
-        : t(`${i18nKey}.caution_text`),
-      icon: isPrimary ? 'bi-chat-left-text' : 'bi-exclamation-circle',
-    };
-  }, [currentStyle, t]);
+  const { data: previewOptions } = usePreviewOptions();
+
+  const calloutConfig: { icon: string; calloutType?: string } =
+    BOOTSTRAP_STYLES_TO_CONFIGS_MAPPINGS[currentStyle];
+  const displayName =
+    currentStyle.charAt(0).toUpperCase() + currentStyle.slice(1);
 
 
   const LAYOUT_GUIDES: LayoutGuideItem[] = useMemo(
   const LAYOUT_GUIDES: LayoutGuideItem[] = useMemo(
-    () => [
-      {
-        id: 'alert',
-        title: t(`${i18nKey}.alert`),
-        code: `> ${styleConfig.alertPrefix}\n> ${styleConfig.alertText}`,
-        preview: (
-          <div
-            className={`d-flex align-items-center border-start border-4 border-${styleConfig.colorName} ps-3 py-1`}
-            style={{ minHeight: '52px' }}
-          >
-            <div className="d-flex flex-column justify-content-center">
-              <div
-                className={`d-flex align-items-center fw-bold text-${styleConfig.colorName} mb-1`}
-              >
-                <span className="me-2 d-flex align-items-center">
-                  <span className="material-symbols-outlined align-middle fs-6">
-                    {styleConfig.iconName}
-                  </span>
-                </span>
-                <span style={{ lineHeight: 1 }}>{styleConfig.alertLabel}</span>
-              </div>
-              <div className="text-body small lh-base">
-                {styleConfig.alertText}
-              </div>
+    () =>
+      [
+        currentStyle !== 'secondary' && {
+          id: 'alert',
+          title: t(`${i18nKey}.alert`),
+          code: `> [!${calloutConfig.calloutType?.toUpperCase()}]\n> ${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}`,
+          preview: (
+            <ReactMarkdown
+              {...previewOptions}
+            >{`> [!${calloutConfig.calloutType?.toUpperCase()}]\n> ${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}`}</ReactMarkdown>
+          ),
+        },
+        currentStyle !== 'secondary' && {
+          id: 'alert2',
+          code: `:::${calloutConfig.calloutType}\n${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}\n:::`,
+          preview: (
+            <ReactMarkdown
+              {...previewOptions}
+            >{`:::${calloutConfig.calloutType}\n${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}\n:::`}</ReactMarkdown>
+          ),
+        },
+        currentStyle !== 'secondary' && {
+          id: 'alert3',
+          title: t(`${i18nKey}.alert_with_custom_title`),
+          code: `:::${calloutConfig.calloutType}[${t(`${i18nKey}.alert_with_custom_title_text`)}]\n${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}\n:::`,
+          preview: (
+            <ReactMarkdown
+              {...previewOptions}
+            >{`:::${calloutConfig.calloutType}[${t(`${i18nKey}.alert_with_custom_title_text`)}]\n${t(`${i18nKey}.${currentStyle}_text`, { defaultValue: t(`${i18nKey}.placeholder`) })}\n:::`}</ReactMarkdown>
+          ),
+        },
+        currentStyle === 'secondary' && {
+          id: 'alert_empty',
+          title: t(`${i18nKey}.alert`),
+        },
+        {
+          id: 'badge',
+          title: t(`${i18nKey}.badge`),
+          code: `<span class="badge text-bg-${currentStyle}">${t(`${i18nKey}.badge`)}</span>`,
+          preview: (
+            <span className={`badge text-bg-${currentStyle}`}>
+              {t(`${i18nKey}.badge`)}
+            </span>
+          ),
+        },
+        {
+          id: 'text-color',
+          title: t(`${i18nKey}.text_color`),
+          code: `<p class="text-${currentStyle}">${t(`${i18nKey}.placeholder`)}</p>`,
+          underContent: (
+            <p className={`text-${currentStyle} m-0`}>
+              {t(`${i18nKey}.placeholder`)}
+            </p>
+          ),
+        },
+        {
+          id: 'back-color',
+          title: t(`${i18nKey}.back_color`),
+          code: `<p class="text-bg-${currentStyle}">${t(`${i18nKey}.placeholder`)}</p>`,
+          underContent: (
+            <p className={`text-bg-${currentStyle} px-2 m-0`}>
+              {t(`${i18nKey}.placeholder`)}
+            </p>
+          ),
+        },
+        {
+          id: 'alert-block',
+          title: t(`${i18nKey}.alert_block`),
+          code: `<div class="alert alert-${currentStyle}" role="alert">\n  ${t(`${i18nKey}.placeholder`)}\n</div>`,
+          underContent: (
+            <div className={`alert alert-${currentStyle} m-0`}>
+              {t(`${i18nKey}.placeholder`)}
             </div>
             </div>
-          </div>
-        ),
-      },
-      {
-        id: 'badge',
-        title: t(`${i18nKey}.badge`),
-        code: `<span class="badge text-bg-${styleConfig.colorName}">${t(`${i18nKey}.badge`)}</span>`,
-        preview: (
-          <span className={`badge text-bg-${styleConfig.colorName}`}>
-            {t(`${i18nKey}.badge`)}
-          </span>
-        ),
-      },
-      {
-        id: 'text-color',
-        title: t(`${i18nKey}.text_color`),
-        code: `<p class="text-${styleConfig.colorName}" >${t(`${i18nKey}.placeholder`)}</p>`,
-        underContent: (
-          <p className={`text-${styleConfig.colorName} m-0`}>
-            {t(`${i18nKey}.placeholder`)}
-          </p>
-        ),
-      },
-      {
-        id: 'back-color',
-        title: t(`${i18nKey}.back_color`),
-        code: `<p class="text-white minWidth: '100%' bg-${styleConfig.colorName}">${t(`${i18nKey}.placeholder`)}</p>`,
-        underContent: (
-          <p className={`text-white bg-${styleConfig.colorName} px-2 m-0`}>
-            {t(`${i18nKey}.placeholder`)}
-          </p>
-        ),
-      },
-      {
-        id: 'alert-block',
-        title: t(`${i18nKey}.alert_block`),
-        code: `<div class="alert alert-${styleConfig.colorName}" role="alert">\n  ${t(`${i18nKey}.placeholder`)}\n</div>`,
-        underContent: (
-          <div className={`alert alert-${styleConfig.colorName} m-0`}>
-            {t(`${i18nKey}.placeholder`)}
-          </div>
-        ),
-      },
-    ],
-    [styleConfig, t],
+          ),
+        },
+      ].filter((item) => item !== false) as LayoutGuideItem[],
+    [currentStyle, t, previewOptions, calloutConfig.calloutType],
   );
   );
 
 
   return (
   return (
-    <div
-      className="px-4 py-3 overflow-y-auto"
-      style={{
-        maxHeight: '80vh',
-        minWidth: '650px',
-      }}
-    >
+    <div className={`px-4 py-3 ${styles.decorationTab}`}>
       <section className="mb-4">
       <section className="mb-4">
         <h3 className="fw-bold mb-2 fs-5">{t(`${i18nKey}.style`)}</h3>
         <h3 className="fw-bold mb-2 fs-5">{t(`${i18nKey}.style`)}</h3>
-        <div className={`dropdown ${isOpen ? 'show' : ''}`}>
-          <button
-            className={`btn btn-light border dropdown-toggle d-flex align-items-center gap-2 text-${styleConfig.colorName}`}
-            type="button"
-            onClick={() => setIsOpen(!isOpen)}
-            aria-expanded={isOpen}
-            style={{ minWidth: '160px', textAlign: 'left' }}
+        <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
+          <DropdownToggle
+            outline
+            color="body"
+            caret
+            className={`border d-flex align-items-center gap-2 text-${currentStyle}`}
+            style={{ minWidth: '160px' }}
           >
           >
-            <span className="material-symbols-outlined align-middle fs-6">
-              {styleConfig.iconName}
+            <span className="flex-grow-1 justify-content-start d-flex align-items-center gap-1">
+              <span className="material-symbols-outlined align-middle fs-6">
+                {calloutConfig.icon}
+              </span>
+              {displayName}
             </span>
             </span>
-            <span className="flex-grow-1">{styleConfig.displayName}</span>
-          </button>
-          <ul
-            className={`dropdown-menu ${isOpen ? 'show' : ''}`}
-            style={{
-              position: 'absolute',
-              display: isOpen ? 'block' : 'none',
-              marginTop: '0.125rem',
-            }}
-          >
-            <li>
-              <button
-                className={`dropdown-item d-flex align-items-center gap-2 ${currentStyle === 'primary' ? 'active' : ''}`}
-                type="button"
-                onClick={() => {
-                  setCurrentStyle('primary');
-                  setIsOpen(false);
-                }}
-                style={
-                  currentStyle === 'primary'
-                    ? { backgroundColor: 'var(--bs-primary)', color: 'white' }
-                    : {}
-                }
-              >
-                <span className="material-symbols-outlined">chat</span> Primary
-              </button>
-            </li>
-            <li>
-              <button
-                className={`dropdown-item d-flex align-items-center gap-2 ${currentStyle === 'danger' ? 'active' : ''}`}
-                type="button"
-                onClick={() => {
-                  setCurrentStyle('danger');
-                  setIsOpen(false);
-                }}
+          </DropdownToggle>
+          <DropdownMenu className={styles.dropdownMenu}>
+            {BOOTSTRAP_STYLES.map((style) => (
+              <DropdownItem
+                key={style}
+                active={currentStyle === style}
+                className="d-flex align-items-center gap-2"
+                onClick={() => setCurrentStyle(style)}
               >
               >
-                <span className="material-symbols-outlined">Error</span> Danger
-              </button>
-            </li>
-          </ul>
-        </div>
+                <span className="material-symbols-outlined">
+                  {BOOTSTRAP_STYLES_TO_CONFIGS_MAPPINGS[style].icon}
+                </span>
+                {style.charAt(0).toUpperCase() + style.slice(1)}
+              </DropdownItem>
+            ))}
+          </DropdownMenu>
+        </Dropdown>
       </section>
       </section>
 
 
       <hr />
       <hr />
 
 
-      <div key={currentStyle}>
+      <div key={currentStyle} className={styles.decorationBody}>
         {LAYOUT_GUIDES.map((item) => (
         {LAYOUT_GUIDES.map((item) => (
           <GuideRow key={item.id} {...item} minWidth="280px" />
           <GuideRow key={item.id} {...item} minWidth="280px" />
         ))}
         ))}

+ 19 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/LayoutTab.module.scss

@@ -0,0 +1,19 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use 'sass:map';
+
+.layoutTabContainer {
+  min-width: map.get(bs.$grid-breakpoints, 'sm');
+  max-height: 80vh;
+}
+
+.checkboxMock {
+  width: bs.$form-check-input-width;
+  height: bs.$form-check-input-width;
+}
+
+.tableContainer {
+  width: fit-content;
+  table {
+    min-width: map.get(bs.$grid-breakpoints, 'sm');
+  }
+}

+ 15 - 97
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/LayoutTab.tsx

@@ -1,83 +1,12 @@
 import type React from 'react';
 import type React from 'react';
-import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
 
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import type { LayoutGuideItem } from '../components/GuideRow';
+import { GuideRow } from '../components/GuideRow';
 
 
-interface LayoutGuideItem {
-  id: string;
-  title: string;
-  code: string;
-  preview?: React.ReactNode;
-  minWidth?: string;
-  underContent?: React.ReactNode;
-}
-type GuideRowProps = Omit<LayoutGuideItem, 'id'>;
-const GuideRow = ({
-  title,
-  code,
-  preview,
-  minWidth = '230px',
-  underContent,
-}: GuideRowProps) => {
-  const { t } = useTranslation();
-  const handleCopy = useCallback(async () => {
-    try {
-      await navigator.clipboard.writeText(code);
-      toastSuccess(t('editor_guide.textstyle.copy_done'));
-    } catch (err) {
-      toastError(t('common:failed_to_copy'));
-    }
-  }, [code, t]);
-  return (
-    <section className={title !== '' ? 'mt-4 mb-2' : 'mb-2'}>
-      {title !== '' && <h3 className="fw-bold mb-2 fs-4 text-body">{title}</h3>}
-      <div className="d-flex flex-row flex-wrap align-items-center gap-4 py-1">
-        <button
-          type="button"
-          onClick={handleCopy}
-          className="btn-none p-0 text-start border-0 bg-transparent"
-          style={{ cursor: 'pointer' }}
-        >
-          <div
-            className="text-light p-2 ps-3 pe-5 rounded position-relative"
-            style={{
-              backgroundColor: 'var(--bs-dark)',
-              minWidth,
-              width: 'fit-content',
-            }}
-          >
-            <pre
-              className="m-0 small font-monospace text-white-50"
-              style={{ whiteSpace: 'pre', lineHeight: '1.5' }}
-            >
-              {code}
-            </pre>
-            <small
-              className="position-absolute badge bg-secondary opacity-50"
-              style={{ fontSize: '0.4rem', top: '4px', right: '4px' }}
-            >
-              Click
-            </small>
-          </div>
-        </button>
-        {preview && (
-          <div
-            className="flex-grow-1"
-            style={{
-              minWidth: '250px',
-              flexBasis: '0',
-            }}
-          >
-            <div className="wiki-content small">{preview}</div>
-          </div>
-        )}
-      </div>
-
-      {underContent && <div className="mt-2 w-100">{underContent}</div>}
-    </section>
-  );
-};
+import styles from './LayoutTab.module.scss';
 
 
 export const LayoutTab: React.FC = () => {
 export const LayoutTab: React.FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -160,8 +89,7 @@ export const LayoutTab: React.FC = () => {
           </div>
           </div>
           <div className="d-flex align-items-center mb-1 ps-4">
           <div className="d-flex align-items-center mb-1 ps-4">
             <span
             <span
-              className="d-inline-block border border-secondary rounded me-2"
-              style={{ width: '18px', height: '18px' }}
+              className={`d-inline-block border border-secondary rounded me-2 ${styles.checkboxMock}`}
             />
             />
             <span>{t(`${i18nKey}.task`)}1-1</span>
             <span>{t(`${i18nKey}.task`)}1-1</span>
           </div>
           </div>
@@ -214,17 +142,13 @@ export const LayoutTab: React.FC = () => {
       title: t(`${i18nKey}.code_block`),
       title: t(`${i18nKey}.code_block`),
       code: `\`\`\`\n${t(`${i18nKey}.code_block_text`)}\n\`\`\``,
       code: `\`\`\`\n${t(`${i18nKey}.code_block_text`)}\n\`\`\``,
       preview: (
       preview: (
-        <div
-          className="rounded p-3 w-100 font-monospace"
-          style={{
-            minWidth: '200px',
-            backgroundColor: 'var(--bs-dark)',
-          }}
+        <PrismAsyncLight
+          style={oneDark}
+          language="markdown"
+          customStyle={{ margin: 0 }}
         >
         >
-          <div className="small text-white-50 lh-base">
-            {t(`${i18nKey}.code_block_text`)}
-          </div>
-        </div>
+          {t(`${i18nKey}.code_block_text`)}
+        </PrismAsyncLight>
       ),
       ),
     },
     },
     {
     {
@@ -238,11 +162,8 @@ export const LayoutTab: React.FC = () => {
           `${t(`${i18nKey}.center`)}${t(`${i18nKey}.row_display`)} |`,
           `${t(`${i18nKey}.center`)}${t(`${i18nKey}.row_display`)} |`,
       ].join('\n'),
       ].join('\n'),
       underContent: (
       underContent: (
-        <div className="table-responsive mt-2" style={{ width: 'fit-content' }}>
-          <table
-            className="table table-sm table-bordered mb-0 small text-body"
-            style={{ minWidth: '580px' }}
-          >
+        <div className={`table-responsive mt-2 ${styles.tableContainer}`}>
+          <table className="table table-sm table-bordered mb-0 small text-body">
             <thead>
             <thead>
               <tr className="table-light">
               <tr className="table-light">
                 <th className="text-start fw-bold p-2 align-middle">
                 <th className="text-start fw-bold p-2 align-middle">
@@ -300,10 +221,7 @@ export const LayoutTab: React.FC = () => {
   ];
   ];
 
 
   return (
   return (
-    <div
-      className="px-4 py-3 overflow-y-auto"
-      style={{ maxHeight: '80vh', minWidth: '650px' }}
-    >
+    <div className={`px-4 py-3 overflow-y-auto ${styles.layoutTabContainer}`}>
       {LAYOUT_GUIDES.map((item) => (
       {LAYOUT_GUIDES.map((item) => (
         <GuideRow key={item.id} {...item} />
         <GuideRow key={item.id} {...item} />
       ))}
       ))}

+ 24 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/TextStyleTab.module.scss

@@ -0,0 +1,24 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use 'sass:map';
+
+.textStyleTab {
+  max-height: 80vh;
+}
+
+.codeBlockWrapper {
+  width: fit-content;
+}
+
+.copyBadge {
+  top: bs.$dropdown-spacer;
+  right: map.get(bs.$spacers, 1);
+  font-size: bs.$badge-font-size;
+}
+
+.wikiPreview {
+  font-size: bs.$font-size-sm;
+}
+
+.inlineCodeLabel {
+  border: 1px solid currentColor;
+}

+ 30 - 51
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/TextStyleTab.tsx

@@ -1,7 +1,12 @@
 import type React from 'react';
 import type React from 'react';
+import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
 
-import { toastSuccess } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+
+import styles from './TextStyleTab.module.scss';
 
 
 const GuideRow = ({
 const GuideRow = ({
   title,
   title,
@@ -13,10 +18,14 @@ const GuideRow = ({
   preview: React.ReactNode;
   preview: React.ReactNode;
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const handleCopy = async () => {
-    await navigator.clipboard.writeText(code);
-    toastSuccess(t('editor_guide.textstyle.copy_done'));
-  };
+  const handleCopy = useCallback(async () => {
+    try {
+      await navigator.clipboard.writeText(code);
+      toastSuccess(t('editor_guide.textstyle.copy_done'));
+    } catch (_err) {
+      toastError(t('common:failed_to_copy'));
+    }
+  }, [code, t]);
 
 
   return (
   return (
     <section className={title !== '' ? 'mt-4 mb-1' : 'mb-1'}>
     <section className={title !== '' ? 'mt-4 mb-1' : 'mb-1'}>
@@ -25,43 +34,27 @@ const GuideRow = ({
         <button
         <button
           type="button"
           type="button"
           onClick={handleCopy}
           onClick={handleCopy}
-          style={{ cursor: 'pointer' }}
-          className="flex-shrink-0 border-0 p-0 bg-transparent text-start"
+          className="flex-shrink-0 border-0 p-0 bg-transparent text-start cursor-pointer"
         >
         >
           <div
           <div
-            className="bg-dark text-light p-2 ps-2 pe-4 rounded position-relative"
-            style={{
-              backgroundColor: '#2D2E32',
-              width: 'fit-content',
-            }}
+            className={`rounded position-relative overflow-hidden ${styles.codeBlockWrapper}`}
           >
           >
-            <pre
-              className="m-0 small font-monospace"
-              style={{
-                whiteSpace: 'pre',
-                color: '#ABB2BF',
-                fontWeight: 400,
-                fontSize: '14px',
-              }}
+            <PrismAsyncLight
+              style={oneDark}
+              language="markdown"
+              customStyle={{ margin: 0 }}
             >
             >
               {code}
               {code}
-            </pre>
+            </PrismAsyncLight>
             <small
             <small
-              className="position-absolute badge bg-secondary opacity-50"
-              style={{ fontSize: '0.4rem', top: '2px', right: '4px' }}
+              className={`position-absolute badge bg-secondary opacity-50 ${styles.copyBadge}`}
             >
             >
-              Click
+              Copy
             </small>
             </small>
           </div>
           </div>
         </button>
         </button>
-        <div className="flex-grow-1" style={{ whiteSpace: 'nowrap' }}>
-          <div
-            className="wiki-content"
-            style={{
-              fontWeight: 400,
-              fontSize: '14px',
-            }}
-          >
+        <div className="flex-grow-1 text-nowrap">
+          <div className={`wiki-content fw-normal ${styles.wikiPreview}`}>
             {preview}
             {preview}
           </div>
           </div>
         </div>
         </div>
@@ -118,26 +111,14 @@ export const TextStyleTab: React.FC = () => {
       title: t(`${i18nKey}.inline_code`),
       title: t(`${i18nKey}.inline_code`),
       code: `\`${t(`${i18nKey}.inline_code`)}\` \n~~~${t(`${i18nKey}.inline_code`)}~~~`,
       code: `\`${t(`${i18nKey}.inline_code`)}\` \n~~~${t(`${i18nKey}.inline_code`)}~~~`,
       preview: (
       preview: (
-        <div className="d-flex flex-column gap-2">
+        <div className="d-flex flex-column gap-2 align-items-start">
           <code
           <code
-            className="rounded px-1"
-            style={{
-              width: 'fit-content',
-              color: '#D63384',
-              border: '1px solid #D63384',
-              backgroundColor: 'transparent',
-            }}
+            className={`rounded px-1 d-inline-block bg-transparent ${styles.inlineCodeLabel}`}
           >
           >
             {t(`${i18nKey}.inline_code`)}
             {t(`${i18nKey}.inline_code`)}
           </code>
           </code>
           <code
           <code
-            className="rounded px-1"
-            style={{
-              width: 'fit-content',
-              color: '#D63384',
-              border: '1px solid #D63384',
-              backgroundColor: 'transparent',
-            }}
+            className={`rounded px-1 d-inline-block bg-transparent ${styles.inlineCodeLabel}`}
           >
           >
             {t(`${i18nKey}.inline_code`)}
             {t(`${i18nKey}.inline_code`)}
           </code>
           </code>
@@ -150,7 +131,7 @@ export const TextStyleTab: React.FC = () => {
       code: `***${t(`${i18nKey}.all_important`)}***`,
       code: `***${t(`${i18nKey}.all_important`)}***`,
       preview: (
       preview: (
         <strong>
         <strong>
-          <u>{t(`${i18nKey}.all_important`).replace('\n', '')}</u>
+          <em>{t(`${i18nKey}.all_important`).replace('\n', '')}</em>
         </strong>
         </strong>
       ),
       ),
     },
     },
@@ -196,7 +177,6 @@ export const TextStyleTab: React.FC = () => {
           target="_blank"
           target="_blank"
           rel="noreferrer"
           rel="noreferrer"
           className="text-secondary text-decoration-underline"
           className="text-secondary text-decoration-underline"
-          style={{ color: '#777570' }}
           onClick={(e) => e.stopPropagation()}
           onClick={(e) => e.stopPropagation()}
         >
         >
           {t(`${i18nKey}.link_growi`)}
           {t(`${i18nKey}.link_growi`)}
@@ -212,7 +192,6 @@ export const TextStyleTab: React.FC = () => {
         <a
         <a
           href="/Sandbox"
           href="/Sandbox"
           className="text-secondary text-decoration-underline"
           className="text-secondary text-decoration-underline"
-          style={{ color: '#777570' }}
           onClick={(e) => e.stopPropagation()}
           onClick={(e) => e.stopPropagation()}
         >
         >
           {t(`${i18nKey}.link_sandbox`)}
           {t(`${i18nKey}.link_sandbox`)}
@@ -222,7 +201,7 @@ export const TextStyleTab: React.FC = () => {
     },
     },
   ];
   ];
   return (
   return (
-    <div className="px-4 py-2 overflow-y-auto" style={{ maxHeight: '80vh' }}>
+    <div className={`px-4 py-2 overflow-y-auto ${styles.textStyleTab}`}>
       {TEXT_STYLE_GUIDES.map((item) => (
       {TEXT_STYLE_GUIDES.map((item) => (
         <GuideRow
         <GuideRow
           key={item.id}
           key={item.id}

+ 12 - 10
packages/editor/src/states/modal/editor-guide.ts

@@ -1,3 +1,4 @@
+import { useMemo } from 'react';
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 
 
 export type EditorGuideModalState = {
 export type EditorGuideModalState = {
@@ -8,19 +9,20 @@ const editorGuideModalAtom = atom<EditorGuideModalState>({
   isOpened: false,
   isOpened: false,
 });
 });
 
 
+const openEditorGuideModalAtom = atom(null, (_get, set) => {
+  set(editorGuideModalAtom, { isOpened: true });
+});
+
+const closeEditorGuideModalAtom = atom(null, (_get, set) => {
+  set(editorGuideModalAtom, { isOpened: false });
+});
+
 export const useEditorGuideModalStatus = () => {
 export const useEditorGuideModalStatus = () => {
   return useAtomValue(editorGuideModalAtom);
   return useAtomValue(editorGuideModalAtom);
 };
 };
 
 
 export const useEditorGuideModalActions = () => {
 export const useEditorGuideModalActions = () => {
-  const setModalState = useSetAtom(editorGuideModalAtom);
-
-  return {
-    open: () => {
-      setModalState({ isOpened: true });
-    },
-    close: () => {
-      setModalState({ isOpened: false });
-    },
-  };
+  const open = useSetAtom(openEditorGuideModalAtom);
+  const close = useSetAtom(closeEditorGuideModalAtom);
+  return useMemo(() => ({ open, close }), [open, close]);
 };
 };