Browse Source

160341-172821 テキストスタイルタブの実装

mariko-h 3 months ago
parent
commit
cdab27084a

+ 26 - 4
apps/app/src/client/components/PageEditor/EditorGuideModal/EditorGuideModal.tsx

@@ -5,6 +5,11 @@ import {
 import { useEditorGuideModalStatus, useEditorGuideModalActions } from '@growi/editor/dist/states/modal/editor-guide';
 import { createPortal } from 'react-dom';
 
+import { DecorationTab } from './contents/DecorationTab';
+import { LayoutTab } from './contents/LayoutTab';
+import { TextStyleTab } from './contents/TextStyleTab';
+
+type TabType = 'textstyle' | 'layout' | 'decoration';
 type Props = {
   containerRef: RefObject<HTMLDivElement | null>,
 };
@@ -21,6 +26,8 @@ export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
   const [isShown, setIsShown] = useState(false);
   const [rect, setRect] = useState<DOMRect | null>(null);
 
+  const [activeTab, setActiveTab] = useState<TabType>('textstyle');
+
   // Get rect on open and on resize
   useLayoutEffect(() => {
     if (!isOpened || containerRef.current == null) return;
@@ -57,11 +64,26 @@ export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
               <h5 className="mb-0">Editor Guide</h5>
               <button type="button" className="btn-close" onClick={close} aria-label="Close" />
             </div>
+            <ul className="nav nav-tabs nav-fill border-bottom-0 mt-2">
+              {(['textstyle', 'layout', 'decoration'] as TabType[]).map(tab => (
+                <li key={tab} className="nav-item">
+                  <button
+                    type="button"
+                    className={`nav-link border-0 border-bottom border-3 py-2 ${
+                      activeTab === tab ? 'active border-primary fw-bold' : 'border-transparent text-secondary'}`}
+                    onClick={() => setActiveTab(tab)}
+                  >
+                    {tab === 'textstyle' && 'テキストスタイル'}
+                    {tab === 'layout' && 'レイアウト'}
+                    {tab === 'decoration' && '装飾'}
+                  </button>
+                </li>
+              ))}
+            </ul>
             <div className="card-body overflow-auto">
-              <p>This is a test modal.</p>
-              <p>It appears in the center of the preview area on the right side.</p>
-              <p>The background is darkened to emphasize the modal.</p>
-              <p className="mb-0">Click the close button or the background to close.</p>
+              {activeTab === 'textstyle' && <TextStyleTab />}
+              {activeTab === 'layout' && <LayoutTab />}
+              {activeTab === 'decoration' && <DecorationTab />}
             </div>
           </div>
         </div>

+ 201 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/contents/TextStyleTab.tsx

@@ -0,0 +1,201 @@
+import React from 'react';
+
+export const ExternalLinkIcon = () => {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      viewBox="0 0 24 24"
+      fill="none"
+      stroke="#ABB2BF"
+      strokeWidth="2"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      style={{
+        flex: 'none',
+        order: 1,
+        flexGrow: 0,
+        verticalAlign: 'middle',
+        marginLeft: '4px',
+      }}
+    >
+      <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
+      <polyline points="15 3 21 3 21 9"></polyline>
+      <line x1="10" y1="14" x2="21" y2="3"></line>
+    </svg>
+  );
+};
+
+const GuideRow = ({
+  title,
+  code,
+  preview,
+}: {
+  title: string;
+  code: string;
+  preview: React.ReactNode;
+}) => {
+  const handleCopy = async () => {
+    await navigator.clipboard.writeText(code);
+    alert('コピーしました!');
+  };
+
+  return (
+    <section className={title !== '' ? 'mt-4 mb-1' : 'mb-1'}>
+      {title !== '' && <h3 className="h6 fw-bold mb-2">{title}</h3>}
+      {/* flex-nowrap を追加して、全体が縦に並ばないように強制します */}
+      <div className="d-flex flex-row align-items-center gap-3 py-1 flex-nowrap">
+        {/* flex-shrink-0 を追加して、コード枠が右側のテキストに押しつぶされないようにします */}
+        <div onClick={handleCopy} style={{ cursor: 'pointer' }} className="flex-shrink-0">
+          <div
+            className="bg-dark text-light p-2 ps-2 pe-4 rounded position-relative"
+            style={{
+              backgroundColor: '#2D2E32',
+              width: 'fit-content',
+            }}
+          >
+            <pre className="m-0 small font-monospace" style={{ whiteSpace: 'pre', color: '#ABB2BF', lineHeight: '1.5' }}>
+              {code}
+            </pre>
+            <small className="position-absolute badge bg-secondary opacity-50" style={{ fontSize: '0.4rem', top: '2px', right: '4px' }}>
+              Click
+            </small>
+          </div>
+        </div>
+
+        {/* whiteSpace: 'nowrap' を追加して、右側のプレビューが勝手に改行されるのを防ぎます */}
+        <div className="flex-grow-1" style={{ whiteSpace: 'nowrap' }}>
+          <div className="wiki-content small">
+            {preview}
+          </div>
+        </div>
+      </div>
+    </section>
+  );
+};
+
+const TEXT_STYLE_GUIDES = [
+  {
+    id: 'bold',
+    title: '太字',
+    code: 'これは **太字** です\nこれは __太字__ です',
+    preview: <div className="lh-base">これは <strong>太字</strong> です<br />これは <strong>太字</strong> です</div>,
+  },
+  {
+    id: 'italic',
+    title: '斜体',
+    code: 'これは *斜体* です\nこれは _斜体_ です',
+    preview: <div className="lh-base">これは <em>斜体</em> です<br />これは <em>斜体</em> です</div>,
+  },
+  {
+    id: 'strikethrough',
+    title: '取り消し線',
+    code: '~~取り消します~~',
+    preview: <del>取り消します</del>,
+  },
+  {
+    id: 'inline-code',
+    title: 'インラインコード',
+    code: '`インラインコード` \n~~~インラインコード~~~',
+    preview: (
+      <div className="d-flex flex-column gap-2">
+        <code
+          className="rounded px-1"
+          style={{
+            width: 'fit-content',
+            color: '#D63384', // 文字の色
+            border: '1px solid #D63384', // 枠線の色(太さ1px、実線、指定の色)
+            backgroundColor: 'transparent', // 背景を透明にする場合(必要に応じて)
+          }}
+        >
+          インラインコード
+        </code>
+        <code
+          className="rounded px-1"
+          style={{
+            width: 'fit-content',
+            color: '#D63384', // 文字の色
+            border: '1px solid #D63384', // 枠線の色
+            backgroundColor: 'transparent',
+          }}
+        >
+          インラインコード
+        </code>
+      </div>
+    ),
+  },
+  {
+    id: 'bold-italic',
+    title: '全体が太字か斜体',
+    code: '***このテキストはすべて\n重要です***',
+    preview: <strong><u>このテキストはすべて重要です</u></strong>,
+  },
+  {
+    id: 'emoji',
+    title: '絵文字',
+    code: ':+1:\n:white_check_mark:\n:lock:',
+    preview: <span style={{ fontSize: '1.2rem' }}>👍✅🔒</span>,
+  },
+  {
+    id: 'sub',
+    title: '下付き・上付き',
+    code: 'これは<sub>下付き</sub>です',
+    preview: <span>これは<sub>下付き</sub>テキストです</span>,
+  },
+  {
+    id: 'sup',
+    title: '',
+    code: 'これは<sup>上付き</sup>です',
+    preview: <span>これは<sup>上付き</sup>テキストです</span>,
+  },
+  {
+    id: 'link-docs',
+    title: 'ラベル付きリンク',
+    code: '[GROWI ドキュメント](https://docs.growi.org/ja/g)',
+    preview: (
+      <a
+        href="https://docs.growi.org/ja/g"
+        target="_blank"
+        rel="noreferrer"
+        className="text-secondary text-decoration-underline"
+        style={{ color: '#777570' }}
+        onClick={e => e.stopPropagation()}
+      >
+        GROWI のリンク
+        <ExternalLinkIcon />
+      </a>
+    ),
+  },
+  {
+    id: 'link-sandbox',
+    title: '',
+    code: '[砂場ページはこちら](/Sandbox)',
+    preview: (
+      <a
+        href="/Sandbox"
+        className="text-secondary text-decoration-underline"
+        style={{ color: '#777570' }}
+        onClick={e => e.stopPropagation()}
+      >
+        砂場ページはこちら
+        <ExternalLinkIcon />
+      </a>
+    ),
+  },
+];
+
+export const TextStyleTab: React.FC = () => {
+  return (
+    <div className="px-4 py-2 overflow-y-auto" style={{ maxHeight: '80vh' }}>
+      {TEXT_STYLE_GUIDES.map(item => (
+        <GuideRow
+          key={item.id}
+          title={item.title}
+          code={item.code}
+          preview={item.preview}
+        />
+      ))}
+    </div>
+  );
+};