1601 lines
37 KiB
Vue
Executable File
1601 lines
37 KiB
Vue
Executable File
<template>
|
||
<div class="editor-shell">
|
||
<!-- نوار ابزار ثابت در بالای صفحه -->
|
||
<div v-if="editor" class="menu-bar-sticky">
|
||
<div class="menu-bar">
|
||
<!-- گروههای دکمههای نوار ابزار -->
|
||
<div
|
||
v-for="(group, groupIndex) in toolbarSchema"
|
||
:key="groupIndex"
|
||
class="menu-group"
|
||
>
|
||
<button
|
||
v-for="(btn, btnIndex) in group.buttons"
|
||
:key="btnIndex"
|
||
@click="handleToolbarButtonClick(btn)"
|
||
:class="getToolbarButtonClass(btn)"
|
||
class="menu-btn"
|
||
:title="btn.title"
|
||
:disabled="btn.disabled && getButtonDisabledState(btn)"
|
||
>
|
||
<span :class="btn.class">{{ btn.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- وضعیت ذخیره -->
|
||
<div class="save-status">
|
||
<span v-if="isSaving" class="saving">
|
||
<span class="dot-pulse"></span>
|
||
در حال ذخیره...
|
||
</span>
|
||
<span v-else class="saved">
|
||
<span class="check-icon">✓</span>
|
||
ذخیره شد
|
||
<span class="time">{{ lastSavedTime }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- محتوای ویرایشگر با Context Menu -->
|
||
<UContextMenu
|
||
:items="contextMenuItems"
|
||
:ui="{
|
||
content: 'w-64',
|
||
itemTrailingIcon: 'hidden',
|
||
}"
|
||
dir="rtl"
|
||
@select="handleContextMenuSelect"
|
||
>
|
||
<!-- Custom slot برای آیکون چپ چرخان -->
|
||
<template #item-trailing="{ item }">
|
||
<UIcon
|
||
v-if="item.children"
|
||
name="i-lucide-chevron-left"
|
||
class="shrink-0 size-5 ms-auto"
|
||
:class="{
|
||
'rotate-180': !$dir === 'rtl',
|
||
}"
|
||
/>
|
||
<div v-else-if="item.kbds" class="flex gap-1 ms-auto">
|
||
<UKbd v-for="(kbd, index) in item.kbds" :key="index" :value="kbd" />
|
||
</div>
|
||
</template>
|
||
<div class="editor-content-wrapper" @mouseup="handleTextSelection">
|
||
<EditorContent :editor="editor" />
|
||
|
||
<!-- آیکونهای بلوک (شبیه Notion) -->
|
||
<div
|
||
v-for="(block, index) in blocks"
|
||
:key="block.id"
|
||
class="block-icon-wrapper"
|
||
:style="getBlockIconStyle(block, index)"
|
||
@click="showBlockMenuAction(block, $event)"
|
||
>
|
||
<div class="block-icon">
|
||
{{ getBlockIcon(block.type) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UContextMenu>
|
||
|
||
<!-- منوی بلوک -->
|
||
<BlockMenu
|
||
v-if="showBlockMenu"
|
||
:visible="showBlockMenu"
|
||
:position="blockMenuPosition"
|
||
:block-type="currentBlockType"
|
||
:block-data="currentBlockData"
|
||
@close="showBlockMenu = false"
|
||
@action="handleBlockMenuAction"
|
||
/>
|
||
|
||
<!-- مودال برای لینک -->
|
||
<div v-if="showLinkModal" class="modal-overlay" @click="closeLinkModal">
|
||
<div class="modal" @click.stop>
|
||
<div class="modal-header">
|
||
<h3>افزودن لینک</h3>
|
||
<button @click="closeLinkModal" class="close-btn">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input
|
||
v-model="linkUrl"
|
||
type="url"
|
||
placeholder="https://example.com"
|
||
class="link-input"
|
||
@keyup.enter="setLink"
|
||
ref="linkInput"
|
||
/>
|
||
<div class="link-example">مثال: https://google.com</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button @click="setLink" class="btn-primary">افزودن لینک</button>
|
||
<button
|
||
v-if="editor && editor.isActive('link')"
|
||
@click="unsetLink"
|
||
class="btn-danger"
|
||
>
|
||
حذف لینک
|
||
</button>
|
||
<button @click="closeLinkModal" class="btn-secondary">انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- وضعیت پردازش AI -->
|
||
<div v-if="isAIProcessing" class="ai-processing-overlay">
|
||
<div class="ai-processing">
|
||
<div class="ai-spinner"></div>
|
||
<div class="ai-text">در حال پردازش توسط هوش مصنوعی...</div>
|
||
<div class="ai-progress"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
||
import StarterKit from "@tiptap/starter-kit";
|
||
import Placeholder from "@tiptap/extension-placeholder";
|
||
import Link from "@tiptap/extension-link";
|
||
import TaskList from "@tiptap/extension-task-list";
|
||
import TaskItem from "@tiptap/extension-task-item";
|
||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||
import TextAlign from "@tiptap/extension-text-align";
|
||
import { usePageStore } from "@/stores/page";
|
||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||
import EditorSchema from "@/json/EditorSchema.json";
|
||
|
||
const pageStore = usePageStore();
|
||
|
||
// وضعیتها
|
||
const showLinkModal = ref(false);
|
||
const linkUrl = ref("");
|
||
const isSaving = ref(false);
|
||
const lastSavedTime = ref("");
|
||
const linkInput = ref(null);
|
||
|
||
// منوها
|
||
const showBlockMenu = ref(false);
|
||
const blockMenuPosition = ref({ x: 0, y: 0 });
|
||
const selectedText = ref("");
|
||
const currentBlockType = ref("paragraph");
|
||
const currentBlockData = ref({});
|
||
const isAIProcessing = ref(false);
|
||
|
||
// دادههای Schema
|
||
const toolbarSchema = EditorSchema.toolbarButtons;
|
||
const contextMenuSchema = EditorSchema.contextMenuItems;
|
||
const blockIcons = EditorSchema.blockIcons;
|
||
const blockTypeNames = EditorSchema.blockTypeNames;
|
||
const alignmentNames = EditorSchema.alignmentNames;
|
||
const editorConfig = EditorSchema.editorConfig;
|
||
|
||
// Context Menu Items برای Nuxt UI
|
||
const contextMenuItems = computed(() => {
|
||
return contextMenuSchema.map((section) => {
|
||
const sectionItems = [];
|
||
|
||
section.items.forEach((row) => {
|
||
const rowItems = row.map((item) => {
|
||
return {
|
||
label: item.label,
|
||
icon: item.icon,
|
||
kbds: item.kbds,
|
||
color: item.color,
|
||
onSelect: (e) => handleContextMenuItemClick(item.action, e),
|
||
};
|
||
});
|
||
sectionItems.push(rowItems);
|
||
});
|
||
|
||
return [
|
||
{
|
||
label: section.label,
|
||
icon: section.icon,
|
||
children: sectionItems,
|
||
},
|
||
];
|
||
});
|
||
});
|
||
|
||
// ==================== توابع مدیریت نوار ابزار ====================
|
||
|
||
// مدیریت کلیک دکمههای نوار ابزار
|
||
const handleToolbarButtonClick = (btn) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (btn.action) {
|
||
case "toggleHeading":
|
||
editor.value.chain().focus().toggleHeading({ level: btn.level }).run();
|
||
break;
|
||
case "toggleBold":
|
||
editor.value.chain().focus().toggleBold().run();
|
||
break;
|
||
case "toggleItalic":
|
||
editor.value.chain().focus().toggleItalic().run();
|
||
break;
|
||
case "toggleLink":
|
||
toggleLink();
|
||
break;
|
||
case "toggleBulletList":
|
||
editor.value.chain().focus().toggleBulletList().run();
|
||
break;
|
||
case "toggleOrderedList":
|
||
editor.value.chain().focus().toggleOrderedList().run();
|
||
break;
|
||
case "toggleTaskList":
|
||
editor.value.chain().focus().toggleTaskList().run();
|
||
break;
|
||
case "toggleCodeBlock":
|
||
editor.value.chain().focus().toggleCodeBlock().run();
|
||
break;
|
||
case "toggleBlockquote":
|
||
editor.value.chain().focus().toggleBlockquote().run();
|
||
break;
|
||
case "setHorizontalRule":
|
||
editor.value.chain().focus().setHorizontalRule().run();
|
||
break;
|
||
case "setTextAlign":
|
||
editor.value.chain().focus().setTextAlign(btn.value).run();
|
||
break;
|
||
case "undo":
|
||
editor.value.chain().focus().undo().run();
|
||
break;
|
||
case "redo":
|
||
editor.value.chain().focus().redo().run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// گرفتن کلاس دکمههای نوار ابزار
|
||
const getToolbarButtonClass = (btn) => {
|
||
const classes = {};
|
||
|
||
if (!editor.value) return classes;
|
||
|
||
switch (btn.action) {
|
||
case "toggleHeading":
|
||
if (btn.level) {
|
||
classes["is-active"] = editor.value.isActive("heading", {
|
||
level: btn.level,
|
||
});
|
||
}
|
||
break;
|
||
case "toggleBold":
|
||
classes["is-active"] = editor.value.isActive("bold");
|
||
break;
|
||
case "toggleItalic":
|
||
classes["is-active"] = editor.value.isActive("italic");
|
||
break;
|
||
case "toggleLink":
|
||
classes["is-active"] = editor.value.isActive("link");
|
||
break;
|
||
case "toggleBulletList":
|
||
classes["is-active"] = editor.value.isActive("bulletList");
|
||
break;
|
||
case "toggleOrderedList":
|
||
classes["is-active"] = editor.value.isActive("orderedList");
|
||
break;
|
||
case "toggleTaskList":
|
||
classes["is-active"] = editor.value.isActive("taskList");
|
||
break;
|
||
case "toggleCodeBlock":
|
||
classes["is-active"] = editor.value.isActive("codeBlock");
|
||
break;
|
||
case "toggleBlockquote":
|
||
classes["is-active"] = editor.value.isActive("blockquote");
|
||
break;
|
||
case "setTextAlign":
|
||
classes["is-active"] = editor.value.isActive({ textAlign: btn.value });
|
||
break;
|
||
}
|
||
|
||
return classes;
|
||
};
|
||
|
||
// بررسی وضعیت disabled بودن دکمه
|
||
const getButtonDisabledState = (btn) => {
|
||
if (!editor.value) return true;
|
||
|
||
switch (btn.action) {
|
||
case "undo":
|
||
return !editor.value.can().undo();
|
||
case "redo":
|
||
return !editor.value.can().redo();
|
||
default:
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// ==================== توابع مدیریت Context Menu ====================
|
||
|
||
// مدیریت کلیک روی آیتمهای Context Menu
|
||
const handleContextMenuItemClick = (action, e) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (action) {
|
||
case "summarize":
|
||
case "improve":
|
||
case "translate":
|
||
case "explain":
|
||
handleAIAction(action, selectedText.value);
|
||
break;
|
||
case "toggleBold":
|
||
editor.value.chain().focus().toggleBold().run();
|
||
break;
|
||
case "toggleItalic":
|
||
editor.value.chain().focus().toggleItalic().run();
|
||
break;
|
||
case "toggleUnderline":
|
||
editor.value.chain().focus().toggleUnderline().run();
|
||
break;
|
||
case "toggleCode":
|
||
editor.value.chain().focus().toggleCode().run();
|
||
break;
|
||
case "toggleLink":
|
||
toggleLink();
|
||
break;
|
||
case "copy":
|
||
navigator.clipboard.writeText(selectedText.value);
|
||
showToast("متن کپی شد");
|
||
break;
|
||
case "cut":
|
||
navigator.clipboard.writeText(selectedText.value);
|
||
editor.value?.chain().focus().deleteSelection().run();
|
||
showToast("متن برش داده شد");
|
||
break;
|
||
case "paste":
|
||
handlePaste();
|
||
break;
|
||
case "delete":
|
||
editor.value?.chain().focus().deleteSelection().run();
|
||
showToast("متن حذف شد");
|
||
break;
|
||
case "convertToHeading1":
|
||
editor.value?.chain().focus().toggleHeading({ level: 1 }).run();
|
||
break;
|
||
case "convertToHeading2":
|
||
editor.value?.chain().focus().toggleHeading({ level: 2 }).run();
|
||
break;
|
||
case "convertToHeading3":
|
||
editor.value?.chain().focus().toggleHeading({ level: 3 }).run();
|
||
break;
|
||
case "convertToBulletList":
|
||
editor.value?.chain().focus().toggleBulletList().run();
|
||
break;
|
||
case "convertToOrderedList":
|
||
editor.value?.chain().focus().toggleOrderedList().run();
|
||
break;
|
||
case "convertToTaskList":
|
||
editor.value?.chain().focus().toggleTaskList().run();
|
||
break;
|
||
case "convertToCodeBlock":
|
||
editor.value?.chain().focus().toggleCodeBlock().run();
|
||
break;
|
||
case "convertToBlockquote":
|
||
editor.value?.chain().focus().toggleBlockquote().run();
|
||
break;
|
||
case "alignRight":
|
||
editor.value?.chain().focus().setTextAlign("right").run();
|
||
break;
|
||
case "alignCenter":
|
||
editor.value?.chain().focus().setTextAlign("center").run();
|
||
break;
|
||
case "alignLeft":
|
||
editor.value?.chain().focus().setTextAlign("left").run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// مدیریت paste
|
||
const handlePaste = async () => {
|
||
try {
|
||
const clipboardText = await navigator.clipboard.readText();
|
||
editor.value?.chain().focus().insertContent(clipboardText).run();
|
||
showToast("متن چسبانده شد");
|
||
} catch (error) {
|
||
console.error("خطا در خواندن کلیپبورد:", error);
|
||
showToast("خطا در چسباندن متن");
|
||
}
|
||
};
|
||
|
||
// مدیریت انتخاب از Context Menu
|
||
const handleContextMenuSelect = (e, item) => {
|
||
console.log("Context menu item selected:", item.label);
|
||
|
||
if (item.onSelect) {
|
||
item.onSelect(e);
|
||
}
|
||
};
|
||
|
||
// ==================== ویرایشگر Tiptap ====================
|
||
|
||
const editor = useEditor({
|
||
extensions: [
|
||
StarterKit,
|
||
Placeholder.configure({
|
||
placeholder: editorConfig.placeholder,
|
||
emptyEditorClass: editorConfig.emptyEditorClass,
|
||
}),
|
||
Link.configure({
|
||
openOnClick: false,
|
||
HTMLAttributes: editorConfig.linkAttributes,
|
||
}),
|
||
TaskList,
|
||
TaskItem.configure({
|
||
nested: true,
|
||
HTMLAttributes: {
|
||
class: "flex items-start gap-2",
|
||
},
|
||
}),
|
||
HorizontalRule.configure({
|
||
HTMLAttributes: {
|
||
class: "my-8",
|
||
},
|
||
}),
|
||
TextAlign.configure({
|
||
types: ["heading", "paragraph"],
|
||
alignments: ["right", "center", "left"],
|
||
defaultAlignment: editorConfig.defaultAlignment,
|
||
}),
|
||
],
|
||
content:
|
||
pageStore.blocks.length > 0
|
||
? pageStore.blocksToEditorContent()
|
||
: EditorSchema.welcomeContent,
|
||
editorProps: {
|
||
attributes: editorConfig.attributes,
|
||
handleKeyDown: (view, event) => {
|
||
// میانبرهای کیبورد
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||
toggleLink();
|
||
event.preventDefault();
|
||
return true;
|
||
}
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
|
||
if (event.shiftKey) {
|
||
editor.commands.redo();
|
||
} else {
|
||
editor.commands.undo();
|
||
}
|
||
event.preventDefault();
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
},
|
||
onUpdate({ editor }) {
|
||
pageStore.updateFromEditor(editor);
|
||
triggerAutoSave();
|
||
},
|
||
onBlur({ editor }) {
|
||
saveToStorage();
|
||
},
|
||
onCreate() {
|
||
updateLastSavedTime();
|
||
},
|
||
});
|
||
|
||
// ==================== توابع مدیریت ====================
|
||
|
||
// انتخاب متن
|
||
const handleTextSelection = () => {
|
||
const selection = window.getSelection();
|
||
selectedText.value = selection.toString().trim();
|
||
};
|
||
|
||
// نمایش منوی بلوک
|
||
const showBlockMenuAction = (block, event) => {
|
||
event.stopPropagation();
|
||
|
||
currentBlockType.value = block.type;
|
||
currentBlockData.value = block.data || {};
|
||
|
||
blockMenuPosition.value = {
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
};
|
||
|
||
showBlockMenu.value = true;
|
||
|
||
console.log("🧱 منوی بلوک باز شد:", {
|
||
type: block.type,
|
||
position: blockMenuPosition.value,
|
||
data: currentBlockData.value,
|
||
});
|
||
};
|
||
|
||
// مدیریت اکشنهای منوی بلوک
|
||
const handleBlockMenuAction = ({ action, value, blockType, blockData }) => {
|
||
console.log("🎯 اکشن منوی بلوک:", action, value, blockType, blockData);
|
||
|
||
switch (action) {
|
||
case "color":
|
||
showToast(`رنگ بلوک به ${value} تغییر کرد`);
|
||
break;
|
||
case "align":
|
||
if (editor.value) {
|
||
editor.value.chain().focus().setTextAlign(value).run();
|
||
}
|
||
showToast(`تراز به ${getAlignmentName(value)} تغییر کرد`);
|
||
break;
|
||
case "convert":
|
||
convertBlockType(value);
|
||
showToast(`بلوک به ${getBlockTypeName(value)} تبدیل شد`);
|
||
break;
|
||
case "duplicate":
|
||
duplicateBlock(blockData);
|
||
showToast("بلوک تکثیر شد");
|
||
break;
|
||
case "comment":
|
||
const comment = prompt("نظر خود را وارد کنید:");
|
||
if (comment) {
|
||
showToast("نظر اضافه شد");
|
||
}
|
||
break;
|
||
case "move-up":
|
||
showToast("بلوک به بالا منتقل شد");
|
||
break;
|
||
case "move-down":
|
||
showToast("بلوک به پایین منتقل شد");
|
||
break;
|
||
case "delete-block":
|
||
if (confirm("آیا مطمئنید که میخواهید این بلوک را حذف کنید؟")) {
|
||
deleteBlock();
|
||
showToast("بلوک حذف شد");
|
||
}
|
||
break;
|
||
case "ai-rewrite":
|
||
case "ai-expand":
|
||
case "ai-simplify":
|
||
const aiAction = action.replace("ai-", "");
|
||
handleAIAction(aiAction, getBlockContent());
|
||
break;
|
||
}
|
||
|
||
showBlockMenu.value = false;
|
||
};
|
||
|
||
// ==================== توابع کمکی ====================
|
||
|
||
const getAlignmentName = (value) => {
|
||
return alignmentNames[value] || value;
|
||
};
|
||
|
||
const getBlockTypeName = (type) => {
|
||
return blockTypeNames[type] || "بلوک";
|
||
};
|
||
|
||
const convertBlockType = (newType) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (newType) {
|
||
case "paragraph":
|
||
editor.value.chain().focus().setParagraph().run();
|
||
break;
|
||
case "heading1":
|
||
editor.value.chain().focus().toggleHeading({ level: 1 }).run();
|
||
break;
|
||
case "heading2":
|
||
editor.value.chain().focus().toggleHeading({ level: 2 }).run();
|
||
break;
|
||
case "heading3":
|
||
editor.value.chain().focus().toggleHeading({ level: 3 }).run();
|
||
break;
|
||
case "todo":
|
||
editor.value.chain().focus().toggleTaskList().run();
|
||
break;
|
||
case "bullet":
|
||
editor.value.chain().focus().toggleBulletList().run();
|
||
break;
|
||
case "number":
|
||
editor.value.chain().focus().toggleOrderedList().run();
|
||
break;
|
||
case "code":
|
||
editor.value.chain().focus().toggleCodeBlock().run();
|
||
break;
|
||
case "quote":
|
||
editor.value.chain().focus().toggleBlockquote().run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
function duplicateBlock(block) {
|
||
const ed = editor.value;
|
||
if (!ed) return;
|
||
|
||
const node = ed.state.doc.nodeAt(block.position);
|
||
if (!node) return;
|
||
|
||
ed.commands.insertContentAt(block.position + node.nodeSize, node.toJSON());
|
||
}
|
||
|
||
const deleteBlock = () => {
|
||
if (editor.value) {
|
||
const { from, to } = editor.value.state.selection;
|
||
editor.value.chain().focus().deleteRange({ from, to }).run();
|
||
}
|
||
};
|
||
|
||
const getBlockContent = () => {
|
||
if (!editor.value) return "";
|
||
const { from, to } = editor.value.state.selection;
|
||
return editor.value.state.doc.textBetween(from, to, " ");
|
||
};
|
||
|
||
// ==================== توابع AI ====================
|
||
|
||
// تابع اتصال به LLM
|
||
const handleAIAction = async (action, text) => {
|
||
if (!text || !text.trim()) {
|
||
showToast("لطفاً متنی را انتخاب کنید");
|
||
return;
|
||
}
|
||
|
||
isAIProcessing.value = true;
|
||
|
||
try {
|
||
// ارسال درخواست به بک
|
||
const response = await $fetch("http://localhost:8000/ai/block", {
|
||
method: "POST",
|
||
body: {
|
||
action,
|
||
block: {
|
||
type: "paragraph",
|
||
content: text,
|
||
attrs: {},
|
||
},
|
||
},
|
||
});
|
||
|
||
console.log("🤖 پاسخ AI:", response);
|
||
|
||
if (response.content && editor.value) {
|
||
// جایگزینی متن سلکت شده با خروجی AI
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteSelection()
|
||
.insertContent(response.content)
|
||
.run();
|
||
|
||
showToast("✅ متن بهبود یافت");
|
||
} else {
|
||
showToast("خطا در پردازش AI");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ خطای AI:", error);
|
||
showToast("خطا در اتصال به AI");
|
||
} finally {
|
||
isAIProcessing.value = false;
|
||
}
|
||
};
|
||
|
||
const applyAIResult = (action, result) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (action) {
|
||
case "summarize":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result summary">
|
||
<h4>📝 خلاصه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "improve":
|
||
case "rewrite":
|
||
const { from, to } = editor.value.state.selection;
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteRange({ from, to })
|
||
.insertContent(result)
|
||
.run();
|
||
break;
|
||
case "translate":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result translation">
|
||
<h4>🌍 ترجمه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "explain":
|
||
case "simplify":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result explanation">
|
||
<h4>💡 توضیح AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "continue":
|
||
case "expand":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result expansion">
|
||
<h4>↪️ ادامه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// ==================== مدیریت بلوکهای قابل مشاهده ====================
|
||
|
||
const blocks = computed(() => {
|
||
if (!editor.value) return [];
|
||
|
||
const list = [];
|
||
|
||
editor.value.state.doc.forEach((node, pos) => {
|
||
list.push({
|
||
id: `${pos}-${node.type.name}`,
|
||
type: node.type.name,
|
||
position: pos,
|
||
content: node.textContent,
|
||
attrs: node.attrs || {},
|
||
});
|
||
});
|
||
|
||
return list;
|
||
});
|
||
|
||
const getBlockIconStyle = (block, index) => ({
|
||
top: `${index * 48 + 20}px`,
|
||
left: `-40px`,
|
||
});
|
||
|
||
const getBlockIcon = (type) => {
|
||
return blockIcons[type] || "📝";
|
||
};
|
||
|
||
// ==================== توابع ذخیره و لینک ====================
|
||
|
||
let saveTimeout = null;
|
||
const triggerAutoSave = () => {
|
||
isSaving.value = true;
|
||
if (saveTimeout) clearTimeout(saveTimeout);
|
||
saveTimeout = setTimeout(() => {
|
||
saveToStorage();
|
||
}, 1000);
|
||
};
|
||
|
||
const saveToStorage = () => {
|
||
pageStore.saveToLocalStorage();
|
||
isSaving.value = false;
|
||
updateLastSavedTime();
|
||
};
|
||
|
||
const updateLastSavedTime = () => {
|
||
const now = new Date();
|
||
lastSavedTime.value = now.toLocaleTimeString("fa-IR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
};
|
||
|
||
// مدیریت لینک
|
||
const toggleLink = () => {
|
||
if (editor.isActive("link")) {
|
||
linkUrl.value = editor.getAttributes("link").href || "";
|
||
showLinkModal.value = true;
|
||
nextTick(() => {
|
||
if (linkInput.value) linkInput.value.focus();
|
||
});
|
||
} else {
|
||
showLinkModal.value = true;
|
||
linkUrl.value = "";
|
||
nextTick(() => {
|
||
if (linkInput.value) linkInput.value.focus();
|
||
});
|
||
}
|
||
};
|
||
|
||
const setLink = () => {
|
||
if (linkUrl.value) {
|
||
let url = linkUrl.value;
|
||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||
url = "https://" + url;
|
||
}
|
||
|
||
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
||
}
|
||
closeLinkModal();
|
||
};
|
||
|
||
const unsetLink = () => {
|
||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||
closeLinkModal();
|
||
};
|
||
|
||
const closeLinkModal = () => {
|
||
showLinkModal.value = false;
|
||
linkUrl.value = "";
|
||
};
|
||
|
||
// نمایش toast
|
||
const showToast = (message) => {
|
||
const toast = document.createElement("div");
|
||
toast.className = "toast-message";
|
||
toast.textContent = message;
|
||
toast.style.cssText = `
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: #10b981;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
z-index: 10000;
|
||
animation: toastIn 0.3s ease-out;
|
||
`;
|
||
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.animation = "toastOut 0.3s ease-out forwards";
|
||
setTimeout(() => {
|
||
document.body.removeChild(toast);
|
||
}, 300);
|
||
}, 3000);
|
||
};
|
||
|
||
// میانبر Ctrl+K برای لینک
|
||
const handleKeyDown = (event) => {
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||
event.preventDefault();
|
||
toggleLink();
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
updateLastSavedTime();
|
||
pageStore.loadFromLocalStorage();
|
||
|
||
// اضافه کردن استایل toast و animation
|
||
const style = document.createElement("style");
|
||
style.textContent = `
|
||
@keyframes toastIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes toastOut {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (saveTimeout) clearTimeout(saveTimeout);
|
||
document.removeEventListener("keydown", handleKeyDown);
|
||
saveToStorage();
|
||
|
||
if (editor) {
|
||
editor.destroy();
|
||
}
|
||
});
|
||
</script>
|
||
<style scoped>
|
||
.editor-shell {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 1rem;
|
||
position: relative;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* نوار ابزار ثابت */
|
||
.menu-bar-sticky {
|
||
position: sticky;
|
||
top: 0;
|
||
background: white;
|
||
z-index: 50;
|
||
padding: 1rem 0;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
margin-bottom: 2rem;
|
||
backdrop-filter: blur(8px);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.menu-bar {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.menu-group {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
padding: 0 0.5rem;
|
||
border-right: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.menu-group:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.menu-btn {
|
||
padding: 0.5rem 0.75rem;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
font-size: 0.875rem;
|
||
transition: all 0.2s;
|
||
min-width: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.menu-btn:hover {
|
||
background: #f9fafb;
|
||
border-color: #9ca3af;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.menu-btn.is-active {
|
||
background: #3b82f6;
|
||
color: white;
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.menu-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
/* وضعیت ذخیره */
|
||
.save-status {
|
||
text-align: center;
|
||
margin-top: 0.5rem;
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.saving {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.saved {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.check-icon {
|
||
color: #10b981;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.dot-pulse {
|
||
position: relative;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background-color: #3b82f6;
|
||
animation: dot-pulse 1.5s infinite linear;
|
||
}
|
||
|
||
@keyframes dot-pulse {
|
||
0%,
|
||
60%,
|
||
100% {
|
||
opacity: 0.3;
|
||
transform: scale(0.8);
|
||
}
|
||
30% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.time {
|
||
font-size: 0.7rem;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* محتوای ویرایشگر */
|
||
.editor-content-wrapper {
|
||
position: relative;
|
||
min-height: 60vh;
|
||
margin-bottom: 80px;
|
||
}
|
||
|
||
/* آیکون بلوک */
|
||
.block-icon-wrapper {
|
||
position: absolute;
|
||
left: -40px;
|
||
cursor: pointer;
|
||
opacity: 0.3;
|
||
transition: opacity 0.2s;
|
||
z-index: 10;
|
||
}
|
||
|
||
.block-icon-wrapper:hover {
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
.block-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.block-icon:hover {
|
||
background: #f3f4f6;
|
||
border-color: #9ca3af;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* دکمههای تست */
|
||
.test-controls {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.test-btn {
|
||
padding: 12px 20px;
|
||
background: #4f46e5;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.test-btn:hover {
|
||
background: #4338ca;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(79, 70, 229, 0.4);
|
||
}
|
||
|
||
.test-btn.ai-test {
|
||
background: linear-gradient(135deg, #8b5cf6, #ec4899);
|
||
}
|
||
|
||
.test-btn.ai-test:hover {
|
||
background: linear-gradient(135deg, #7c3aed, #db2777);
|
||
}
|
||
|
||
/* وضعیت پردازش AI */
|
||
.ai-processing-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10001;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.ai-processing {
|
||
background: white;
|
||
padding: 32px;
|
||
border-radius: 16px;
|
||
text-align: center;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
min-width: 300px;
|
||
}
|
||
|
||
.ai-spinner {
|
||
width: 48px;
|
||
height: 48px;
|
||
border: 3px solid #e0e7ff;
|
||
border-top-color: #8b5cf6;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.ai-text {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #4f46e5;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.ai-progress {
|
||
height: 4px;
|
||
background: #e0e7ff;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ai-progress::after {
|
||
content: "";
|
||
display: block;
|
||
height: 100%;
|
||
width: 60%;
|
||
background: linear-gradient(90deg, #8b5cf6, #ec4899);
|
||
animation: progress 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes progress {
|
||
0%,
|
||
100% {
|
||
transform: translateX(-100%);
|
||
}
|
||
50% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
|
||
/* مودال لینک */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal {
|
||
background: white;
|
||
border-radius: 0.75rem;
|
||
width: 450px;
|
||
max-width: 90%;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
animation: modal-appear 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes modal-appear {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px) scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1.25rem 1.5rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
color: #6b7280;
|
||
padding: 0.25rem;
|
||
border-radius: 0.25rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.link-input {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 0.5rem;
|
||
font-size: 0.875rem;
|
||
direction: ltr;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.link-input:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
.link-example {
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
margin-top: 0.5rem;
|
||
text-align: right;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
padding: 1.25rem 1.5rem;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.btn-primary {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #2563eb;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
.btn-danger {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #ef4444;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #dc2626;
|
||
}
|
||
|
||
/* استایلهای ویرایشگر */
|
||
:deep(.notion-editor) {
|
||
outline: none;
|
||
min-height: 60vh;
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
:deep(.notion-editor p) {
|
||
margin: 1rem 0;
|
||
line-height: 1.8;
|
||
color: #374151;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
:deep(.notion-editor h1) {
|
||
font-size: 2.25rem;
|
||
font-weight: 800;
|
||
margin: 2rem 0 1rem;
|
||
color: #111827;
|
||
border-bottom: 3px solid #f3f4f6;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h2) {
|
||
font-size: 1.875rem;
|
||
font-weight: 700;
|
||
margin: 1.75rem 0 0.75rem;
|
||
color: #1f2937;
|
||
}
|
||
|
||
:deep(.notion-editor h3) {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
margin: 1.5rem 0 0.5rem;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.notion-editor ul),
|
||
:deep(.notion-editor ol) {
|
||
padding-right: 1.75rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor li) {
|
||
margin: 0.5rem 0;
|
||
padding-right: 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor .task-list) {
|
||
padding-right: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
:deep(.notion-editor .task-item) {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 0.25rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor .task-item input[type="checkbox"]) {
|
||
margin-top: 0.35rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
:deep(.notion-editor code) {
|
||
background: #f3f4f6;
|
||
padding: 0.2rem 0.4rem;
|
||
border-radius: 0.25rem;
|
||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||
font-size: 0.875rem;
|
||
color: #dc2626;
|
||
}
|
||
|
||
:deep(.notion-editor pre) {
|
||
background: #1f2937;
|
||
color: #f9fafb;
|
||
padding: 1.25rem;
|
||
border-radius: 0.5rem;
|
||
overflow-x: auto;
|
||
direction: ltr;
|
||
text-align: left;
|
||
margin: 1.5rem 0;
|
||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||
font-size: 0.875rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.notion-editor blockquote) {
|
||
border-right: 4px solid #3b82f6;
|
||
padding-right: 1.5rem;
|
||
margin: 1.5rem 0;
|
||
color: #4b5563;
|
||
font-style: italic;
|
||
background: #f9fafb;
|
||
padding: 1.25rem;
|
||
border-radius: 0 0.75rem 0.75rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor hr) {
|
||
border: none;
|
||
border-top: 2px solid #e5e7eb;
|
||
margin: 2rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor a) {
|
||
color: #3b82f6;
|
||
text-decoration: none;
|
||
border-bottom: 1px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
:deep(.notion-editor a:hover) {
|
||
color: #2563eb;
|
||
border-bottom-color: #2563eb;
|
||
}
|
||
|
||
/* نتایج AI */
|
||
:deep(.ai-result) {
|
||
background: #f0f9ff;
|
||
border: 1px solid #0ea5e9;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin: 16px 0;
|
||
position: relative;
|
||
}
|
||
|
||
:deep(.ai-result h4) {
|
||
color: #0369a1;
|
||
margin-top: 0;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.ai-result p) {
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* ترازبندی متن */
|
||
:deep(.notion-editor [data-text-align="right"]) {
|
||
text-align: right;
|
||
}
|
||
|
||
:deep(.notion-editor [data-text-align="center"]) {
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.notion-editor [data-text-align="left"]) {
|
||
text-align: left;
|
||
}
|
||
|
||
:deep(.ProseMirror-focused) {
|
||
outline: none;
|
||
}
|
||
|
||
:deep(.is-editor-empty:first-child::before) {
|
||
content: attr(data-placeholder);
|
||
float: right;
|
||
color: #9ca3af;
|
||
pointer-events: none;
|
||
height: 0;
|
||
font-style: italic;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* ریسپانسیو */
|
||
@media (max-width: 768px) {
|
||
.editor-shell {
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.menu-bar-sticky {
|
||
position: relative;
|
||
top: 0;
|
||
margin-bottom: 1rem;
|
||
padding: 0.75rem 0;
|
||
}
|
||
|
||
.menu-bar {
|
||
gap: 0.25rem;
|
||
padding: 0.25rem;
|
||
}
|
||
|
||
.menu-group {
|
||
padding: 0 0.25rem;
|
||
border-right: none;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
padding-bottom: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.menu-group:last-child {
|
||
border-bottom: none;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.menu-btn {
|
||
padding: 0.375rem 0.5rem;
|
||
min-width: 36px;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.modal {
|
||
width: 95%;
|
||
}
|
||
|
||
.modal-footer {
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.notion-editor) {
|
||
padding: 0 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h1) {
|
||
font-size: 1.875rem;
|
||
}
|
||
|
||
:deep(.notion-editor h2) {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h3) {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.block-icon-wrapper {
|
||
left: -30px;
|
||
}
|
||
|
||
.test-controls {
|
||
left: 10px;
|
||
bottom: 10px;
|
||
}
|
||
|
||
.test-btn {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style>
|