Explorar el Código

160341-180922 make components, fb response

mariko-h hace 1 mes
padre
commit
dec90bda2f

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

@@ -1078,7 +1078,6 @@
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "error-toaster": "Synchronization of the latest text failed"
   },
-
   "editor_guide": {
     "title": "Editor Guide",
     "tabs": {
@@ -1103,7 +1102,8 @@
       "link_sandbox": "Sandbox page is here",
       "all_important": "This text is all\nimportant",
       "sub_text": "subscript",
-      "sup_text": "superscript"
+      "sup_text": "superscript",
+      "is_text": "This is {{val}}"
     },
     "layout": {
       "copy_done": "Copied!",

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

@@ -1070,7 +1070,6 @@
     "skipped-toaster": "Le mode édition doit être activé pour déclencher la synchronisation. Synchronisation annulée.",
     "error-toaster": "Synchronisation échouée"
   },
-
   "editor_guide": {
     "title": "Guide de l'Éditeur",
     "tabs": {
@@ -1095,7 +1094,8 @@
       "link_sandbox": "La page bac à sable est ici",
       "all_important": "Tout ce texte est\nimportant",
       "sub_text": "indice",
-      "sup_text": "exposant"
+      "sup_text": "exposant",
+      "is_text": "Ceci est du {{val}}"
     },
     "layout": {
       "copy_done": "Copié !",

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

@@ -1111,7 +1111,6 @@
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "error-toaster": "最新の本文の同期に失敗しました"
   },
-
   "editor_guide": {
     "title": "エディターガイド",
     "tabs": {
@@ -1136,7 +1135,8 @@
       "link_sandbox": "砂場ページはこちら",
       "all_important": "このテキストはすべて\n重要です",
       "sub_text": "下付き",
-      "sup_text": "上付き"
+      "sup_text": "上付き",
+      "is_text": "これは{{val}}です"
     },
     "layout": {
       "copy_done": "コピーしました!",

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

@@ -1062,7 +1062,8 @@
       "link_sandbox": "연습장 페이지는 여기입니다",
       "all_important": "이 텍스트는 모두\n중요합니다",
       "sub_text": "아래 첨자",
-      "sup_text": "위 첨자"
+      "sup_text": "위 첨자",
+      "is_text": "이것은 {{val}}입니다"
     },
     "layout": {
       "copy_done": "복사되었습니다!",

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

@@ -1083,7 +1083,6 @@
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "error-toaster": "同步最新文本失败"
   },
-
   "editor_guide": {
     "title": "编辑器指南",
     "tabs": {
@@ -1108,7 +1107,8 @@
       "link_sandbox": "沙盒页面在这里",
       "all_important": "这段文字非常\n重要",
       "sub_text": "下标",
-      "sup_text": "上标"
+      "sup_text": "上标",
+      "is_text": "这是{{val}}效果"
     },
     "layout": {
       "copy_done": "已复制!",

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

@@ -1,4 +1,3 @@
-
 .editor-guide-modal {
 
   .modal-backdrop {
@@ -27,26 +26,28 @@
 
 
   .editor-guide-tabs-container {
-    .nav-tabs, ul {
-      display: flex !important;
-      flex-wrap: nowrap !important;
-      justify-content: space-between !important;
-      width: 100% !important;
-      padding: 0 !important;
-      margin: 0 !important;
-      list-style: none !important;
-    }
-
-    .nav-item, li {
-      flex: 1 1 0 !important;
-      min-width: 100px;
-      text-align: center !important;
-    }
-
-    .nav-link, button, a {
-      display: block !important;
-      width: 100% !important;
-      white-space: nowrap;
+    &: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;
+      }
     }
   }
 }

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

@@ -86,6 +86,16 @@ export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
     return () => cancelAnimationFrame(id);
   }, [isOpened]);
 
+  // Close on Escape key
+  useEffect(() => {
+    if (!isOpened) return;
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') close();
+    };
+    document.addEventListener('keydown', handleKeyDown);
+    return () => document.removeEventListener('keydown', handleKeyDown);
+  }, [isOpened, close]);
+
   if (!isOpened || rect == null) return <></>;
 
   const dynamicStyle: React.CSSProperties = {

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

@@ -0,0 +1,25 @@
+.copyButton {
+  display: block;
+  padding: 0;
+  text-align: left;
+  cursor: pointer;
+  background-color: transparent;
+  border: 0;
+}
+
+.codeBox {
+  width: fit-content;
+  background-color: var(--bs-dark);
+}
+
+.codePre {
+  margin: 0;
+  line-height: 1.5;
+  white-space: pre;
+}
+
+.copyBadge {
+  top: 4px;
+  right: 4px;
+  font-size: 0.4rem;
+}

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

@@ -0,0 +1,75 @@
+import type React from 'react';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+
+import styles from './GuideRow.module.scss';
+
+export interface LayoutGuideItem {
+  id: string;
+  title: string;
+  code: string;
+  preview?: React.ReactNode;
+  minWidth?: string;
+  underContent?: React.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 {
+      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 }}
+        >
+          <div
+            className={`${styles.codeBox} text-light p-2 ps-3 pe-5 rounded position-relative ${isFullWidth ? 'w-100' : ''}`}
+          >
+            <pre
+              className={`${styles.codePre} small font-monospace text-white-50 ${isFullWidth ? 'text-wrap' : ''}`}
+            >
+              {code}
+            </pre>
+            <small
+              className={`position-absolute badge bg-secondary opacity-50 ${styles.copyBadge}`}
+            >
+              Copy
+            </small>
+          </div>
+        </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>
+  );
+};

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

@@ -4,30 +4,6 @@
   overflow-y: auto;
 }
 
