محرر نص منسق بنمط Notion - متطلبات التنفيذ
هذا المستند موجود بجانب الكود ويجب أن يظل محدثًا كلما تغير تنفيذ المحرر.
بيان المشكلة
- يلتف المكون
<MDXEditorComponent>الحالي حول @mdxeditor/editor مما يمنحنا تحرير Markdown الكلاسيكي. - يرغب المؤلفون في تجربة شبيهة بـ Notion، قائمة على الكتل: أوامر الشرطة المائلة، سحب الكتل لإعادة ترتيبها، قوائم المهام، الجداول، التضمينات، إلخ.
- يجب أن يتكامل البديل بسلاسة مع محملات/إجراءات React Router، ويستمر في الحفظ التلقائي للمسودات، ويعمل في كل من تدفقات الإنشاء الجديد والتحرير.
نظرة عامة على الحل
- استبدال MDXEditor بـ TipTap + ProseMirror – محرر مفتوح المصدر، بدون واجهة، قابل للتوسيع، يستخدمه Craft و GitLab و ChatGPT وغيرهم.
- الحفاظ على مكون غلاف رفيع (
NotionEditor.tsx) يخفي تفاصيل TipTap ويعرض واجهة برمجة تطبيقات مألوفة (value,onChange). - الاحتفاظ بـ JSON أو Markdown (مصفوفة القرار أدناه) في عمود
blog_posts.contentالحالي؛ تساعد أدوات التحويل في الحفاظ على التوافق مع الإصدارات السابقة.
الحزم المراد إضافتها
pnpm --filter web add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder \
@tiptap/extension-link @tiptap/extension-task-list @tiptap/extension-task-item \
@tiptap/extension-underline @tiptap/extension-table @tiptap/extension-table-row \
@tiptap/extension-table-cell @tiptap/extension-table-header \
@tiptap/extension-code-block-lowlight lowlight classnamesإضافات اختيارية (عمل مستقبلي): @hocuspocus/provider للمزامنة في الوقت الفعلي، tiptap-extension-command لقائمة الشرطة المائلة، tiptap-extension-placeholder للذكاء الاصطناعي.
تخطيط الملفات
apps/web/app/components/rich-text/
NotionEditor.tsx <-- غلاف TipTap قابل لإعادة الاستخدام (export default)
EditorBubbleMenu.tsx <-- شريط أدوات التنسيق المضمن
EditorSlashMenu.tsx <-- قائمة إدراج الكتل
editor-extensions.ts <-- مصنع يعيد الامتدادات الشائعة
serializer.ts <-- مساعدات JSON ⇆ Markdown (showdown / remark)ملاحظة: احتفظ بالمجلد
mdx-editor/الحالي حتى اكتمال الترحيل؛ ثم قم بإيقافه.
قائمة التحقق من التنفيذ
-
إنشاء مكون المحرر ✅
- استخدام
useEditorمن@tiptap/react. - تحميل
StarterKit+ الامتدادات الإضافية (المعرفة فيeditor-extensions.ts). - عرض الخصائص:
defaultContent,readOnly,onUpdate(يعيد كلاً من JSON و Markdown). - تطبيق فئات Tailwind لمظهر Notion (
prose,focus:outline-none, إلخ).
- استخدام
-
BubbleMenu & SlashMenu ✅
- BubbleMenu: غامق، مائل، تسطير، رابط.
- SlashMenu:
/h1,/h2,/table,/code,/todo,/image. - استخدام Headless UI / Radix
Commandلقائمة سهولة الوصول (a11y).
-
تكامل الحفظ التلقائي ✅
- في
triggerAutosave()استبدلconst currentContent = editor.getJSON(); - إرسال كـ
JSON.stringify(currentContent).
- في
-
تغييرات المسار (
posts/new&posts/[id]) ✅- تبديل
<MDXEditorComponent>←<NotionEditor>. - يحتوي
<input name="content" />المخفي الآن على الحمولة المسلسلة. - الحفاظ على منطق Sheet الحالي والحوارات والحفظ التلقائي.
- تبديل
-
قرار قاعدة البيانات ✅
| الخيار | الإيجابيات | السلبيات |
|---|---|---|
عمود text، تخزين Markdown | لا تغيير في المخطط | فقدان ثراء الكتل (الجداول، التضمينات) |
عمود jsonb، تخزين TipTap JSON | دقة كاملة، مقاومة للمستقبل | يتطلب ترحيل وتسلسل عند القراءة للمشاركات القديمة |
الخطة الحالية: الترحيل إلى jsonb ولكن شحن محول قراءة/كتابة Markdown لتجنب كسر المدونة العامة على الفور.
-
نص الترحيل البرمجي ✅
- ترحيل Drizzle بإضافة عمود
content_json jsonbجديد (قابل للإلغاء). - التعبئة الخلفية:
content_json = markdownToTiptapJSON(content). - في النهاية، إسقاط عمود
contentالقديم.
- ترحيل Drizzle بإضافة عمود
-
اعتبارات SSR والبناء
- يستخدم TipTap
window؛ قم بتغليف الخطاف في حارسuseClientأو استيراد كسول داخلuseEffectلمسارات SSR. - الاستمرار في التحميل الكسول للحفاظ على حجم الحزمة صغيرًا.
- يستخدم TipTap
-
إمكانية الوصول واختصارات لوحة المفاتيح
Cmd/Ctrl + b/i/uللعلامات.Cmd/Ctrl + s→ يفتح مربع حوار حفظ المسودة (موجود بالفعل).Cmd/Ctrl + Shift + p→ نشر.
-
الاختبار وضمان الجودة
- اختبار وحدة لمصنع الامتدادات – يعيد القائمة المتوقعة.
- Cypress e2e: إنشاء مشاركة، إضافة كتل، حفظ تلقائي، تحديث – المسودة تستمر.
- الانحدار البصري باستخدام Storybook (اختياري).
-
التحسينات المستقبلية
- مؤشرات تعاونية مع Y.js.
- وضع التعليق والاقتراح.
- أمر شريط مائل للذكاء الاصطناعي لتوليد المحتوى.
- معاينات التضمين (Twitter، Loom، Figma، ...).
مقتطف مرجعي – NotionEditor.tsx
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import Link from "@tiptap/extension-link";
export function NotionEditor({ defaultContent, onUpdate }) {
const editor = useEditor({
extensions: [
StarterKit.configure({ codeBlock: false }),
Placeholder.configure({ placeholder: "اكتب / للكتل" }),
TaskList,
TaskItem,
Link.configure({ openOnClick: false })
],
content: defaultContent,
onUpdate: ({ editor }) => {
onUpdate?.({
json: editor.getJSON(),
markdown: editor.getHTML() // أو مُسلسل مخصص
});
}
});
return (
<div className="prose leading-relaxed max-w-none border rounded-md p-4 focus:outline-none">
<EditorContent editor={editor} />
</div>
);
}الجدول الزمني للترحيل
| الأسبوع | المهمة |
|---|---|
| 1 | بناء مكون المحرر والامتدادات الأساسية |
| 2 | التكامل في مسار المشاركة الجديدة، الحفاظ على MDXEditor جنبًا إلى جنب كخيار احتياطي |
| 3 | تحديث صفحة التحرير، تمكين تحويل Markdown→JSON |
| 4 | ترحيل قاعدة البيانات، التعبئة الخلفية، إزالة كود المحرر القديم |
المشرف: @samishukri • آخر تحديث: ٢٣/١٠/٢٠٢٦