refactor(markdown-editor): migrate to tiptap for WYSIWYG editing, standardize UI spacing, and update export engine
This commit is contained in:
BIN
._MARKDOWN_EDITOR_REWRITE_PLAN.md
Executable file
BIN
._MARKDOWN_EDITOR_REWRITE_PLAN.md
Executable file
Binary file not shown.
BIN
._MARKDOWN_EDITOR_UX_FINDINGS.md
Executable file
BIN
._MARKDOWN_EDITOR_UX_FINDINGS.md
Executable file
Binary file not shown.
BIN
._tailwind.config.js
Executable file
BIN
._tailwind.config.js
Executable file
Binary file not shown.
46
MARKDOWN_EDITOR_REWRITE_PLAN.md
Executable file
46
MARKDOWN_EDITOR_REWRITE_PLAN.md
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
# Markdown Editor - Architecture Rewrite Plan
|
||||||
|
|
||||||
|
## Context & Motivation
|
||||||
|
The current Markdown Editor violates the core UX philosophy of the Dewe.Dev suite: `[input] program -> [process/edit] human -> [output] program`.
|
||||||
|
|
||||||
|
Currently, users input Markdown and edit raw Markdown in a CodeMirror instance. To align with the rest of the suite (Object Editor, Table Editor, Invoice Editor), the tool must provide a **WYSIWYG (What You See Is What You Get) Rich Text Editor**. The user should interact with human-readable text visually, while the system seamlessly translates it back to raw Markdown/HTML for export.
|
||||||
|
|
||||||
|
## Phase 1: Dependency Updates & Tiptap Integration
|
||||||
|
To achieve a robust WYSIWYG experience that translates perfectly to/from Markdown, we will use **Tiptap** (a headless wrapper around ProseMirror).
|
||||||
|
|
||||||
|
**Dependencies to Install:**
|
||||||
|
* `@tiptap/react`
|
||||||
|
* `@tiptap/starter-kit` (Core formatting)
|
||||||
|
* `@tiptap/extension-link` (Hyperlinks)
|
||||||
|
* `@tiptap/extension-image` (Image support)
|
||||||
|
* `@tiptap/extension-table` (Table support)
|
||||||
|
* `@tiptap/extension-task-list` & `@tiptap/extension-task-item`
|
||||||
|
* `tiptap-markdown` (Crucial: Handles native parsing/serializing of markdown to the Tiptap state)
|
||||||
|
|
||||||
|
**Dependencies to Remove:**
|
||||||
|
* `marked` (Tiptap handles parsing now)
|
||||||
|
* `dompurify` (Tiptap handles sanitization)
|
||||||
|
* Custom CodeMirror Markdown implementations inside `MarkdownEditor.js` (We will keep CodeMirror only for the final "Export Code" view, if needed).
|
||||||
|
|
||||||
|
## Phase 2: Component Restructuring
|
||||||
|
Create a new component: `RichMarkdownEditor.js`.
|
||||||
|
This component will replace the split-view CodeMirror setup.
|
||||||
|
|
||||||
|
**Features of `RichMarkdownEditor.js`:**
|
||||||
|
1. **Floating/Sticky Toolbar:** Similar to Google Docs or Notion. Bold, Italic, H1-H3, Lists, Blockquotes, Code Blocks.
|
||||||
|
2. **Interactive Editor Content:** The actual prose area where users type naturally.
|
||||||
|
3. **Two-way Binding:** When the Tiptap state updates, it immediately serializes the state to a hidden Markdown string, which is saved to `localStorage` (for data-loss prevention) and readied for export.
|
||||||
|
|
||||||
|
## Phase 3: UX Flow Adjustments (Addressing Previous Findings)
|
||||||
|
* **Input Flow:** User pastes Markdown -> Tiptap parses it via `tiptap-markdown` -> User sees rendered Rich Text immediately (Fixes UX Gap #3 from previous findings).
|
||||||
|
* **View Modes:** Remove the complex "Split View" vs "Editor Only" logic. The primary view is *always* the Rich Text Editor.
|
||||||
|
* **File Uploads:** Restrict the `<input type="file">` to only accept `.md` and `.txt` files. Remove UI mentions of `.html` or `.docx` until a dedicated parser (like mammoth.js) is explicitly implemented (Fixes UX Gap #2).
|
||||||
|
|
||||||
|
## Phase 4: Export Engine
|
||||||
|
The Export card will now feature:
|
||||||
|
1. **Raw Markdown:** A read-only CodeMirror block displaying the exact Markdown output generated by Tiptap, with "Copy" and "Download" buttons.
|
||||||
|
2. **HTML:** Tiptap's `editor.getHTML()` output.
|
||||||
|
3. **PDF Export:** Retain `html2pdf.js`, but explicitly inject CSS print rules (`break-inside: avoid`, `white-space: pre-wrap`) targeting Tiptap's code block classes to prevent page overflow (Fixes UX Gap #4).
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
By executing this rewrite, the Markdown Editor will transition from a basic code validator into a premium document translation hub, perfectly aligning with the product's overarching vision.
|
||||||
21
MARKDOWN_EDITOR_UX_FINDINGS.md
Executable file
21
MARKDOWN_EDITOR_UX_FINDINGS.md
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
# Markdown Editor UX Findings & Proposed Improvements
|
||||||
|
|
||||||
|
## 1. Core Goal Misalignment (The Primary UX Defect)
|
||||||
|
**Current State:** The user inputs markdown (via paste, URL, file, or typing), and the primary editing experience happens in a raw text editor (CodeMirror) where they edit markdown syntax. The "rendered" version is read-only (Preview).
|
||||||
|
**The Gap:** As a developer tool, the goal is often to translate system language (markdown) into human language (rich text) *and vice-versa*. Humans expect a WYSIWYG (What You See Is What You Get) document editor experience.
|
||||||
|
**The Proposed Fix:** The "Preview" should actually be an interactive, editable Rich Text interface. A user should be able to input raw markdown, see the rich text, *edit the rich text visually like a Word document*, and then export the result back to raw markdown.
|
||||||
|
|
||||||
|
## 2. File Import Format Limitations
|
||||||
|
**Current State:** The UI implies users can open files.
|
||||||
|
**The Gap:** `.html` and `.docx` imports require external libraries (`turndown`, `mammoth.js`) which are not yet fully implemented according to the Phase 2 roadmap.
|
||||||
|
**The Proposed Fix:** Restrict the `<input type="file">` to `.md` and `.txt` until the conversion logic is built, preventing users from loading raw binary/HTML into the text editor.
|
||||||
|
|
||||||
|
## 3. View Mode Toggles & Workflow
|
||||||
|
**Current State:** Defaults to 'split' on desktop and 'editor' on mobile. After importing data, it jumps to the raw editor.
|
||||||
|
**The Gap:** Similar to the Object Editor, jumping straight to raw code isn't ideal.
|
||||||
|
**The Proposed Fix:** After data is provided, default to the (newly proposed) Rich Text / Preview mode first.
|
||||||
|
|
||||||
|
## 4. PDF Export Styling
|
||||||
|
**Current State:** Uses `html2pdf.js` for export.
|
||||||
|
**The Gap:** Long code blocks in markdown often overflow the page boundaries when converted to PDF because CSS print media rules for `white-space: pre-wrap` or `break-inside` are missing.
|
||||||
|
**The Proposed Fix:** Inject print-specific CSS rules before triggering the PDF export to ensure code blocks wrap gracefully.
|
||||||
1316
package-lock.json
generated
1316
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -18,9 +18,21 @@
|
|||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.26.1",
|
||||||
|
"@tiptap/extension-image": "^3.26.1",
|
||||||
|
"@tiptap/extension-link": "^3.26.1",
|
||||||
|
"@tiptap/extension-table": "^3.26.1",
|
||||||
|
"@tiptap/extension-table-cell": "^3.26.1",
|
||||||
|
"@tiptap/extension-table-header": "^3.26.1",
|
||||||
|
"@tiptap/extension-table-row": "^3.26.1",
|
||||||
|
"@tiptap/extension-task-item": "^3.26.1",
|
||||||
|
"@tiptap/extension-task-list": "^3.26.1",
|
||||||
|
"@tiptap/react": "^3.26.1",
|
||||||
|
"@tiptap/starter-kit": "^3.26.1",
|
||||||
"@uiw/react-codemirror": "^4.25.1",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
@@ -31,6 +43,7 @@
|
|||||||
"js-beautify": "^1.15.4",
|
"js-beautify": "^1.15.4",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"lucide-react": "^0.540.0",
|
"lucide-react": "^0.540.0",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"marked-emoji": "^2.0.1",
|
"marked-emoji": "^2.0.1",
|
||||||
@@ -44,6 +57,8 @@
|
|||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"serve": "^14.2.4",
|
"serve": "^14.2.4",
|
||||||
|
"tailwindcss-typography": "^3.1.0",
|
||||||
|
"tiptap-markdown": "^0.9.0",
|
||||||
"turndown": "^7.2.1",
|
"turndown": "^7.2.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
src/components/._CodeBlockComponent.js
Executable file
BIN
src/components/._CodeBlockComponent.js
Executable file
Binary file not shown.
BIN
src/components/._RichMarkdownEditor.js
Executable file
BIN
src/components/._RichMarkdownEditor.js
Executable file
Binary file not shown.
41
src/components/CodeBlockComponent.js
Executable file
41
src/components/CodeBlockComponent.js
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
|
|
||||||
|
const CodeBlockComponent = ({ node, updateAttributes, extension }) => {
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(node.textContent).then(() => {
|
||||||
|
// Optional: Add visual feedback for copy
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="code-block-wrapper relative">
|
||||||
|
<div className="code-block-header flex justify-between items-center px-4 py-2 bg-[#161b22] border border-[#30363d] border-b-0 rounded-t-md text-xs font-mono">
|
||||||
|
<select
|
||||||
|
contentEditable={false}
|
||||||
|
value={node.attrs.language || "text"}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateAttributes({ language: event.target.value })
|
||||||
|
}
|
||||||
|
className="bg-transparent text-[#8b949e] border-none outline-none focus:ring-0 uppercase tracking-wider cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="text">text</option>
|
||||||
|
<option value="javascript">javascript</option>
|
||||||
|
<option value="typescript">typescript</option>
|
||||||
|
<option value="html">html</option>
|
||||||
|
<option value="css">css</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="bash">bash</option>
|
||||||
|
<option value="python">python</option>
|
||||||
|
<option value="sql">sql</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<pre className="!mt-0 !rounded-t-none !bg-transparent !border-[#30363d] !p-4 !text-[#e6edf3]">
|
||||||
|
<NodeViewContent as="code" />
|
||||||
|
</pre>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeBlockComponent;
|
||||||
289
src/components/RichMarkdownEditor.js
Executable file
289
src/components/RichMarkdownEditor.js
Executable file
@@ -0,0 +1,289 @@
|
|||||||
|
import React, { useEffect, useCallback } from "react";
|
||||||
|
import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import Link from "@tiptap/extension-link";
|
||||||
|
import Image from "@tiptap/extension-image";
|
||||||
|
import { Table } from "@tiptap/extension-table";
|
||||||
|
import { TableRow } from "@tiptap/extension-table-row";
|
||||||
|
import { TableHeader } from "@tiptap/extension-table-header";
|
||||||
|
import { TableCell } from "@tiptap/extension-table-cell";
|
||||||
|
import { TaskList } from "@tiptap/extension-task-list";
|
||||||
|
import { TaskItem } from "@tiptap/extension-task-item";
|
||||||
|
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||||
|
import { common, createLowlight } from "lowlight";
|
||||||
|
import CodeBlockComponent from "./CodeBlockComponent";
|
||||||
|
import { Markdown } from "tiptap-markdown";
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Strikethrough,
|
||||||
|
Code,
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
CheckSquare,
|
||||||
|
Quote,
|
||||||
|
Link2,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Table as TableIcon,
|
||||||
|
Minus,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// Set up lowlight for syntax highlighting in Tiptap
|
||||||
|
const lowlight = createLowlight(common);
|
||||||
|
|
||||||
|
const MenuBar = ({ editor }) => {
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-center items-center gap-1 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10 w-full">
|
||||||
|
<div className="flex flex-wrap items-center gap-1 w-full max-w-[85ch]">
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bold") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Bold"
|
||||||
|
>
|
||||||
|
<Bold className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("italic") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Italic"
|
||||||
|
>
|
||||||
|
<Italic className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("strike") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Strikethrough"
|
||||||
|
>
|
||||||
|
<Strikethrough className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleCode().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("code") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Inline Code"
|
||||||
|
>
|
||||||
|
<Code className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||||
|
}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 1 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Heading 1"
|
||||||
|
>
|
||||||
|
<Heading1 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||||
|
}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 2 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Heading 2"
|
||||||
|
>
|
||||||
|
<Heading2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||||
|
}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 3 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Heading 3"
|
||||||
|
>
|
||||||
|
<Heading3 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bulletList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Bullet List"
|
||||||
|
>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("orderedList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Ordered List"
|
||||||
|
>
|
||||||
|
<ListOrdered className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("taskList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Task List"
|
||||||
|
>
|
||||||
|
<CheckSquare className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("blockquote") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Blockquote"
|
||||||
|
>
|
||||||
|
<Quote className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("codeBlock") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Code Block"
|
||||||
|
>
|
||||||
|
<Code className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
|
||||||
|
title="Horizontal Rule"
|
||||||
|
>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = window.prompt("URL");
|
||||||
|
if (url) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.extendMarkRange("link")
|
||||||
|
.setLink({ href: url })
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("link") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
|
||||||
|
title="Add Link"
|
||||||
|
>
|
||||||
|
<Link2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = window.prompt("Image URL");
|
||||||
|
if (url) {
|
||||||
|
editor.chain().focus().setImage({ src: url }).run();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
|
||||||
|
title="Add Image"
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
|
||||||
|
title="Insert Table"
|
||||||
|
>
|
||||||
|
<TableIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RichMarkdownEditor = ({
|
||||||
|
initialContent,
|
||||||
|
onChange,
|
||||||
|
className = "",
|
||||||
|
height = "600px",
|
||||||
|
isFullscreen = false,
|
||||||
|
}) => {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
codeBlock: false, // We'll use our own codeblock extension
|
||||||
|
}),
|
||||||
|
CodeBlockLowlight.extend({
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(CodeBlockComponent);
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
lowlight,
|
||||||
|
}),
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
}),
|
||||||
|
Image,
|
||||||
|
Table.configure({
|
||||||
|
resizable: true,
|
||||||
|
}),
|
||||||
|
TableRow,
|
||||||
|
TableHeader,
|
||||||
|
TableCell,
|
||||||
|
TaskList,
|
||||||
|
TaskItem.configure({
|
||||||
|
nested: true,
|
||||||
|
}),
|
||||||
|
Markdown.configure({
|
||||||
|
html: true,
|
||||||
|
tightLists: true,
|
||||||
|
tightListClass: "tight",
|
||||||
|
bulletListMarker: "-",
|
||||||
|
linkify: true,
|
||||||
|
breaks: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: initialContent,
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
// Serialize back to markdown and send to parent
|
||||||
|
const markdownOutput = editor.storage.markdown.getMarkdown();
|
||||||
|
const htmlOutput = editor.getHTML();
|
||||||
|
onChange(markdownOutput, htmlOutput);
|
||||||
|
},
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class:
|
||||||
|
"prose prose-sm sm:prose dark:prose-invert prose-blue focus:outline-none w-full max-w-none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update editor content when initialContent prop completely changes from outside (e.g. loading a template)
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && initialContent !== undefined) {
|
||||||
|
const currentMarkdown = editor.storage.markdown.getMarkdown();
|
||||||
|
if (initialContent !== currentMarkdown) {
|
||||||
|
editor.commands.setContent(initialContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editor, initialContent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col bg-white dark:bg-gray-900 overflow-hidden ${className}`}
|
||||||
|
>
|
||||||
|
<MenuBar editor={editor} />
|
||||||
|
<div
|
||||||
|
className={`overflow-y-auto w-full custom-scrollbar flex justify-center p-6`}
|
||||||
|
style={{ height }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-full max-w-[85ch] markdown-content-wrapper ${isFullscreen ? "is-fullscreen" : "is-normal"} is-edit-mode`}
|
||||||
|
>
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RichMarkdownEditor;
|
||||||
BIN
src/pages/._MarkdownEditor.js
Executable file
BIN
src/pages/._MarkdownEditor.js
Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
src/styles/._markdown-preview.css
Executable file
BIN
src/styles/._markdown-preview.css
Executable file
Binary file not shown.
@@ -1,23 +1,25 @@
|
|||||||
/* GitHub-style Markdown Preview Styling */
|
/* GitHub-style Markdown Preview Styling */
|
||||||
.markdown-preview {
|
.markdown-preview {
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
font-size: 16px;
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
|
||||||
line-height: 1.6;
|
Arial, sans-serif;
|
||||||
word-wrap: break-word;
|
font-size: 16px;
|
||||||
overflow-wrap: break-word;
|
line-height: 1.6;
|
||||||
max-width: 100%;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
overflow-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure all child elements respect container width */
|
/* Ensure all child elements respect container width */
|
||||||
.markdown-preview * {
|
.markdown-preview * {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview {
|
.dark .markdown-preview {
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h1,
|
.markdown-preview h1,
|
||||||
@@ -26,254 +28,256 @@
|
|||||||
.markdown-preview h4,
|
.markdown-preview h4,
|
||||||
.markdown-preview h5,
|
.markdown-preview h5,
|
||||||
.markdown-preview h6 {
|
.markdown-preview h6 {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h1 {
|
.markdown-preview h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h1 {
|
.dark .markdown-preview h1 {
|
||||||
border-bottom-color: #21262d;
|
border-bottom-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h2 {
|
.markdown-preview h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h2 {
|
.dark .markdown-preview h2 {
|
||||||
border-bottom-color: #21262d;
|
border-bottom-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h3 {
|
.markdown-preview h3 {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h4 {
|
.markdown-preview h4 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h5 {
|
.markdown-preview h5 {
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h6 {
|
.markdown-preview h6 {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h6 {
|
.dark .markdown-preview h6 {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview p {
|
.markdown-preview p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inline code - with background */
|
/* Inline code - with background */
|
||||||
.markdown-preview code {
|
.markdown-preview code {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: rgba(175, 184, 193, 0.2);
|
background-color: rgba(175, 184, 193, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
"Liberation Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview code {
|
.dark .markdown-preview code {
|
||||||
background-color: rgba(110, 118, 129, 0.4);
|
background-color: rgba(110, 118, 129, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block wrapper with header */
|
/* Code block wrapper with header */
|
||||||
.markdown-preview .code-block-wrapper {
|
.markdown-preview .code-block-wrapper {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-wrapper {
|
.dark .markdown-preview .code-block-wrapper {
|
||||||
border-color: #30363d;
|
border-color: #30363d;
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block header */
|
/* Code block header */
|
||||||
.markdown-preview .code-block-header {
|
.markdown-preview .code-block-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-header {
|
.dark .markdown-preview .code-block-header {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
border-bottom-color: #30363d;
|
border-bottom-color: #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Language label */
|
/* Language label */
|
||||||
.markdown-preview .code-block-language {
|
.markdown-preview .code-block-language {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-language {
|
.dark .markdown-preview .code-block-language {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy button */
|
/* Copy button */
|
||||||
.markdown-preview .code-block-copy {
|
.markdown-preview .code-block-copy {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview .code-block-copy:hover {
|
.markdown-preview .code-block-copy:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
border-color: #1f2328;
|
border-color: #1f2328;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-copy {
|
.dark .markdown-preview .code-block-copy {
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
border-color: #30363d;
|
border-color: #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-copy:hover {
|
.dark .markdown-preview .code-block-copy:hover {
|
||||||
background-color: #21262d;
|
background-color: #21262d;
|
||||||
border-color: #8b949e;
|
border-color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code blocks - with background */
|
/* Code blocks - with background */
|
||||||
.markdown-preview .code-block-wrapper pre {
|
.markdown-preview .code-block-wrapper pre {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Legacy pre blocks (without wrapper) */
|
/* Legacy pre blocks (without wrapper) */
|
||||||
.markdown-preview pre:not(.code-block-wrapper pre) {
|
.markdown-preview pre:not(.code-block-wrapper pre) {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #afb8c133;
|
background-color: #afb8c133;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview pre:not(.code-block-wrapper pre) {
|
.dark .markdown-preview pre:not(.code-block-wrapper pre) {
|
||||||
background-color: rgba(110, 118, 129, 0.4);
|
background-color: rgba(110, 118, 129, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code inside pre blocks - NO background (transparent) */
|
/* Code inside pre blocks - NO background (transparent) */
|
||||||
.markdown-preview pre code {
|
.markdown-preview pre code {
|
||||||
display: inline;
|
display: inline;
|
||||||
max-width: auto;
|
max-width: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preserve highlight.js syntax highlighting colors */
|
/* Preserve highlight.js syntax highlighting colors */
|
||||||
.markdown-preview pre code.hljs {
|
.markdown-preview pre code.hljs {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table {
|
.markdown-preview table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table tr {
|
.markdown-preview table tr {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-top: 1px solid #d0d7de;
|
border-top: 1px solid #d0d7de;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table tr {
|
.dark .markdown-preview table tr {
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
border-top-color: #21262d;
|
border-top-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table tr:nth-child(2n) {
|
.markdown-preview table tr:nth-child(2n) {
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table tr:nth-child(2n) {
|
.dark .markdown-preview table tr:nth-child(2n) {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table th,
|
.markdown-preview table th,
|
||||||
.markdown-preview table td {
|
.markdown-preview table td {
|
||||||
padding: 6px 13px;
|
padding: 6px 13px;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table th,
|
.dark .markdown-preview table th,
|
||||||
.dark .markdown-preview table td {
|
.dark .markdown-preview table td {
|
||||||
border-color: #21262d;
|
border-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table th {
|
.markdown-preview table th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table th {
|
.dark .markdown-preview table th {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview blockquote {
|
.markdown-preview blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
border-left: 0.25em solid #d0d7de;
|
border-left: 0.25em solid #d0d7de;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview blockquote {
|
.dark .markdown-preview blockquote {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
border-left-color: #3b434b;
|
border-left-color: #3b434b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul,
|
.markdown-preview ul,
|
||||||
.markdown-preview ol {
|
.markdown-preview ol {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nested lists */
|
/* Nested lists */
|
||||||
@@ -281,100 +285,192 @@
|
|||||||
.markdown-preview ul ol,
|
.markdown-preview ul ol,
|
||||||
.markdown-preview ol ul,
|
.markdown-preview ol ul,
|
||||||
.markdown-preview ol ol {
|
.markdown-preview ol ol {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List items */
|
/* List items */
|
||||||
.markdown-preview li {
|
.markdown-preview li {
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li + li {
|
.markdown-preview li + li {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better bullet points */
|
/* Better bullet points */
|
||||||
.markdown-preview ul > li {
|
.markdown-preview ul > li {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul ul > li {
|
.markdown-preview ul ul > li {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul ul ul > li {
|
.markdown-preview ul ul ul > li {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ordered list styling */
|
/* Ordered list styling */
|
||||||
.markdown-preview ol > li {
|
.markdown-preview ol > li {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ol ol > li {
|
.markdown-preview ol ol > li {
|
||||||
list-style-type: lower-alpha;
|
list-style-type: lower-alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ol ol ol > li {
|
.markdown-preview ol ol ol > li {
|
||||||
list-style-type: lower-roman;
|
list-style-type: lower-roman;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List item content spacing */
|
/* List item content spacing */
|
||||||
.markdown-preview li > p {
|
.markdown-preview li > p {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li > p:first-child {
|
.markdown-preview li > p:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li > p:last-child {
|
.markdown-preview li > p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview hr {
|
.markdown-preview hr {
|
||||||
height: 0.25em;
|
height: 0.25em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
background-color: #d0d7de;
|
background-color: #d0d7de;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview hr {
|
.dark .markdown-preview hr {
|
||||||
background-color: #21262d;
|
background-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview a {
|
.markdown-preview a {
|
||||||
color: #0969da;
|
color: #0969da;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview a {
|
.dark .markdown-preview a {
|
||||||
color: #58a6ff;
|
color: #58a6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview a:hover {
|
.markdown-preview a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview strong {
|
.markdown-preview strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview em {
|
.markdown-preview em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview u {
|
.markdown-preview u {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview img {
|
.markdown-preview img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tiptap specific styling overrides to match prose */
|
||||||
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
|
color: #adb5bd;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type="taskList"] {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type="taskList"] li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type="taskList"] li > label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type="taskList"] li > div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type="taskList"] li > div > p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing logic for PDF export */
|
||||||
|
@media print {
|
||||||
|
.tiptap pre,
|
||||||
|
.markdown-preview pre {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Node Views (Code Block) */
|
||||||
|
.tiptap .code-block-wrapper {
|
||||||
|
margin-bottom: 0.65em;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #0d1117;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .code-block-wrapper pre {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Content Wrapper Padding Strategies */
|
||||||
|
.markdown-content-wrapper.is-normal.is-read-mode > .prose {
|
||||||
|
padding-bottom: 3rem; /* 48px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content-wrapper.is-fullscreen.is-read-mode > .prose {
|
||||||
|
padding-bottom: 4rem; /* 128px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content-wrapper.is-normal.is-edit-mode > div {
|
||||||
|
padding-bottom: 3rem; /* 48px */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content-wrapper.is-fullscreen.is-edit-mode > div {
|
||||||
|
padding-bottom: 6rem; /* 128px */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,165 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
"./src/**/*.{js,jsx,ts,tsx}",
|
darkMode: "class", // Enable manual dark mode control via class
|
||||||
],
|
|
||||||
darkMode: 'class', // Enable manual dark mode control via class
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#f0f9ff',
|
50: "#f0f9ff",
|
||||||
100: '#e0f2fe',
|
100: "#e0f2fe",
|
||||||
200: '#bae6fd',
|
200: "#bae6fd",
|
||||||
300: '#7dd3fc',
|
300: "#7dd3fc",
|
||||||
400: '#38bdf8',
|
400: "#38bdf8",
|
||||||
500: '#0ea5e9',
|
500: "#0ea5e9",
|
||||||
600: '#0284c7',
|
600: "#0284c7",
|
||||||
700: '#0369a1',
|
700: "#0369a1",
|
||||||
800: '#075985',
|
800: "#075985",
|
||||||
900: '#0c4a6e',
|
900: "#0c4a6e",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'],
|
mono: [
|
||||||
|
"JetBrains Mono",
|
||||||
|
"Monaco",
|
||||||
|
"Cascadia Code",
|
||||||
|
"Segoe UI Mono",
|
||||||
|
"Roboto Mono",
|
||||||
|
"Oxygen Mono",
|
||||||
|
"Ubuntu Monospace",
|
||||||
|
"Source Code Pro",
|
||||||
|
"Fira Code",
|
||||||
|
"Droid Sans Mono",
|
||||||
|
"Courier New",
|
||||||
|
"monospace",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
maxWidth: {
|
typography: (theme) => ({
|
||||||
'1/4': '25%',
|
DEFAULT: {
|
||||||
'1/2': '50%',
|
css: {
|
||||||
'3/4': '75%',
|
"--tw-prose-body": "#24292f",
|
||||||
}
|
"--tw-prose-headings": "#24292f",
|
||||||
|
"--tw-prose-lead": "#57606a",
|
||||||
|
"--tw-prose-links": "#0969da",
|
||||||
|
"--tw-prose-bold": "#24292f",
|
||||||
|
"--tw-prose-counters": "#57606a",
|
||||||
|
"--tw-prose-bullets": "#d0d7de",
|
||||||
|
"--tw-prose-hr": "#d0d7de",
|
||||||
|
"--tw-prose-quotes": "#57606a",
|
||||||
|
"--tw-prose-quote-borders": "#d0d7de",
|
||||||
|
"--tw-prose-captions": "#57606a",
|
||||||
|
"--tw-prose-code": "#24292f",
|
||||||
|
"--tw-prose-pre-code": "#24292f",
|
||||||
|
"--tw-prose-pre-bg": "#f6f8fa",
|
||||||
|
"--tw-prose-th-borders": "#d0d7de",
|
||||||
|
"--tw-prose-td-borders": "#d0d7de",
|
||||||
|
|
||||||
|
// Invert colors for dark mode
|
||||||
|
"--tw-prose-invert-body": "#c9d1d9",
|
||||||
|
"--tw-prose-invert-headings": "#c9d1d9",
|
||||||
|
"--tw-prose-invert-lead": "#8b949e",
|
||||||
|
"--tw-prose-invert-links": "#58a6ff",
|
||||||
|
"--tw-prose-invert-bold": "#c9d1d9",
|
||||||
|
"--tw-prose-invert-counters": "#8b949e",
|
||||||
|
"--tw-prose-invert-bullets": "#30363d",
|
||||||
|
"--tw-prose-invert-hr": "#21262d",
|
||||||
|
"--tw-prose-invert-quotes": "#8b949e",
|
||||||
|
"--tw-prose-invert-quote-borders": "#30363d",
|
||||||
|
"--tw-prose-invert-captions": "#8b949e",
|
||||||
|
"--tw-prose-invert-code": "#c9d1d9",
|
||||||
|
"--tw-prose-invert-pre-code": "#c9d1d9",
|
||||||
|
"--tw-prose-invert-pre-bg": "#161b22",
|
||||||
|
"--tw-prose-invert-th-borders": "#30363d",
|
||||||
|
"--tw-prose-invert-td-borders": "#30363d",
|
||||||
|
|
||||||
|
// Adjust margins and sizes (Standardizing to GitHub Markdown / Modern defaults)
|
||||||
|
maxWidth: "none",
|
||||||
|
lineHeight: "1.4",
|
||||||
|
p: {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
},
|
||||||
|
"h1, h2, h3, h4, h5, h6": {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
fontWeight: "600",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontSize: "2em",
|
||||||
|
paddingBottom: "0.2em",
|
||||||
|
borderBottomWidth: "1px",
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: "1.5em",
|
||||||
|
paddingBottom: "0.2em",
|
||||||
|
borderBottomWidth: "1px",
|
||||||
|
},
|
||||||
|
h3: { fontSize: "1.25em" },
|
||||||
|
h4: { fontSize: "1em" },
|
||||||
|
h5: { fontSize: "0.875em" },
|
||||||
|
h6: { fontSize: "0.85em", color: "var(--tw-prose-lead)" },
|
||||||
|
"ul, ol": {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
paddingLeft: "1.5em",
|
||||||
|
},
|
||||||
|
li: {
|
||||||
|
marginTop: "0.15em",
|
||||||
|
marginBottom: "0.15em",
|
||||||
|
},
|
||||||
|
"li > p": {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0",
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
paddingLeft: "1em",
|
||||||
|
fontStyle: "normal",
|
||||||
|
borderLeftWidth: "4px",
|
||||||
|
},
|
||||||
|
pre: {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
padding: "0.75em",
|
||||||
|
borderRadius: "6px",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
backgroundColor: "rgba(175, 184, 193, 0.2)",
|
||||||
|
padding: "0.2em 0.4em",
|
||||||
|
borderRadius: "6px",
|
||||||
|
fontWeight: "inherit",
|
||||||
|
},
|
||||||
|
"code::before": { content: '""' },
|
||||||
|
"code::after": { content: '""' },
|
||||||
|
"pre code": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
padding: "0",
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.65em",
|
||||||
|
},
|
||||||
|
"thead th": {
|
||||||
|
padding: "0.4em 0.75em",
|
||||||
|
borderWidth: "1px",
|
||||||
|
},
|
||||||
|
"tbody td": {
|
||||||
|
padding: "0.4em 0.75em",
|
||||||
|
borderWidth: "1px",
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
height: "0.25em",
|
||||||
|
borderWidth: "0",
|
||||||
|
backgroundColor: "var(--tw-prose-hr)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("@tailwindcss/typography")],
|
||||||
}
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user