Compare commits

..

2 Commits

Author SHA1 Message Date
parsa aghaei
5371f1ba22 feat: complete rich text editor toolbar with full states, hex pickers, and clean layout
All checks were successful
Deploy Canina / deploy (push) Successful in 1m36s
2026-08-24 15:08:57 +03:30
parsa aghaei
ad0d98f846 feat: enhance RichTextEditor with color/bg pickers, active states, and code toggle; fix hero subtitle typography
Some checks failed
Deploy Canina / deploy (push) Has been cancelled
2026-08-24 15:08:12 +03:30
2 changed files with 427 additions and 71 deletions

View File

@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
import { import {
Bold, Bold,
Italic, Italic,
@ -8,13 +8,16 @@ import {
Heading3, Heading3,
List, List,
ListOrdered, ListOrdered,
Link, Link as LinkIcon,
AlignRight, AlignRight,
AlignCenter, AlignCenter,
AlignLeft, AlignLeft,
Palette, Palette,
PaintBucket,
Undo, Undo,
Redo, Redo,
Code,
RemoveFormatting,
} from 'lucide-react'; } from 'lucide-react';
interface RichTextEditorProps { interface RichTextEditorProps {
@ -24,7 +27,7 @@ interface RichTextEditorProps {
className?: string; className?: string;
} }
const COLORS = [ const PRESET_COLORS = [
{ label: 'سیاه / پیش‌فرض', value: '#111827' }, { label: 'سیاه / پیش‌فرض', value: '#111827' },
{ label: 'خاکستری', value: '#4b5563' }, { label: 'خاکستری', value: '#4b5563' },
{ label: 'آبی کنینا', value: '#1d4ed8' }, { label: 'آبی کنینا', value: '#1d4ed8' },
@ -34,6 +37,29 @@ const COLORS = [
{ label: 'کهربایی / زرد', value: '#d97706' }, { label: 'کهربایی / زرد', value: '#d97706' },
]; ];
const PRESET_BG_COLORS = [
{ label: 'بی‌رنگ / حذف پس‌زمینه', value: 'transparent' },
{ label: 'هایلایت زرد', value: '#fef08a' },
{ label: 'هایلایت آبی', value: '#dbeafe' },
{ label: 'هایلایت بنفش', value: '#f3e8ff' },
{ label: 'هایلایت سبز', value: '#dcfce7' },
{ label: 'هایلایت قرمز', value: '#fee2e2' },
{ label: 'خاکستری ملایم', value: '#f3f4f6' },
];
interface ActiveStates {
isBold: boolean;
isItalic: boolean;
isUnderline: boolean;
isH1: boolean;
isH2: boolean;
isH3: boolean;
isP: boolean;
isUL: boolean;
isOL: boolean;
align: 'right' | 'center' | 'left' | 'justify';
}
export default function RichTextEditor({ export default function RichTextEditor({
value, value,
onChange, onChange,
@ -43,8 +69,81 @@ export default function RichTextEditor({
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(null);
const isTypingRef = useRef(false); const isTypingRef = useRef(false);
const isMountedRef = useRef(false); const isMountedRef = useRef(false);
const [isCodeMode, setIsCodeMode] = useState(false);
const [showColorPicker, setShowColorPicker] = useState(false);
const [showBgColorPicker, setShowBgColorPicker] = useState(false);
const [customColor, setCustomColor] = useState('#111827');
const [customBgColor, setCustomBgColor] = useState('#fef08a');
// Sync value from props on initial mount or when external change occurs (not while active) const [activeStates, setActiveStates] = useState<ActiveStates>({
isBold: false,
isItalic: false,
isUnderline: false,
isH1: false,
isH2: false,
isH3: false,
isP: false,
isUL: false,
isOL: false,
align: 'right',
});
// Query formatting states on cursor/selection change
const updateActiveStates = useCallback(() => {
if (!editorRef.current || isCodeMode) return;
try {
const doc = document as any;
const isBold = doc.queryCommandState ? doc.queryCommandState('bold') : false;
const isItalic = doc.queryCommandState ? doc.queryCommandState('italic') : false;
const isUnderline = doc.queryCommandState ? doc.queryCommandState('underline') : false;
const isUL = doc.queryCommandState ? doc.queryCommandState('insertUnorderedList') : false;
const isOL = doc.queryCommandState ? doc.queryCommandState('insertOrderedList') : false;
let block = (doc.queryCommandValue ? doc.queryCommandValue('formatBlock') : '') || '';
block = block.toLowerCase().replace(/[<>]/g, '');
// Check parent tag if queryCommandValue is ambiguous
const selection = window.getSelection();
let parentTag = '';
if (selection && selection.rangeCount > 0) {
let node: Node | null = selection.getRangeAt(0).commonAncestorContainer;
if (node && node.nodeType === 3 && node.parentNode) {
node = node.parentNode;
}
while (node && node !== editorRef.current) {
const tag = (node as HTMLElement).tagName?.toLowerCase();
if (['h1', 'h2', 'h3', 'p', 'ul', 'ol'].includes(tag)) {
parentTag = tag;
break;
}
node = node.parentNode;
}
}
const activeBlock = parentTag || block;
setActiveStates({
isBold,
isItalic,
isUnderline,
isH1: activeBlock === 'h1',
isH2: activeBlock === 'h2',
isH3: activeBlock === 'h3',
isP: activeBlock === 'p' || (!['h1', 'h2', 'h3'].includes(activeBlock) && !isUL && !isOL),
isUL,
isOL,
align: doc.queryCommandState && doc.queryCommandState('justifyCenter')
? 'center'
: doc.queryCommandState && doc.queryCommandState('justifyLeft')
? 'left'
: 'right',
});
} catch {
// Ignored if document commands aren't ready
}
}, [isCodeMode]);
// Sync value from props on initial mount or when external change occurs
useEffect(() => { useEffect(() => {
if (!editorRef.current) return; if (!editorRef.current) return;
if (!isMountedRef.current) { if (!isMountedRef.current) {
@ -53,22 +152,43 @@ export default function RichTextEditor({
return; return;
} }
// Only update innerHTML if it's different and not currently focused/typing
if (editorRef.current.innerHTML !== (value || '')) { if (editorRef.current.innerHTML !== (value || '')) {
if (!isTypingRef.current && document.activeElement !== editorRef.current) { if (!isTypingRef.current && document.activeElement !== editorRef.current) {
editorRef.current.innerHTML = value || ''; editorRef.current.innerHTML = value || '';
} }
} }
}, [value]); }, [value, isCodeMode]);
const exec = (command: string, val: string | undefined = undefined) => { const exec = (command: string, val: string | undefined = undefined) => {
if (isCodeMode) return;
editorRef.current?.focus();
document.execCommand(command, false, val); document.execCommand(command, false, val);
if (editorRef.current) { if (editorRef.current) {
onChange(editorRef.current.innerHTML); onChange(editorRef.current.innerHTML);
} }
setTimeout(updateActiveStates, 20);
};
// Robust formatBlock handler for H1, H2, H3, P
const setBlockTag = (tagName: 'h1' | 'h2' | 'h3' | 'p') => {
if (isCodeMode) return;
editorRef.current?.focus();
// Cross-browser formatBlock
const blockValue = `<${tagName}>`;
const success = document.execCommand('formatBlock', false, blockValue);
if (!success) {
document.execCommand('formatBlock', false, tagName);
}
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
}
setTimeout(updateActiveStates, 20);
}; };
const handleLink = () => { const handleLink = () => {
if (isCodeMode) return;
const url = prompt('آدرس لینک (URL) را وارد کنید:', 'https://'); const url = prompt('آدرس لینک (URL) را وارد کنید:', 'https://');
if (url) { if (url) {
exec('createLink', url); exec('createLink', url);
@ -82,99 +202,167 @@ export default function RichTextEditor({
setTimeout(() => { setTimeout(() => {
isTypingRef.current = false; isTypingRef.current = false;
}, 50); }, 50);
updateActiveStates();
} }
}; };
const clearFormatting = () => {
exec('removeFormat');
setBlockTag('p');
};
return ( return (
<div className={`border border-gray-200 rounded-2xl bg-white overflow-hidden shadow-xs ${className}`}> <div className={`border border-gray-200 rounded-2xl bg-white overflow-hidden shadow-xs relative ${className}`}>
{/* Toolbar */} {/* Toolbar */}
<div className="bg-gray-50/80 border-b border-gray-100 p-2 flex flex-wrap items-center gap-1 text-gray-700"> <div className="bg-gray-50/80 border-b border-gray-100 p-2 flex flex-wrap items-center gap-1 text-gray-700 select-none">
{/* Headings */} {/* Headings */}
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200"> <div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
<button <button
type="button" type="button"
onClick={() => exec('formatBlock', '<h1>')} disabled={isCodeMode}
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer" onClick={() => setBlockTag('h1')}
title="تگ تیتر H1 (برای سئو)" className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
activeStates.isH1
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-700'
}`}
title="تگ تیتر بزرگ H1 (سئو)"
> >
<Heading1 className="w-4 h-4 text-purple-700" /> <Heading1 className="w-3.5 h-3.5" />
<span>H1</span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => exec('formatBlock', '<h2>')} disabled={isCodeMode}
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer" onClick={() => setBlockTag('h2')}
className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
activeStates.isH2
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-600'
}`}
title="تگ تیتر H2" title="تگ تیتر H2"
> >
<Heading2 className="w-4 h-4 text-purple-600" /> <Heading2 className="w-3.5 h-3.5" />
<span>H2</span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => exec('formatBlock', '<h3>')} disabled={isCodeMode}
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer" onClick={() => setBlockTag('h3')}
className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
activeStates.isH3
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-500'
}`}
title="تگ تیتر H3" title="تگ تیتر H3"
> >
<Heading3 className="w-4 h-4 text-purple-500" /> <Heading3 className="w-3.5 h-3.5" />
<span>H3</span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => exec('formatBlock', '<p>')} disabled={isCodeMode}
className="p-1.5 hover:bg-gray-100 rounded-lg text-[11px] font-bold transition-colors cursor-pointer text-gray-600 px-2" onClick={() => setBlockTag('p')}
className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer disabled:opacity-40 ${
activeStates.isP && !activeStates.isH1 && !activeStates.isH2 && !activeStates.isH3
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="پاراگراف عادی (P)" title="پاراگراف عادی (P)"
> >
P P
</button> </button>
</div> </div>
{/* Basic Formats */} {/* Basic Formats (Bold, Italic, Underline) */}
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200"> <div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('bold')} onClick={() => exec('bold')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
title="Bold" activeStates.isBold
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="بولد (ضخیم)"
> >
<Bold className="w-4 h-4" /> <Bold className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('italic')} onClick={() => exec('italic')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
title="Italic" activeStates.isItalic
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="ایتالیک (کج)"
> >
<Italic className="w-4 h-4" /> <Italic className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('underline')} onClick={() => exec('underline')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
title="Underline" activeStates.isUnderline
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="خط زیرین (Underline)"
> >
<Underline className="w-4 h-4" /> <Underline className="w-4 h-4" />
</button> </button>
<button
type="button"
disabled={isCodeMode}
onClick={clearFormatting}
className="p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-40"
title="پاکسازی قالب‌بندی و استایل‌ها"
>
<RemoveFormatting className="w-4 h-4" />
</button>
</div> </div>
{/* Alignment */} {/* Alignment */}
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200"> <div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('justifyRight')} onClick={() => exec('justifyRight')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
activeStates.align === 'right'
? 'bg-purple-50 text-purple-700 font-bold'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="راست‌چین" title="راست‌چین"
> >
<AlignRight className="w-4 h-4" /> <AlignRight className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('justifyCenter')} onClick={() => exec('justifyCenter')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
activeStates.align === 'center'
? 'bg-purple-50 text-purple-700 font-bold'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="وسط‌چین" title="وسط‌چین"
> >
<AlignCenter className="w-4 h-4" /> <AlignCenter className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('justifyLeft')} onClick={() => exec('justifyLeft')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
activeStates.align === 'left'
? 'bg-purple-50 text-purple-700 font-bold'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="چپ‌چین" title="چپ‌چین"
> >
<AlignLeft className="w-4 h-4" /> <AlignLeft className="w-4 h-4" />
@ -182,81 +370,249 @@ export default function RichTextEditor({
</div> </div>
{/* Lists & Link */} {/* Lists & Link */}
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200"> <div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('insertUnorderedList')} onClick={() => exec('insertUnorderedList')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
title="لیست بالت‌دار" activeStates.isUL
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="لیست بالت‌دار (نقطه‌ای)"
> >
<List className="w-4 h-4" /> <List className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('insertOrderedList')} onClick={() => exec('insertOrderedList')}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer" className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
activeStates.isOL
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="لیست شماره‌دار" title="لیست شماره‌دار"
> >
<ListOrdered className="w-4 h-4" /> <ListOrdered className="w-4 h-4" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={handleLink} onClick={handleLink}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer text-blue-600" className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer text-blue-600 disabled:opacity-40"
title="درج لینک" title="درج لینک اینترنتی"
> >
<Link className="w-4 h-4" /> <LinkIcon className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Color Palette */} {/* Text Color Picker & Custom HEX */}
<div className="flex items-center gap-1 bg-white px-2 py-1 rounded-xl border border-gray-200"> <div className="relative">
<Palette className="w-3.5 h-3.5 text-gray-400" /> <button
<div className="flex items-center gap-1"> type="button"
{COLORS.map((c) => ( disabled={isCodeMode}
<button onClick={() => {
key={c.value} setShowColorPicker(!showColorPicker);
type="button" setShowBgColorPicker(false);
onClick={() => exec('foreColor', c.value)} }}
className="w-3.5 h-3.5 rounded-full border border-black/10 cursor-pointer hover:scale-125 transition-transform" className={`flex items-center gap-1.5 bg-white px-2.5 py-1.5 rounded-xl border border-gray-200 shadow-2xs text-xs font-bold cursor-pointer disabled:opacity-40 ${
style={{ backgroundColor: c.value }} showColorPicker ? 'ring-2 ring-purple-500 bg-purple-50' : 'hover:bg-gray-50'
title={c.label} }`}
/> title="انتخاب رنگ متن"
))} >
</div> <Palette className="w-3.5 h-3.5 text-purple-600" />
<span>رنگ متن</span>
<span
className="w-3 h-3 rounded-full border border-black/20"
style={{ backgroundColor: customColor }}
/>
</button>
{showColorPicker && (
<div className="absolute top-full right-0 mt-1.5 p-3 bg-white rounded-2xl shadow-xl border border-gray-200 z-50 w-56 space-y-2.5 font-vazir text-right">
<span className="text-[11px] font-bold text-gray-500 block">رنگ‌های پیشنهادی:</span>
<div className="grid grid-cols-7 gap-1">
{PRESET_COLORS.map((c) => (
<button
key={c.value}
type="button"
onClick={() => {
exec('foreColor', c.value);
setCustomColor(c.value);
setShowColorPicker(false);
}}
className="w-6 h-6 rounded-lg border border-black/10 cursor-pointer hover:scale-110 transition-transform"
style={{ backgroundColor: c.value }}
title={c.label}
/>
))}
</div>
<div className="pt-2 border-t border-gray-100 flex items-center gap-2">
<input
type="color"
value={customColor}
onChange={(e) => {
setCustomColor(e.target.value);
exec('foreColor', e.target.value);
}}
className="w-8 h-8 rounded-lg cursor-pointer border-none bg-transparent"
/>
<input
type="text"
value={customColor}
onChange={(e) => {
setCustomColor(e.target.value);
exec('foreColor', e.target.value);
}}
placeholder="#111827"
className="w-full text-xs font-mono font-bold px-2 py-1 border border-gray-200 rounded-lg outline-none uppercase"
dir="ltr"
/>
</div>
</div>
)}
</div>
{/* Background Color Picker & Custom HEX */}
<div className="relative">
<button
type="button"
disabled={isCodeMode}
onClick={() => {
setShowBgColorPicker(!showBgColorPicker);
setShowColorPicker(false);
}}
className={`flex items-center gap-1.5 bg-white px-2.5 py-1.5 rounded-xl border border-gray-200 shadow-2xs text-xs font-bold cursor-pointer disabled:opacity-40 ${
showBgColorPicker ? 'ring-2 ring-purple-500 bg-purple-50' : 'hover:bg-gray-50'
}`}
title="هایلایت و رنگ پس‌زمینه متن"
>
<PaintBucket className="w-3.5 h-3.5 text-amber-500" />
<span>هایلایت</span>
<span
className="w-3 h-3 rounded-full border border-black/20"
style={{ backgroundColor: customBgColor }}
/>
</button>
{showBgColorPicker && (
<div className="absolute top-full right-0 mt-1.5 p-3 bg-white rounded-2xl shadow-xl border border-gray-200 z-50 w-56 space-y-2.5 font-vazir text-right">
<span className="text-[11px] font-bold text-gray-500 block">رنگ‌های هایلایت:</span>
<div className="grid grid-cols-7 gap-1">
{PRESET_BG_COLORS.map((c) => (
<button
key={c.value}
type="button"
onClick={() => {
exec('hiliteColor', c.value);
setCustomBgColor(c.value);
setShowBgColorPicker(false);
}}
className="w-6 h-6 rounded-lg border border-black/10 cursor-pointer hover:scale-110 transition-transform"
style={{ backgroundColor: c.value }}
title={c.label}
/>
))}
</div>
<div className="pt-2 border-t border-gray-100 flex items-center gap-2">
<input
type="color"
value={customBgColor === 'transparent' ? '#ffffff' : customBgColor}
onChange={(e) => {
setCustomBgColor(e.target.value);
exec('hiliteColor', e.target.value);
}}
className="w-8 h-8 rounded-lg cursor-pointer border-none bg-transparent"
/>
<input
type="text"
value={customBgColor}
onChange={(e) => {
setCustomBgColor(e.target.value);
exec('hiliteColor', e.target.value);
}}
placeholder="#fef08a"
className="w-full text-xs font-mono font-bold px-2 py-1 border border-gray-200 rounded-lg outline-none uppercase"
dir="ltr"
/>
</div>
</div>
)}
</div>
{/* HTML Code View vs Visual */}
<div className="flex items-center bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
<button
type="button"
onClick={() => {
setIsCodeMode(!isCodeMode);
setShowColorPicker(false);
setShowBgColorPicker(false);
}}
className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer ${
isCodeMode
? 'bg-purple-600 text-white shadow-xs'
: 'text-gray-700 hover:bg-gray-100'
}`}
title="نمایش و ویرایش مستقیم کد HTML"
>
<Code className="w-3.5 h-3.5" />
<span>{isCodeMode ? 'نمای دیداری' : 'کد HTML'}</span>
</button>
</div> </div>
{/* Undo / Redo */} {/* Undo / Redo */}
<div className="flex items-center gap-0.5 ms-auto"> <div className="flex items-center gap-0.5 ms-auto">
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('undo')} onClick={() => exec('undo')}
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer" className="p-1.5 hover:bg-gray-200 disabled:opacity-40 rounded-lg transition-colors cursor-pointer"
title="Undo" title="بازگشت به عقب (Undo)"
> >
<Undo className="w-3.5 h-3.5 text-gray-500" /> <Undo className="w-3.5 h-3.5 text-gray-500" />
</button> </button>
<button <button
type="button" type="button"
disabled={isCodeMode}
onClick={() => exec('redo')} onClick={() => exec('redo')}
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer" className="p-1.5 hover:bg-gray-200 disabled:opacity-40 rounded-lg transition-colors cursor-pointer"
title="Redo" title="حرکت به جلو (Redo)"
> >
<Redo className="w-3.5 h-3.5 text-gray-500" /> <Redo className="w-3.5 h-3.5 text-gray-500" />
</button> </button>
</div> </div>
</div> </div>
{/* Editable Area */} {/* Editor Content Area */}
<div {isCodeMode ? (
ref={editorRef} <textarea
contentEditable value={value || ''}
suppressContentEditableWarning onChange={(e) => onChange(e.target.value)}
onInput={handleInput} placeholder="کد HTML را اینجا وارد یا ویرایش کنید..."
data-placeholder={placeholder} className="w-full p-4 min-h-[140px] max-h-[350px] font-mono text-xs text-emerald-400 bg-slate-950 outline-none resize-y leading-relaxed"
className="p-4 min-h-[140px] max-h-[350px] overflow-y-auto outline-none text-xs leading-relaxed text-gray-800 font-vazir empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400" dir="ltr"
dir="rtl" spellCheck={false}
/> />
) : (
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
onInput={handleInput}
onKeyUp={updateActiveStates}
onMouseUp={updateActiveStates}
onClick={updateActiveStates}
onFocus={updateActiveStates}
data-placeholder={placeholder}
className="p-4 min-h-[140px] max-h-[350px] overflow-y-auto outline-none text-sm leading-relaxed text-gray-800 font-vazir empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400 [&_h1]:text-2xl [&_h1]:font-black [&_h1]:my-2.5 [&_h1]:text-gray-900 [&_h2]:text-xl [&_h2]:font-bold [&_h2]:my-2 [&_h2]:text-gray-900 [&_h3]:text-lg [&_h3]:font-bold [&_h3]:my-1.5 [&_h3]:text-gray-900 [&_p]:my-1 [&_p]:leading-relaxed [&_ul]:list-disc [&_ul]:ms-6 [&_ul]:my-2 [&_ol]:list-decimal [&_ol]:ms-6 [&_ol]:my-2 [&_a]:text-blue-600 [&_a]:underline"
dir="rtl"
/>
)}
</div> </div>
); );
} }

View File

@ -138,8 +138,8 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
{titleParts[1] && <span className="text-canina-blue ">{titleParts[1]}</span>} {titleParts[1] && <span className="text-canina-blue ">{titleParts[1]}</span>}
</h1> </h1>
<p <div
className="text-sm lg:text-xl text-medical-gray-600 mb-3 sm:mb-5 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir animate-fade-in" className="text-sm lg:text-xl text-medical-gray-600 mb-3 sm:mb-5 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir animate-fade-in [&_p]:text-sm [&_p]:lg:text-xl [&_p]:leading-relaxed [&_p]:my-0 [&_span]:text-inherit"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: subtitle __html: subtitle
}} }}