-.copyButton {
-  display: block;
-  padding: 0;
-  text-align: left;
-  cursor: pointer;
-  background-color: transparent;
-  border: 0;
-}
-
-.codeContainer {
-  background-color: var(--bs-dark);
-
-  pre {
-    margin: 0;
-    line-height: 1.5;
-  }
-}
-
-.copyBadge {
-  top: 4px;
-  right: 4px;
-  font-size: 0.4rem;
-}
-
 .dropdownMenu {
   max-height: 300px;
   margin-top: 0.125rem;

+ 21 - 68
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/DecorationTab.tsx

@@ -1,21 +1,12 @@
 import type React from 'react';
-import { useMemo, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { toastSuccess } from '~/client/util/toastr';
+import type { LayoutGuideItem } from '../components/GuideRow';
+import { GuideRow } from '../components/GuideRow';
 
 import styles from './DecorationTab.module.scss';
 
-interface LayoutGuideItem {
-  id: string;
-  title: string;
-  code: string;
-  preview?: React.ReactNode;
-  minWidth?: string;
-  underContent?: React.ReactNode;
-}
-type GuideRowProps = Omit<LayoutGuideItem, 'id'>;
-
 const BOOTSTRAP_COLORS = [
   'primary',
   'danger',
@@ -28,65 +19,27 @@ const BOOTSTRAP_COLORS = [
 ] as const;
 type BootstrapColor = (typeof BOOTSTRAP_COLORS)[number];
 
-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-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 }}
-        >
-          <div
-            className={`${styles.codeContainer} text-light p-2 ps-3 pe-5 rounded position-relative ${isFullWidth ? 'w-100' : ''}`}
-          >
-            <pre
-              className={`small font-monospace text-white-50 ${isFullWidth ? 'text-wrap' : ''}`}
-            >
-              {code}
-            </pre>
-            <small
-              className={`position-absolute badge bg-secondary opacity-50 ${styles.copyBadge}`}
-            >
-              Copy
-            </small>
-          </div>
-        </button>
-
-        {preview && (
-          <div
-            className={`flex-grow-0 flex-shrink-0 ${isFullWidth ? 'w-100' : ''}`}
-          >
-            <div className="wiki-content small">{preview}</div>
-          </div>
-        )}
-      </div>
-      {underContent && <div className="mt-2 w-100">{underContent}</div>}
-    </section>
-  );
-};
-
 export const DecorationTab: React.FC = () => {
   const { t } = useTranslation();
   const i18nKey = 'editor_guide.decoration';
   const [currentStyle, setCurrentStyle] = useState<BootstrapColor>('primary');
   const [isOpen, setIsOpen] = useState(false);
+  const dropdownRef = useRef<HTMLDivElement>(null);
+
+  // Close dropdown on outside click
+  useEffect(() => {
+    if (!isOpen) return;
+    const handleClickOutside = (e: MouseEvent) => {
+      if (
+        dropdownRef.current &&
+        !dropdownRef.current.contains(e.target as Node)
+      ) {
+        setIsOpen(false);
+      }
+    };
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => document.removeEventListener('mousedown', handleClickOutside);
+  }, [isOpen]);
 
   const colorConfigs: Record<BootstrapColor, { icon: string; prefix: string }> =
     {
@@ -173,7 +126,7 @@ export const DecorationTab: React.FC = () => {
       {
         id: 'back-color',
         title: t(`${i18nKey}.back_color`),
-        code: `<p class="text-white minWidth: '100%' bg-${styleConfig.colorName}">${t(`${i18nKey}.placeholder`)}</p>`,
+        code: `<p class="text-white bg-${styleConfig.colorName}">${t(`${i18nKey}.placeholder`)}</p>`,
         underContent: (
           <p className={`text-white bg-${styleConfig.colorName} px-2 m-0`}>
             {t(`${i18nKey}.placeholder`)}
@@ -198,7 +151,7 @@ export const DecorationTab: React.FC = () => {
     <div className={`px-4 py-3 ${styles.decorationTab}`}>
       <section className="mb-4">
         <h3 className="fw-bold mb-2 fs-5">{t(`${i18nKey}.style`)}</h3>
-        <div className={`dropdown ${isOpen ? 'show' : ''}`}>
+        <div ref={dropdownRef} className={`dropdown ${isOpen ? 'show' : ''}`}>
           <button
             className={`btn btn-light border dropdown-toggle d-flex align-items-center gap-2 text-${styleConfig.colorName === 'light' ? 'dark' : styleConfig.colorName}`}
             type="button"

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

@@ -3,22 +3,6 @@
   max-height: 80vh;
 }
 
-.codeBox {
-  width: fit-content;
-  background-color: var(--bs-dark);
-}
-
-.codePre {
-  line-height: 1.5;
-  white-space: pre;
-}
-
-.copyBadge {
-  top: 4px;
-  right: 4px;
-  font-size: 0.4rem;
-}
-
 .checkboxMock {
   width: 18px;
   height: 18px;

+ 2 - 64
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/LayoutTab.tsx

@@ -1,73 +1,11 @@
 import type React from 'react';
-import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import type { LayoutGuideItem } from '../components/GuideRow';
+import { GuideRow } from '../components/GuideRow';
 
 import styles from './LayoutTab.module.scss';
 
-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-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={`p-0 text-start border-0 bg-transparent ${styles.copyButton}`}
-        >
-          <div
-            className={`text-light p-2 ps-3 pe-5 rounded position-relative ${styles.codeBox}`}
-            style={{ minWidth }}
-          >
-            <pre
-              className={`m-0 small font-monospace text-white-50 ${styles.codePre}`}
-            >
-              {code}
-            </pre>
-            <small
-              className={`position-absolute badge bg-secondary opacity-50 ${styles.copyBadge}`}
-            >
-              Copy
-            </small>
-          </div>
-        </button>
-        {preview && (
-          <div className={`flex-grow-1 ${styles.previewContainer}`}>
-            <div className="wiki-content small">{preview}</div>
-          </div>
-        )}
-      </div>
-
-      {underContent && <div className="mt-2 w-100">{underContent}</div>}
-    </section>
-  );
-};
-
 export const LayoutTab: React.FC = () => {
   const { t } = useTranslation();
   const i18nKey = 'editor_guide.layout';

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

@@ -4,12 +4,12 @@
 
 .codeBlockWrapper {
   width: fit-content;
-  background-color: #2D2E32;
+  background-color: var(--bs-dark);
 }
 
 .codeContent {
   font-size: 14px;
-  color: #ABB2BF;
+  color: rgba(var(--bs-white-rgb), 0.5);
 }
 
 .copyBadge {
@@ -24,10 +24,10 @@
 
 .inlineCodeLabel {
   width: fit-content;
-  color: #D63384;
-  border: 1px solid #D63384;
+  color: var(--bs-pink);
+  border: 1px solid var(--bs-pink);
 }
 
 .externalLink {
-  color: #777570 !important;
+  color: var(--bs-secondary-color);
 }

+ 13 - 8
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/TextStyleTab.tsx

@@ -1,7 +1,8 @@
 import type React from 'react';
+import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { toastSuccess } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import styles from './TextStyleTab.module.scss';
 
@@ -15,10 +16,14 @@ const GuideRow = ({
   preview: React.ReactNode;
 }) => {
   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 (
     <section className={title !== '' ? 'mt-4 mb-1' : 'mb-1'}>
@@ -123,7 +128,7 @@ export const TextStyleTab: React.FC = () => {
       code: `***${t(`${i18nKey}.all_important`)}***`,
       preview: (
         <strong>
-          <u>{t(`${i18nKey}.all_important`).replace('\n', '')}</u>
+          <em>{t(`${i18nKey}.all_important`).replace('\n', '')}</em>
         </strong>
       ),
     },
@@ -169,7 +174,7 @@ export const TextStyleTab: React.FC = () => {
           target="_blank"
           rel="noreferrer"
           className="text-secondary text-decoration-underline"
-          style={{ color: '#777570' }}
+          style={{ color: 'var(--bs-secondary-color)' }}
           onClick={(e) => e.stopPropagation()}
         >
           {t(`${i18nKey}.link_growi`)}
@@ -185,7 +190,7 @@ export const TextStyleTab: React.FC = () => {
         <a
           href="/Sandbox"
           className="text-secondary text-decoration-underline"
-          style={{ color: '#777570' }}
+          style={{ color: 'var(--bs-secondary-color)' }}
           onClick={(e) => e.stopPropagation()}
         >
           {t(`${i18nKey}.link_sandbox`)}

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

@@ -1,3 +1,4 @@
+import { useMemo } from 'react';
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 
 export type EditorGuideModalState = {
@@ -8,19 +9,20 @@ const editorGuideModalAtom = atom<EditorGuideModalState>({
   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 = () => {
   return useAtomValue(editorGuideModalAtom);
 };
 
 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]);
 };