fix(admin): resolve Tiptap editor bugs, dynamic RTL/LTR layout, sticky TOC, and equalize Bidi typography
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react';
|
||||
import { ArrowLeftIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { renderToHTMLString } from '@tiptap/static-renderer';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
import { ButtonLink } from '../components/Button';
|
||||
@@ -12,17 +12,23 @@ import { useSite } from '../api';
|
||||
import { applySeo } from '../seo';
|
||||
import { useEffect } from 'react';
|
||||
import { ARTICLE_EXTENSIONS } from '../components/admin/ArticleEditor';
|
||||
import { docToText } from '../utils/tiptapDoc';
|
||||
import { detectDirection } from '../utils/direction';
|
||||
import { wrapScriptRuns } from '../utils/bidiText';
|
||||
import type { Article } from '../data/site';
|
||||
|
||||
/** بدنه مقاله را به HTML و فهرست مطالب تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||
function renderBody(article: Article): { html: string | null; toc: TocItem[]; legacy: string[] } {
|
||||
function renderBody(
|
||||
article: Article,
|
||||
dir: 'rtl' | 'ltr',
|
||||
): { html: string | null; toc: TocItem[]; legacy: string[] } {
|
||||
const body = article.body as unknown;
|
||||
if (Array.isArray(body)) return { html: null, toc: [], legacy: body };
|
||||
if (body && typeof body === 'object' && (body as { type?: string }).type === 'doc') {
|
||||
try {
|
||||
const html = renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never });
|
||||
// تیترهای H2/H3 شناسه لنگر میگیرند تا فهرست مطالب به آنها بپرد
|
||||
const { items, html: htmlWithIds } = extractToc(html);
|
||||
// تیترها شناسه لنگر میگیرند و متن لاتین/فارسی برای قاعده «۲ پیکسل کوچکتر» بستهبندی میشود
|
||||
const { items, html: htmlWithIds } = extractToc(html, (root) => wrapScriptRuns(root, dir));
|
||||
return { html: htmlWithIds, toc: items, legacy: [] };
|
||||
} catch {
|
||||
return { html: null, toc: [], legacy: [] };
|
||||
@@ -73,12 +79,23 @@ export function ArticleDetail() {
|
||||
|
||||
}
|
||||
|
||||
const { html, toc, legacy } = renderBody(article);
|
||||
const dir = detectDirection(article.title, article.excerpt, docToText(article.body));
|
||||
const { html, toc, legacy } = renderBody(article, dir);
|
||||
const related = articles.filter((a) => a.slug !== article.slug && a.status !== 'draft').slice(0, 3);
|
||||
|
||||
/**
|
||||
* جهت غالب متن مقاله — بدنه (با data-dir، طبق قاعده CSS مقاله) و سمت فهرست مطالب
|
||||
* از همین پیروی میکنند: فارسی راستبهچپ (فهرست سمت راست)، انگلیسی چپبهراست (فهرست سمت چپ).
|
||||
*/
|
||||
const hasToc = toc.length > 0;
|
||||
// در چیدمان راستبهچپ ستون شماره ۱ سمت راست است؛ پس ستونها صریح شماره میخورند
|
||||
const gridColumns = hasToc ? 'lg:grid-cols-[17rem_minmax(0,1fr)]' : '';
|
||||
const bodyColumns = 'lg:col-start-2';
|
||||
const tocColumns = 'lg:col-start-1';
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex h-[56vh] min-h-[400px] items-end overflow-hidden">
|
||||
<section dir={dir} className="relative flex h-[56vh] min-h-[400px] items-end overflow-hidden">
|
||||
<img
|
||||
src={article.image}
|
||||
alt={article.image_alt || article.title}
|
||||
@@ -96,11 +113,11 @@ export function ArticleDetail() {
|
||||
<Link to="/" className="transition-colors hover:text-copper">
|
||||
صفحه اصلی
|
||||
</Link>
|
||||
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||
{dir === 'ltr' ? <ChevronRightIcon className="h-3.5 w-3.5" /> : <ChevronLeftIcon className="h-3.5 w-3.5" />}
|
||||
<Link to="/blog" className="transition-colors hover:text-copper">
|
||||
وبلاگ
|
||||
</Link>
|
||||
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||
{dir === 'ltr' ? <ChevronRightIcon className="h-3.5 w-3.5" /> : <ChevronLeftIcon className="h-3.5 w-3.5" />}
|
||||
<span className="text-copper">{article.category}</span>
|
||||
</nav>
|
||||
<h1 className="mt-5 max-w-[700px] text-hero font-bold text-ivory">{article.title}</h1>
|
||||
@@ -112,10 +129,14 @@ export function ArticleDetail() {
|
||||
|
||||
<article className="bg-ink py-20 lg:py-28">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<div className="mx-auto grid max-w-5xl gap-12 lg:grid-cols-[minmax(0,1fr)_17rem] lg:gap-14">
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
dir={dir}
|
||||
data-dir={dir}
|
||||
className={`mx-auto grid max-w-5xl gap-12 lg:gap-14 ${gridColumns}`}
|
||||
>
|
||||
<div className={`min-w-0 lg:row-start-1 ${hasToc ? bodyColumns : ''}`}>
|
||||
<Reveal>
|
||||
<p className="border-r-2 border-copper pr-6 text-lg font-light leading-10 text-ivory/80">
|
||||
<p className="border-s-2 border-copper ps-6 text-lg font-light leading-10 text-ivory/80">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
</Reveal>
|
||||
@@ -134,9 +155,12 @@ export function ArticleDetail() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* فهرست مطالب — در دسکتاپ کنار متن چسبان میماند */}
|
||||
{toc.length > 0 && (
|
||||
<aside className="order-first lg:order-none">
|
||||
{/* فهرست مطالب — در دسکتاپ کنار متن میچسبد و همراه مقاله پایین میآید */}
|
||||
{hasToc && (
|
||||
<aside
|
||||
dir={dir}
|
||||
className={`order-first lg:order-none lg:row-start-1 ${tocColumns}`}
|
||||
>
|
||||
<TableOfContents items={toc} className="lg:sticky lg:top-28" />
|
||||
</aside>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user