conflict-nuxt-4/app/components/auto-import/TiptapEditor2.vue
2026-02-12 11:24:27 +03:30

1601 lines
37 KiB
Vue
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>