Files
teknav-bak/app/teknav-articles.jsx
arsam 47e3eaded1 Import source: app/teknav-articles
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:26:11 +03:30

1146 lines
72 KiB
React
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.
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { TeknavData } from './teknav-data.js';
import {
useNav, useToast, useAuth,
AuthorAvatar, CategoryBadge, TypeBadge, StatusBadge, EmptyState, Header, ReadingProgress,
ReadingListModal,
inputStyle, pillBtn, iconBtnStyle, useViewportPrefetch,
} from './teknav-ui.jsx';
import { LazyDiagram } from './src/components/lazy-diagram.jsx';
import { ArticleCard } from './teknav-home.jsx';
import { contentApi } from './src/lib/content-api.js';
import { engagementApi } from './src/lib/engagement-api.js';
import { QuoteSharer } from './src/components/QuoteSharer.jsx';
import { iconNode } from './src/lib/icon.jsx';
import crownIcon from './src/assets/icons/crown.svg';
import thumbsUpIcon from './src/assets/icons/thumbs-up.svg';
import starIcon from './src/assets/icons/star.svg';
import heartIcon from './src/assets/icons/heart.svg';
import flameIcon from './src/assets/icons/flame.svg';
import lightbulbIcon from './src/assets/icons/lightbulb.svg';
function articleDateValue(article) {
return article?.dateEn || article?.publishedAt || article?.date || '';
}
function articleDateLabel(article) {
return article?.date || (typeof article?.publishedAt === 'string' ? article.publishedAt.slice(0, 10) : '') || article?.dateEn || '';
}
const TEKRAG_SLUG = 'tekrag-persian-data-centric-rag-benchmark';
const isTekRagArticle = (articleOrSlug) => (typeof articleOrSlug === 'string' ? articleOrSlug : articleOrSlug?.slug) === TEKRAG_SLUG;
function numberLabel(value, english = false) {
return Number(value ?? 0).toLocaleString(english ? 'en-US' : 'fa-IR');
}
function ArticleRow({ article, idx }) {
const { navigate } = useNav();
const [vis, setVis] = useState(false);
const ref = useRef();
useViewportPrefetch(ref, article.slug);
const cat = TeknavData.categories.find(c => c.id === article.category);
const author = TeknavData.authors.find(a => a.id === article.authorId);
useEffect(() => {
const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) setVis(true); }, { threshold: 0.05 });
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
return (
<div ref={ref} className="article-row" style={{
display: 'grid', gridTemplateColumns: '1fr 140px', gap: 24,
padding: '32px 0', borderBottom: '1px solid #E4DDD2', direction: 'rtl',
cursor: 'pointer', opacity: vis ? 1 : 0, transform: vis ? 'none' : 'translateY(16px)',
transition: `opacity 0.4s ${idx * 60}ms, transform 0.4s ${idx * 60}ms`,
}} onClick={() => navigate('/article/' + article.slug)}
onMouseEnter={e => { e.currentTarget.style.opacity = '0.85'; }}
onMouseLeave={e => { e.currentTarget.style.opacity = '1'; }}>
<div>
<div className="article-row-meta" style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>{articleDateLabel(article)}</span>
<span style={{ color: '#E4DDD2' }}>·</span>
<TypeBadge type={article.type} small />
<CategoryBadge name={article.categoryName} color={cat?.color} small />
</div>
<h2 style={{ fontSize: 19, fontWeight: 800, color: '#263238', margin: '0 0 6px', lineHeight: 1.5, fontFamily: 'Vazirmatn,sans-serif' }}>{article.title}</h2>
{article.subtitle && <div style={{ fontSize: 14, color: '#0F6B73', fontWeight: 600, margin: '0 0 8px' }}>{article.subtitle}</div>}
<div style={{ fontSize: 13, color: '#5F6B6D', margin: '0 0 10px' }}>
<button
onClick={(e) => {
e.stopPropagation();
if (article.authorUsername) navigate('/profile/@' + article.authorUsername);
}}
style={{ background: 'none', border: 'none', padding: 0, cursor: article.authorUsername ? 'pointer' : 'default', color: article.authorUsername ? '#0F6B73' : '#5F6B6D', fontFamily: 'Vazirmatn,sans-serif', fontSize: 13, fontWeight: 700 }}
>
{article.authorName}
</button>
{author?.specialty && <span style={{ color: '#E4DDD2' }}> · </span>}
{author?.specialty && <span>{author.specialty}</span>}
</div>
<p style={{ fontSize: 14, color: '#5F6B6D', lineHeight: 1.8, margin: '0 0 12px' }}>{article.summary}</p>
<div className="article-row-stats" style={{ display: 'flex', gap: 16, fontSize: 12, color: '#5F6B6D', alignItems: 'center' }}>
<span style={{ color: '#C46A4D', fontWeight: 600 }}>{article.type}</span>
<span>·</span>
<span>{article.readTime} دقیقه مطالعه</span>
<span>·</span>
<span>{(article.views || 0).toLocaleString('fa')} بازدید</span>
{article.tags?.slice(0, 2).map(t => (
<span key={t} style={{ background: '#F4EFE6', padding: '2px 8px', borderRadius: 4, fontSize: 11 }}>{t}</span>
))}
</div>
</div>
<div className="article-row-thumb" style={{ background: '#F4EFE6', borderRadius: 10, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', height: 120, alignSelf: 'center' }}>
<div style={{ transform: 'scale(0.5)', transformOrigin: 'center', width: 280 }}>
<LazyDiagram type={article.diagram} compact />
</div>
</div>
</div>
);
}
function FilterBar({ filters, onChange }) {
const cats = [{ id: '', name: 'همه' }, ...TeknavData.categories];
const types = ['همه', 'تحلیل عمیق', 'راهنمای فنی', 'داده‌نما', 'پژوهش', 'راستی‌آزمایی‌شده', 'پرونده'];
const sorts = [{ v: 'latest', l: 'جدیدترین' }, { v: 'popular', l: 'پربازدیدترین' }, { v: 'readtime', l: 'زمان مطالعه' }];
const chip = (active) => ({
border: active ? '1.5px solid #0F6B73' : '1px solid #E4DDD2',
background: active ? '#0F6B7312' : '#fff',
color: active ? '#0F6B73' : '#5F6B6D',
padding: '5px 12px', borderRadius: 20, fontSize: 12, cursor: 'pointer',
fontFamily: 'Vazirmatn,sans-serif', fontWeight: active ? 700 : 400,
transition: 'all 0.15s', whiteSpace: 'nowrap',
});
return (
<div className="filter-bar" style={{ direction: 'rtl', marginBottom: 32, display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className="filter-chip-row" style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D', whiteSpace: 'nowrap' }}>دسته:</span>
{cats.map(c => (
<button key={c.id} style={chip(filters.cat === c.id)} onClick={() => onChange({ ...filters, cat: c.id })}>{c.name}</button>
))}
</div>
<div className="filter-chip-row" style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D', whiteSpace: 'nowrap' }}>نوع:</span>
{types.map(t => (
<button key={t} style={chip(filters.type === t || (!filters.type && t === 'همه'))} onClick={() => onChange({ ...filters, type: t === 'همه' ? '' : t })}>{t}</button>
))}
</div>
<div className="filter-chip-row" style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>مرتب‌سازی:</span>
{sorts.map(s => (
<button key={s.v} style={chip(filters.sort === s.v)} onClick={() => onChange({ ...filters, sort: s.v })}>{s.l}</button>
))}
</div>
{filters.q && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#F4EFE6', padding: '5px 12px', borderRadius: 20 }}>
<span style={{ fontSize: 12, color: '#263238' }}>جستجو: «{filters.q}»</span>
<button onClick={() => onChange({ ...filters, q: '' })} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#5F6B6D', fontSize: 14 }}>✕</button>
</div>
)}
</div>
</div>
);
}
function ArticleListPage({ initQ = '' }) {
const [filters, setFilters] = useState({ cat: '', type: '', sort: 'latest', q: initQ });
const [searchInput, setSearchInput] = useState(initQ);
const [page, setPage] = useState(1);
const [allArticles, setAllArticles] = useState(() => contentApi.fallbackArticles());
const PER = 8;
useEffect(() => {
let cancelled = false;
contentApi.listArticles({ limit: 100 })
.then((res) => { if (!cancelled) setAllArticles(res.items ?? []); })
.catch(() => {});
return () => { cancelled = true; };
}, []);
const filtered = useMemo(() => {
let list = allArticles;
if (filters.q) {
const q = filters.q.toLowerCase();
list = list.filter(a => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q) || a.authorName?.toLowerCase().includes(q) || a.categoryName?.toLowerCase().includes(q));
}
if (filters.cat) list = list.filter(a => a.category === filters.cat);
if (filters.type) list = list.filter(a => a.type === filters.type);
if (filters.sort === 'popular') list = [...list].sort((a, b) => b.views - a.views);
else if (filters.sort === 'readtime') list = [...list].sort((a, b) => b.readTime - a.readTime);
else list = [...list].sort((a, b) => articleDateValue(b).localeCompare(articleDateValue(a)));
return list;
}, [filters, allArticles]);
const visible = filtered.slice(0, page * PER);
const hasMore = visible.length < filtered.length;
const doSearch = (e) => { e.preventDefault(); setFilters(f => ({ ...f, q: searchInput })); setPage(1); };
return (
<div className="article-list-page" style={{ paddingTop: 80, background: '#FAF7F0', minHeight: '100vh', fontFamily: 'Vazirmatn,sans-serif' }}>
<div className="article-list-inner" style={{ maxWidth: 1000, margin: '0 auto', padding: '0 24px' }}>
<div className="article-list-head" style={{ direction: 'rtl', padding: '32px 0 0' }}>
<h1 style={{ fontSize: 28, fontWeight: 900, color: '#263238', margin: '0 0 4px' }}>همه مقاله‌ها</h1>
<p style={{ fontSize: 14, color: '#5F6B6D', margin: '0 0 28px' }}>{filtered.length} مقاله یافت شد</p>
<form className="article-search-form" onSubmit={doSearch} style={{ display: 'flex', gap: 10, marginBottom: 24 }}>
<input value={searchInput} onChange={e => setSearchInput(e.target.value)}
placeholder="جستجو در عنوان، خلاصه، نویسنده..."
style={{ flex: 1, ...inputStyle }} dir="rtl" />
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>جستجو</button>
</form>
</div>
<FilterBar filters={filters} onChange={(f) => { setFilters(f); setPage(1); }} />
{visible.length === 0 ? (
<EmptyState title="مقاله‌ای یافت نشد" subtitle="فیلترها یا کلمات جستجو را تغییر دهید" />
) : (
<>
{visible.map((a, i) => <ArticleRow key={a.id} article={a} idx={i} />)}
{hasMore && (
<div className="load-more-wrap" style={{ textAlign: 'center', padding: '32px 0' }}>
<button onClick={() => setPage(p => p + 1)} style={{ ...pillBtn, background: '#0F6B73', color: '#fff', padding: '12px 32px' }}>
بارگذاری مقاله‌های بیشتر
</button>
</div>
)}
</>
)}
</div>
</div>
);
}
function TableOfContents({ headings, english = false }) {
const [active, setActive] = useState(null);
useEffect(() => {
const obs = new IntersectionObserver(entries => {
entries.forEach(e => { if (e.isIntersecting) setActive(e.target.id); });
}, { rootMargin: '-20% 0px -70% 0px' });
headings.forEach(h => { const el = document.getElementById(h.id); if (el) obs.observe(el); });
return () => obs.disconnect();
}, [headings]);
return (
<nav style={{ position: 'sticky', top: 90, maxHeight: 'calc(100vh - 100px)', overflowY: 'auto', direction: 'rtl' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#5F6B6D', letterSpacing: '0.08em', marginBottom: 12 }}>{english ? 'CONTENTS' : 'فهرست مطالب'}</div>
{headings.map(h => (
<a key={h.id} href={'#' + h.id} onClick={e => { e.preventDefault(); document.getElementById(h.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }}
style={{
display: 'block', padding: h.level === 3 ? '4px 12px' : '5px 0', fontSize: h.level === 3 ? 12 : 13,
color: active === h.id ? '#0F6B73' : '#5F6B6D', textDecoration: 'none',
fontWeight: active === h.id ? 700 : 400, borderRight: active === h.id ? '2px solid #0F6B73' : '2px solid transparent',
transition: 'all 0.15s', lineHeight: 1.5,
}}>{h.text}</a>
))}
</nav>
);
}
function EmbeddedDiagramCard({ title, subtitle, type, english = false }) {
return (
<div className={english ? 'article-diagram-card tekrag-diagram-card' : 'article-diagram-card'}>
<div style={{ direction: english ? 'ltr' : 'rtl', textAlign: english ? 'center' : 'right', marginBottom: 14 }}>
<div style={{ fontSize: english ? 15 : 13, fontWeight: 800, color: '#0F6B73', marginBottom: 4 }}>{title}</div>
{subtitle && <div style={{ fontSize: 12, color: '#5F6B6D', lineHeight: 1.8, maxWidth: english ? 680 : 'none', margin: english ? '0 auto' : 0 }}>{subtitle}</div>}
</div>
<LazyDiagram type={type} />
</div>
);
}
function ArticleEmbeddedDiagrams({ slug }) {
if (isTekRagArticle(slug)) {
return null;
}
if (slug === 'ai-clean-data') {
return (
<EmbeddedDiagramCard
title="نمودار تعاملی: پایپ‌لاین داده"
subtitle="با موس روی هر مرحله بروید تا توضیح بیشتر ببینید"
type="pipeline"
/>
);
}
if (slug === 'ai-agents-2026' || slug === 'agentic-ai-production') {
return (
<div style={{ display: 'grid', gap: 22, margin: '34px 0' }}>
<EmbeddedDiagramCard
title="معماری یک AI Agent جدی"
subtitle="از مدل پایه و حافظه تا ابزارها، کنترل دسترسی، لاگ و بازبینی انسانی."
type="diagram-12"
/>
<EmbeddedDiagramCard
title="جریان تصمیم تا اجرای ابزار"
subtitle="عامل باید هدف را بشکند، ابزار درست را انتخاب کند، خروجی را ارزیابی کند و فقط در محدوده مجاز اقدام کند."
type="pipeline"
/>
<EmbeddedDiagramCard
title="ریسک‌های امنیتی در حلقه عامل"
subtitle="هر tool call یک سطح حمله است: prompt injection، نشت داده، سوءاستفاده از مجوز و اقدام اشتباه."
type="cyber"
/>
<EmbeddedDiagramCard
title="حرکت بازار از Copilot به Coworker"
subtitle="ارزش نرم‌افزار از نمایش داشبورد به واگذاری کنترل‌شده کارهای واقعی منتقل می‌شود."
type="orbit"
/>
</div>
);
}
return null;
}
function SeriesNavBanner({ series, currentSlug }) {
const { navigate } = useNav();
const idx = series.articles.findIndex(a => a.slug === currentSlug);
const prev = idx > 0 ? series.articles[idx - 1] : null;
const next = idx < series.articles.length - 1 ? series.articles[idx + 1] : null;
if (!prev && !next) return null;
const btnBase = {
background: 'transparent', border: '1px solid #0F6B7330', borderRadius: 8,
padding: '10px 16px', cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif',
color: '#0F6B73', fontSize: 13, fontWeight: 600, maxWidth: 280, textAlign: 'right',
transition: 'all 0.15s', lineHeight: 1.5,
};
return (
<div style={{ display: 'flex', gap: 12, justifyContent: 'space-between', padding: '16px 0', borderTop: '1px solid #E4DDD2', marginTop: 8, direction: 'rtl' }}>
{prev ? (
<button onClick={() => navigate('/article/' + prev.slug)} style={{ ...btnBase, borderRight: '3px solid #0F6B73' }}
onMouseEnter={e => e.currentTarget.style.background = '#0F6B7308'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<div style={{ fontSize: 10, color: '#5F6B6D', marginBottom: 3 }}>← قسمت قبلی</div>
<div>{prev.title.slice(0, 50)}{prev.title.length > 50 ? '…' : ''}</div>
</button>
) : <div />}
{next ? (
<button onClick={() => navigate('/article/' + next.slug)} style={{ ...btnBase, textAlign: 'left', borderLeft: '3px solid #0F6B73' }}
onMouseEnter={e => e.currentTarget.style.background = '#0F6B7308'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<div style={{ fontSize: 10, color: '#5F6B6D', marginBottom: 3, textAlign: 'left' }}>قسمت بعدی →</div>
<div>{next.title.slice(0, 50)}{next.title.length > 50 ? '…' : ''}</div>
</button>
) : <div />}
</div>
);
}
function ArticleQaSection({ slug, english = false }) {
const { user } = useAuth();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [body, setBody] = useState('');
const [replyTo, setReplyTo] = useState(null);
const toast = useToast();
const load = useCallback(() => {
setLoading(true);
engagementApi.listQa(slug)
.then(res => setItems(res.items ?? []))
.finally(() => setLoading(false));
}, [slug]);
useEffect(() => { load(); }, [load]);
const submit = async (e) => {
e.preventDefault();
if (!body.trim()) return;
try {
if (replyTo) await engagementApi.postAnswer(replyTo.id, body);
else await engagementApi.postQuestion(slug, body);
setBody('');
setReplyTo(null);
toast(english ? 'Submitted' : 'ثبت شد');
load();
} catch {
toast(english ? 'Submission failed' : 'خطا در ثبت', 'error');
}
};
return (
<section style={{ marginTop: 48, direction: english ? 'ltr' : 'rtl', textAlign: english ? 'left' : 'right' }}>
<h3 style={{ fontSize: 18, fontWeight: 800, color: '#263238', marginBottom: 20 }}>{english ? 'Questions and Answers' : 'پرسش و پاسخ'}</h3>
{user ? (
<form onSubmit={submit} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 14, marginBottom: 24 }}>
{replyTo && <div style={{ fontSize: 12, color: '#0F6B73', marginBottom: 8 }}>{english ? 'Replying to' : 'پاسخ به'}: {replyTo.author?.name} <button type="button" onClick={() => setReplyTo(null)} style={{ background: 'none', border: 'none', color: '#C94C4C', cursor: 'pointer', marginInlineStart: 8 }}>{english ? 'Cancel' : 'لغو'}</button></div>}
<textarea value={body} onChange={e => setBody(e.target.value)} placeholder={replyTo ? (english ? 'Write your answer...' : 'پاسخ خود را بنویسید...') : (english ? 'Ask a question...' : 'سوال خود را بپرسید...')} style={{ ...inputStyle, minHeight: 80, direction: english ? 'ltr' : 'rtl', textAlign: english ? 'left' : 'right' }} />
<div style={{ textAlign: english ? 'right' : 'left', marginTop: 10 }}>
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>{replyTo ? (english ? 'Submit Answer' : 'ثبت پاسخ') : (english ? 'Submit Question' : 'ثبت پرسش')}</button>
</div>
</form>
) : <div style={{ background: '#F4EFE6', padding: 16, borderRadius: 12, fontSize: 13, marginBottom: 24 }}>{english ? 'Sign in to ask a question.' : 'برای پرسیدن سوال وارد شوید.'}</div>}
{loading ? <div>{english ? 'Loading...' : 'در حال بارگذاری...'}</div> : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{items.length === 0 && <div style={{ fontSize: 13, color: '#5F6B6D' }}>{english ? 'No questions yet.' : 'هنوز پرسشی ثبت نشده است.'}</div>}
{items.map(q => (
<div key={q.id} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 16 }}>
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 8 }}>
<div style={{ fontSize: 13, fontWeight: 700 }}>{q.author?.name}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>{q.time}</div>
</div>
<p style={{ fontSize: 14, lineHeight: 1.8, marginBottom: 12 }}>{q.body}</p>
<button onClick={() => setReplyTo(q)} style={{ background: 'none', border: 'none', color: '#0F6B73', fontSize: 12, cursor: 'pointer', fontWeight: 600 }}>{english ? 'Reply' : 'پاسخ دادن'}</button>
{q.answers?.map(a => (
<div key={a.id} style={{ marginTop: 12, padding: 12, borderRight: '2px solid #E4DDD2', background: '#F4EFE680', borderRadius: '0 8px 8px 0' }}>
<div style={{ fontSize: 12, fontWeight: 700, marginBottom: 4 }}>{a.author?.name}</div>
<p style={{ fontSize: 13, lineHeight: 1.7 }}>{a.body}</p>
</div>
))}
</div>
))}
</div>
)}
</section>
);
}
function ArticleCommentSection({ article, onTotal, english = false }) {
const { user } = useAuth();
const { navigate } = useNav();
const toast = useToast();
const [items, setItems] = useState([]);
const [body, setBody] = useState('');
const [replyTo, setReplyTo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const refresh = useCallback(() => {
setLoading(true);
setError(false);
return engagementApi.listComments(article.slug)
.then(res => {
setItems(res?.items ?? []);
onTotal?.(res?.total ?? 0);
})
.catch(() => { setItems([]); setError(true); })
.finally(() => setLoading(false));
}, [article.slug, onTotal]);
useEffect(() => { refresh(); }, [refresh]);
const submit = async (e) => {
e.preventDefault();
if (!body.trim()) return;
try {
await engagementApi.postComment(article.slug, body, replyTo?.id);
setBody('');
setReplyTo(null);
toast(english ? 'Comment submitted' : 'نظر شما ثبت شد');
refresh();
} catch {
toast(english ? 'Comment submission failed' : 'ثبت نظر ناموفق بود', 'error');
}
};
const upvote = async (id) => {
try {
const res = await engagementApi.upvoteComment(id);
setItems(current => current.map(comment => {
if (comment.id === id) return { ...comment, upvotedByMe: res.upvoted, upvoteCount: res.upvoteCount };
return { ...comment, replies: (comment.replies ?? []).map(reply => reply.id === id ? { ...reply, upvotedByMe: res.upvoted, upvoteCount: res.upvoteCount } : reply) };
}));
} catch {
toast(english ? 'Sign in to vote' : 'برای رأی دادن ابتدا وارد شوید', 'warning');
}
};
const flag = async (id) => {
try {
await engagementApi.flagComment(id);
toast(english ? 'Report submitted' : 'گزارش نظر ثبت شد');
refresh();
} catch {
toast(english ? 'Sign in to report a comment' : 'برای گزارش نظر ابتدا وارد شوید', 'warning');
}
};
const like = async (id) => {
try {
const res = await engagementApi.likeComment(id);
setItems(current => current.map(comment => {
if (comment.id === id) return { ...comment, likedByMe: res.liked, likeCount: res.likeCount };
return { ...comment, replies: (comment.replies ?? []).map(reply => reply.id === id ? { ...reply, likedByMe: res.liked, likeCount: res.likeCount } : reply) };
}));
} catch {
toast(english ? 'Sign in to like a comment' : 'برای پسندیدن نظر ابتدا وارد شوید', 'warning');
}
};
const renderComment = (comment, child = false) => (
<div key={comment.id} style={{ background: child ? '#FAF7F0' : '#fff', border: '1px solid #E4DDD2', borderRadius: 10, padding: 14, marginTop: child ? 10 : 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start' }}>
<div>
<button
onClick={() => comment.author?.username && navigate('/profile/@' + comment.author.username)}
style={{ background: 'none', border: 'none', padding: 0, cursor: comment.author?.username ? 'pointer' : 'default', fontFamily: 'Vazirmatn,sans-serif', fontSize: 13, fontWeight: 800, color: comment.author?.username ? '#0F6B73' : '#263238' }}
>
{comment.author?.name ?? (english ? 'Reader' : 'کاربر')}
</button>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>@{comment.author?.username ?? 'user'} · {comment.time}</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<button onClick={() => upvote(comment.id)} style={{
...pillBtn, padding: '4px 10px', fontSize: 12,
background: comment.upvotedByMe ? '#0F6B7318' : '#F4EFE6',
color: comment.upvotedByMe ? '#0F6B73' : '#263238',
border: `1px solid ${comment.upvotedByMe ? '#0F6B7340' : '#E4DDD2'}`,
}}>
▲ {numberLabel(comment.upvoteCount, english)}
</button>
<button onClick={() => flag(comment.id)} style={{ ...pillBtn, background: '#F4EFE6', color: '#5F6B6D', fontSize: 11, padding: '4px 10px' }}>{english ? 'Report' : 'گزارش'}</button>
</div>
</div>
<p style={{ margin: '12px 0 0', fontSize: 14, lineHeight: 1.9, color: '#263238' }}>{comment.body}</p>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginTop: 8 }}>
<button onClick={() => like(comment.id)} style={{ ...pillBtn, background: comment.likedByMe ? '#0F6B7318' : 'transparent', color: comment.likedByMe ? '#0F6B73' : '#5F6B6D', fontSize: 12, padding: '4px 8px', border: comment.likedByMe ? '1px solid #0F6B7330' : '1px solid transparent' }}>
{english ? 'Like' : 'پسندیدن'} {numberLabel(comment.likeCount, english)}
</button>
{user && !child && <button onClick={() => setReplyTo(comment)} style={{ ...pillBtn, background: 'transparent', color: '#0F6B73', fontSize: 12, padding: '4px 0' }}>{english ? 'Reply' : 'پاسخ'}</button>}
</div> {comment.replies?.map(reply => renderComment(reply, true))}
</div>
);
return (
<section style={{ borderTop: '1px solid #E4DDD2', marginTop: 40, paddingTop: 24, direction: english ? 'ltr' : 'rtl', textAlign: english ? 'left' : 'right' }}>
<h3 style={{ fontSize: 18, fontWeight: 800, color: '#263238', margin: '0 0 16px' }}>{english ? 'Comments' : 'نظرها'}</h3>
{user ? (
<form onSubmit={submit} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 14, marginBottom: 14 }}>
{replyTo && (
<div style={{ fontSize: 12, color: '#0F6B73', marginBottom: 8 }}>
{english ? 'Replying to' : 'پاسخ به'} {replyTo.author?.name}
<button type="button" onClick={() => setReplyTo(null)} style={{ background: 'none', border: 'none', color: '#C94C4C', cursor: 'pointer', marginInlineStart: 8 }}>{english ? 'Cancel' : 'لغو'}</button>
</div>
)}
<textarea value={body} onChange={e => setBody(e.target.value)} maxLength={1000} placeholder={english ? 'Write a comment...' : 'نظر خود را بنویسید...'} style={{ ...inputStyle, minHeight: 90, resize: 'vertical', direction: english ? 'ltr' : 'rtl', textAlign: english ? 'left' : 'right' }} dir={english ? 'ltr' : 'rtl'} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}>
<span style={{ fontSize: 11, color: '#5F6B6D' }}>{numberLabel(body.length, english)} / 1000</span>
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>{english ? 'Submit Comment' : 'ثبت نظر'}</button>
</div>
</form>
) : (
<div style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', borderRadius: 12, padding: 16, color: '#5F6B6D', fontSize: 13 }}>
{english ? 'Sign in to comment.' : 'برای نظر دادن وارد شوید.'}
</div>
)}
{loading && <div style={{ color: '#5F6B6D', fontSize: 13 }}>{english ? 'Loading comments...' : 'در حال بارگذاری نظرها...'}</div>}
{error && <EmptyState title={english ? 'Comments are temporarily unavailable' : 'بخش نظرها موقتاً در دسترس نیست'} subtitle={english ? 'Please try again later' : 'لطفاً بعداً دوباره امتحان کنید'} />}
{!loading && !error && items.length === 0 && <EmptyState title={english ? 'No comments yet' : 'هنوز نظری ثبت نشده است'} />}
{!loading && !error && items.map(comment => renderComment(comment))}
</section>
);
}
function PremiumGateCard({ navigate, user }) {
return (
<div style={{
background: 'linear-gradient(135deg, #0F2A2E 0%, #1A3A40 100%)',
borderRadius: 16, padding: '36px 32px', marginTop: -24, position: 'relative',
border: '1px solid #D49A2A40', direction: 'rtl', textAlign: 'center',
}}>
<div style={{ fontSize: 36, marginBottom: 12 }}><span className="tek-icon" style={{ ['--i']: `url("${crownIcon}")` }} aria-hidden="true" /></div>
<div style={{ fontSize: 18, fontWeight: 700, color: '#D49A2A', marginBottom: 8 }}>این مقاله برای اعضای ویژه</div>
<div style={{ fontSize: 14, color: '#A8B8BA', marginBottom: 24, lineHeight: 1.8 }}>
برای مطالعه کامل این مقاله، عضو ویژه تکناو شوید
</div>
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 28px', display: 'inline-block', textAlign: 'right' }}>
{[
'✓ دسترسی به همه مقالات ویژه',
'✓ تجربه بدون تبلیغ',
'✓ حمایت مستقیم از تکناو',
].map((item) => (
<li key={item} style={{ color: '#C8D8DA', fontSize: 13, marginBottom: 8, lineHeight: 1.6 }}>{item}</li>
))}
</ul>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
<button onClick={() => navigate('/membership')} style={{
background: 'linear-gradient(135deg, #D49A2A, #B8821E)', color: '#fff',
border: 'none', borderRadius: 8, padding: '12px 28px', fontSize: 14, fontWeight: 700,
cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif',
}}>عضویت ویژه</button>
{!user && (
<button onClick={() => navigate('/login')} style={{
background: 'transparent', color: '#A8B8BA',
border: '1px solid #A8B8BA60', borderRadius: 8, padding: '12px 24px', fontSize: 14,
cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif',
}}>ورود به حساب</button>
)}
</div>
</div>
);
}
function ArticleDetail({ slug }) {
const { navigate } = useNav();
const { user } = useAuth();
const toast = useToast();
const contentRef = useRef(null);
const canonicalSlug = slug === 'ai-agents-2026' ? 'agentic-ai-production' : slug;
const [saved, setSaved] = useState(false);
const [listModalOpen, setListModalOpen] = useState(false);
const [reacted, setReacted] = useState({});
const [article, setArticle] = useState(() => contentApi.fallbackArticles().find(a => a.slug === canonicalSlug));
const [related, setRelated] = useState([]);
const [commentTotal, setCommentTotal] = useState(0);
const processedContent = useMemo(() => {
if (!article?.content) return '';
let hidx = 0;
const withLead = article.content.includes('class="lead"')
? article.content
: article.content.replace(/<p(?![^>]*class=)([^>]*)>/, '<p class="lead"$1>');
return withLead.replace(/<h([23])[^>]*>([^<]+)<\/h\1>/g, (_, lvl, txt) => {
const id = 'h-' + hidx++;
return `<h${lvl} id="${id}">${txt}</h${lvl}>`;
});
}, [article?.content]);
useEffect(() => {
let cancelled = false;
if (slug !== canonicalSlug && typeof window !== 'undefined') {
window.history.replaceState({}, '', `/article/${canonicalSlug}`);
}
const fallback = contentApi.fallbackArticles().find(a => a.slug === canonicalSlug);
setArticle(fallback);
setRelated(fallback ? contentApi.fallbackArticles().filter(a => a.category === fallback.category && a.id !== fallback.id).slice(0, 3) : []);
setSaved(false);
setReacted({});
contentApi.getArticle(canonicalSlug)
.then((res) => {
if (cancelled) return;
setArticle(res.article);
setRelated(res.related ?? []);
setSaved(!!res.saved);
setReacted(res.reactions ?? {});
})
.catch(() => {});
return () => { cancelled = true; };
}, [canonicalSlug]);
useEffect(() => {
if (!article?.id) return;
const h = () => {
const el = document.documentElement;
const progress = (el.scrollTop || document.body.scrollTop) / (el.scrollHeight - el.clientHeight);
if (progress > 0.05) {
engagementApi.recordHistory(article.id, Math.min(progress, 1)).catch(() => {});
}
};
window.addEventListener('scroll', h);
return () => window.removeEventListener('scroll', h);
}, [article?.id]);
useEffect(() => {
if (!processedContent) return;
document.querySelectorAll('.article-content pre').forEach((pre) => {
if (pre.querySelector('.code-copy-btn')) return;
const code = pre.querySelector('code');
const lang = code?.className?.match(/language-(\w+)/)?.[1];
if (lang) pre.setAttribute('data-lang', lang);
const btn = document.createElement('button');
btn.className = 'code-copy-btn';
btn.textContent = 'کپی';
btn.onclick = () => {
navigator.clipboard?.writeText(code?.textContent ?? '').then(() => {
btn.textContent = '✓ کپی شد';
setTimeout(() => { btn.textContent = 'کپی'; }, 2000);
}).catch(() => {});
};
pre.appendChild(btn);
});
}, [processedContent]);
if (!article) return (
<div style={{ paddingTop: 100, textAlign: 'center', fontFamily: 'Vazirmatn,sans-serif' }}>
<EmptyState title="مقاله یافت نشد" subtitle="این مقاله وجود ندارد یا حذف شده است" />
<button onClick={() => navigate('/articles')} style={{ ...pillBtn, background: '#0F6B73', color: '#fff', marginTop: 16 }}>بازگشت به مقاله‌ها</button>
</div>
);
const author = article.author || TeknavData.authors.find(a => a.id === article.authorId);
const cat = TeknavData.categories.find(c => c.id === article.category);
const tekRag = isTekRagArticle(article);
const authorName = tekRag ? 'Arsam Sabbagh' : (author?.name || article.authorName);
const authorSpecialty = tekRag ? 'AI and Data Engineering Researcher' : author?.specialty;
const authorBio = tekRag ? 'Arsam Sabbagh is an AI and data engineering researcher focused on retrieval-augmented generation, data-centric AI, Persian technical content systems, and evaluation of grounded language-model applications.' : author?.bio;
const categoryName = tekRag ? 'Data Science' : article.categoryName;
const articleType = tekRag ? 'Research Article' : article.type;
const displayDate = tekRag ? 'May 8, 2026' : articleDateLabel(article);
const statusLabel = tekRag ? 'Published' : article.status;
const layoutDirection = tekRag ? 'ltr' : 'rtl';
const metaLocaleEnglish = tekRag;
const discoverImage = null;
const headings = [];
const h2re = /<h2[^>]*>([^<]+)<\/h2>/g;
const h3re = /<h3[^>]*>([^<]+)<\/h3>/g;
let m;
while ((m = h2re.exec(article.content || '')) !== null) headings.push({ id: 'h-' + headings.length, text: m[1], level: 2 });
while ((m = h3re.exec(article.content || '')) !== null) headings.push({ id: 'h-' + headings.length, text: m[1], level: 3 });
const handleSave = async () => {
try {
const res = await contentApi.toggleSaved(article.id);
setSaved(!!res.saved);
if (res.saved) setListModalOpen(true);
toast(tekRag ? (res.saved ? 'Saved to reading list' : 'Removed from saved articles') : (res.saved ? 'در لیست مطالعه ذخیره شد' : 'از ذخیره‌شده‌ها حذف شد'));
} catch {
toast(tekRag ? 'Sign in to save this article' : 'برای ذخیره مقاله ابتدا وارد شوید', 'warning');
}
};
const handleShare = (channel = 'copy') => {
const url = window.location.href;
const text = article.title;
contentApi.trackEvent({
type: 'share_clicked',
articleId: article.id,
topic: article.category,
metadata: { channel },
}).catch(() => {});
if (channel === 'telegram') {
window.open(`https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`, '_blank', 'noopener,noreferrer');
return;
}
if (channel === 'whatsapp') {
window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${url}`)}`, '_blank', 'noopener,noreferrer');
return;
}
if (channel === 'x') {
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`, '_blank', 'noopener,noreferrer');
return;
}
if (channel === 'linkedin') {
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`, '_blank', 'noopener,noreferrer');
return;
}
navigator.clipboard?.writeText(url).catch(() => {});
toast('لینک مقاله کپی شد');
};
const handleReact = async (type) => {
try {
const res = await contentApi.toggleReaction(article.id, type);
setReacted(r => ({ ...r, [type]: !!res.active }));
if (res.active) toast(tekRag ? 'Reaction saved' : 'واکنش شما ثبت شد');
} catch {
toast(tekRag ? 'Sign in to react' : 'برای ثبت واکنش ابتدا وارد شوید', 'warning');
}
};
return (
<div className={tekRag ? 'article-detail-page tekrag-article-page' : 'article-detail-page'} style={{ paddingTop: 64, background: '#FAF7F0', minHeight: '100vh', fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', overflowX: 'hidden', maxWidth: '100vw' }}>
<ReadingProgress />
<div className="article-breadcrumb" style={{ background: '#F4EFE6', borderBottom: '1px solid #E4DDD2', padding: '12px 24px', direction: layoutDirection, fontSize: 12, color: '#5F6B6D' }}>
<div style={{ maxWidth: 1280, margin: '0 auto', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<button onClick={() => navigate('/')} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#0F6B73', fontSize: 12, fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', padding: 0 }}>{tekRag ? 'Home' : 'خانه'}</button>
<span>›</span>
<button onClick={() => navigate('/category/' + article.category)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#0F6B73', fontSize: 12, fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', padding: 0 }}>{categoryName}</button>
<span>›</span>
<span style={{ color: '#263238', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0, flex: '1 1 auto' }}>{article.title.slice(0, 40)}…</span>
</div>
</div>
<div style={{ maxWidth: tekRag ? 1360 : 1280, margin: '0 auto', padding: tekRag ? '44px 24px 64px' : '40px 24px', display: 'grid', gridTemplateColumns: tekRag ? '220px minmax(0, 860px)' : '200px 1fr', justifyContent: tekRag ? 'center' : 'initial', gap: tekRag ? 52 : 48, direction: layoutDirection, overflowX: 'hidden', minWidth: 0 }} className="article-layout">
<aside className="toc-sidebar" role="complementary" aria-label={tekRag ? 'Table of contents' : 'فهرست مطالب'}>
{headings.length > 0 && <TableOfContents headings={headings} english={tekRag} />}
</aside>
<article className="article-main" style={{ maxWidth: tekRag ? 860 : 760, direction: layoutDirection, textAlign: tekRag ? 'left' : 'right', minWidth: 0, overflowX: 'hidden' }}>
<header className="article-header" style={{ marginBottom: 32, paddingBottom: 24, borderBottom: '1px solid #E4DDD2' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
<TypeBadge type={articleType} />
<CategoryBadge name={categoryName} color={cat?.color} />
{tekRag ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 20, fontSize: 11, fontWeight: 600, background: '#2F8F6B15', color: '#2F8F6B', border: '1px solid #2F8F6B30' }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#2F8F6B' }} />
{statusLabel}
</span>
) : <StatusBadge status={article.status} />}
{article.sponsored && <span style={{ background: '#FFF4D8', color: '#8A5A00', border: '1px solid #D49A2A55', borderRadius: 6, padding: '5px 10px', fontSize: 11, fontWeight: 800 }}>Promoted Content</span>}
{article.premiumOnly && <span style={{ background: '#0F2A2E', color: '#D49A2A', borderRadius: 6, padding: '5px 10px', fontSize: 11, fontWeight: 800 }}>Premium</span>}
</div>
<h1 style={{ fontSize: 'clamp(22px,4vw,34px)', fontWeight: 900, color: '#263238', margin: '0 0 12px', lineHeight: 1.4, fontFamily: 'Vazirmatn,sans-serif', wordWrap: 'break-word', overflowWrap: 'break-word', hyphens: 'auto', maxWidth: '100%' }}>{article.title}</h1>
{article.subtitle && <p style={{ fontSize: 18, color: '#5F6B6D', margin: '0 0 20px', lineHeight: 1.6, fontWeight: 400 }}>{article.subtitle}</p>}
<div className="article-byline-row" style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
{author && (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<AuthorAvatar author={author} size={40} />
<div>
<button
onClick={() => article.authorUsername && navigate('/profile/@' + article.authorUsername)}
style={{ background: 'none', border: 'none', padding: 0, cursor: article.authorUsername ? 'pointer' : 'default', fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', fontSize: 14, fontWeight: 700, color: article.authorUsername ? '#0F6B73' : '#263238' }}
aria-label={tekRag ? `Author: ${authorName}` : `نویسنده: ${authorName}`}
>
{authorName}
{author.verifiedExpert && <span style={{ marginInlineStart: 6, fontSize: 11, color: '#0F6B73', background: '#0F6B7314', border: '1px solid #0F6B7330', borderRadius: 999, padding: '1px 6px' }}>{tekRag ? 'Verified' : 'تاییدشده'}</span>}
</button>
<div style={{ fontSize: 12, color: '#5F6B6D' }}>{authorSpecialty}</div>
</div>
</div>
)}
<div className="article-meta-line" style={{ fontSize: 13, color: '#5F6B6D', display: 'flex', gap: 12, minWidth: 0, flex: '1 1 auto' }}>
<time dateTime={article.dateEn}>{displayDate}</time>
<span>·</span>
<span style={{ color: '#C46A4D', fontWeight: 600, fontSize: 11 }}>{articleType}</span>
<span>·</span>
<span>{tekRag ? `${article.readTime} min read` : `${article.readTime} دقیقه مطالعه`}</span>
<span>·</span>
<span>{tekRag ? `${numberLabel(commentTotal, true)} comments` : `${numberLabel(commentTotal)} نظر`}</span>
</div>
</div>
<div className="article-action-row" style={{ display: 'flex', gap: 10, marginTop: 16 }}>
<button onClick={handleSave} style={{ ...pillBtn, background: saved ? '#0F6B7318' : '#F4EFE6', color: saved ? '#0F6B73' : '#263238', border: saved ? '1px solid #0F6B7340' : '1px solid #E4DDD2', padding: '6px 14px' }}>
{saved ? (tekRag ? 'Saved' : '✓ ذخیره‌شده') : (tekRag ? 'Save' : '⊕ ذخیره')}
</button>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', borderInlineStart: '1px solid #E4DDD2', paddingInlineStart: 10 }}>
<button onClick={() => handleShare('telegram')} title="Telegram" style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 6, display: 'flex', color: '#0088cc' }} aria-label={tekRag ? 'Share on Telegram' : 'اشتراک در تلگرام'}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221l-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.446 1.394c-.14.18-.357.223-.548.223l.188-2.85 5.18-4.686c.223-.198-.054-.31-.346-.11l-6.4 4.02-2.76-.86c-.6-.188-.61-.6.126-.894l10.78-4.148c.5-.188.94.116.808.843z"/></svg>
</button>
<button onClick={() => handleShare('whatsapp')} title="WhatsApp" style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 6, display: 'flex', color: '#25D366' }} aria-label={tekRag ? 'Share on WhatsApp' : 'اشتراک در واتساپ'}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12.031 0C5.412 0 0 5.414 0 12.035c0 2.124.553 4.195 1.603 6.01L.063 23.575l5.666-1.486c1.765.952 3.738 1.455 5.765 1.455 6.617 0 12.03-5.412 12.03-12.034S18.647 0 12.031 0zm0 21.601c-1.782 0-3.52-.48-5.04-1.385l-.36-.215-3.75.983.998-3.66-.236-.375C2.69 15.352 2.15 13.722 2.15 12.035 2.15 6.593 6.577 2.15 12.03 2.15s9.88 4.443 9.88 9.885c0 5.44-4.428 9.881-9.88 9.881zm5.417-7.41c-.297-.15-1.762-.871-2.036-.973-.274-.102-.473-.15-.672.15-.202.3-.77 .973-.944 1.173-.175.2-.35.225-.648.075-.297-.15-1.258-.464-2.395-1.405-.885-.733-1.482-1.64-1.657-1.94-.175-.3 0-.462.15-.612.134-.135.297-.35.446-.525.15-.175.2-.3.297-.5.102-.2.05-.375-.025-.525-.075-.15-.672-1.618-.92-2.215-.24-.582-.483-.502-.67-.512-.174-.01-.373-.01-.57-.01s-.52.075-.794.375c-.274.3-1.045 1.025-1.045 2.5 0 1.475 1.07 2.9 1.22 3.1.15.2 2.115 3.226 5.126 4.526 2.053.886 2.85 1.018 3.86 1.045.894.025 2.87-.923 3.27-1.848.397-.925.397-1.722.274-1.895-.125-.175-.472-.275-.77-.425z"/></svg>
</button>
<button onClick={() => handleShare('x')} title="X (Twitter)" style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 6, display: 'flex', color: '#14171A' }} aria-label={tekRag ? 'Share on X' : 'اشتراک در X'}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</button>
<button onClick={() => handleShare('linkedin')} title="LinkedIn" style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 6, display: 'flex', color: '#0A66C2' }} aria-label={tekRag ? 'Share on LinkedIn' : 'اشتراک در لینکدین'}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</button>
<button onClick={() => handleShare('copy')} title={tekRag ? 'Copy link' : 'کپی لینک'} style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 6, display: 'flex', color: '#5F6B6D' }} aria-label={tekRag ? 'Copy article link' : 'کپی لینک مقاله'}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</div>
</header>
{article.series && (
<div style={{ background: '#0F6B7308', border: '1px solid #0F6B7330', borderRadius: 10, padding: 16, marginBottom: 28, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ background: '#0F6B73', color: '#fff', fontSize: 11, padding: '3px 8px', borderRadius: 6, fontWeight: 700 }}>سری مقاله</span>
<span style={{ fontSize: 14, fontWeight: 700, color: '#263238' }}>{article.series.title}</span>
{article.series.articles?.length > 0 && (
<span style={{ fontSize: 11, color: '#5F6B6D' }}>قسمت {article.series.position} از {article.series.articles.length}</span>
)}
</div>
<button onClick={() => navigate('/series/' + article.series.slug)} style={{ background: 'transparent', border: 'none', color: '#0F6B73', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif' }}>مشاهده همه قسمت‌ها ←</button>
</div>
)}
{discoverImage && (
<figure className="article-discover-hero" style={{ margin: '0 0 28px', borderRadius: 12, overflow: 'hidden', border: '1px solid #E4DDD2', background: '#F4EFE6' }}>
<img src={discoverImage} alt={article.ogTitle || article.title} width="1200" height="630" loading="eager" fetchPriority="high" decoding="async" style={{ display: 'block', width: '100%', aspectRatio: '1200 / 630', objectFit: 'cover' }} />
</figure>
)}
{(article.correctionNotice || article.corrections?.length > 0) && (
<section style={{ background: '#FFF8EA', border: '1px solid #D49A2A55', borderRadius: 12, padding: 18, marginBottom: 28, direction: 'rtl' }}>
<div style={{ fontSize: 14, fontWeight: 900, color: '#263238', marginBottom: 8 }}>اصلاحیه و به‌روزرسانی</div>
{article.correctionNotice && <p style={{ margin: '0 0 10px', color: '#5F6B6D', lineHeight: 1.9 }}>{article.correctionNotice}</p>}
{article.corrections?.length > 0 && (
<ul style={{ margin: 0, paddingRight: 18, color: '#5F6B6D', lineHeight: 1.9, fontSize: 13 }}>
{article.corrections.map(item => <li key={item.id}>{item.date || new Date(item.createdAt).toLocaleDateString('fa-IR')}: {item.note}</li>)}
</ul>
)}
</section>
)}
{!tekRag && (
<div className="article-hero-diagram" style={{ background: '#F4EFE6', borderRadius: 12, padding: 24, marginBottom: 32, overflow: 'hidden', maxWidth: '100%', overflowX: 'auto' }}>
<div style={{ fontSize: 11, color: '#5F6B6D', marginBottom: 8, fontWeight: 600 }}>نمودار تعاملی — با موس روی عناصر بروید</div>
<LazyDiagram type={article.diagram} />
</div>
)}
<div style={{ position: 'relative' }}>
<div className="article-content" ref={contentRef} style={{
...articleContentStyle,
...(article.premiumLocked ? {
maskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
maxHeight: 320, overflow: 'hidden',
} : {}),
maxWidth: '100%', overflowX: 'hidden', wordWrap: 'break-word', overflowWrap: 'break-word',
}} dangerouslySetInnerHTML={{ __html: processedContent }} />
{!article.premiumLocked && !tekRag && (
<QuoteSharer containerRef={contentRef} article={article} />
)}
</div>
{article.premiumLocked && <PremiumGateCard navigate={navigate} user={user} />}
<ArticleEmbeddedDiagrams slug={article.slug} />
<div style={{ borderTop: '1px solid #E4DDD2', marginTop: 40, paddingTop: 24, direction: layoutDirection, textAlign: tekRag ? 'left' : 'right' }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#263238', marginBottom: 16 }}>{tekRag ? 'Was this article useful?' : 'واکنش شما به این مقاله؟'}</div>
<div className="article-reaction-row" style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{(tekRag ? [
['helpful', 'Helpful', thumbsUpIcon],
['star', 'Excellent', starIcon],
['heart', 'Valuable', '♡'],
['fire', 'Important', '●'],
['thinking', 'Thought-provoking', '?'],
] : [
['helpful', 'مفید', thumbsUpIcon],
['star', 'عالی', starIcon],
['heart', 'عاشقانه', heartIcon],
['fire', 'داغ', flameIcon],
['thinking', 'تأمل‌برانگیز', lightbulbIcon],
]).map(([type, label, icon]) => (
<button key={type} onClick={() => handleReact(type)} style={{
...pillBtn, background: reacted[type] ? '#0F6B7318' : '#F4EFE6',
color: reacted[type] ? '#0F6B73' : '#263238',
border: `1px solid ${reacted[type] ? '#0F6B7340' : '#E4DDD2'}`,
fontSize: 13, padding: '8px 14px',
}}>{iconNode(icon)} {label}</button>
))}
</div>
</div>
{article.series?.articles?.length > 1 && (
<SeriesNavBanner series={article.series} currentSlug={article.slug} />
)}
{article.tags?.length > 0 && (
<div style={{ marginTop: 24, direction: layoutDirection, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>{tekRag ? 'Keywords:' : 'برچسب‌ها:'}</span>
{article.tags.map(t => (
<button key={t} onClick={() => navigate(`/tag/${encodeURIComponent(t)}`)} style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', padding: '3px 10px', borderRadius: 4, fontSize: 12, color: '#5F6B6D', cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif' }}>{t}</button>
))}
</div>
)}
<ArticleCommentSection article={article} onTotal={setCommentTotal} english={tekRag} />
<ArticleQaSection slug={article.slug} english={tekRag} />
{author && (
<div className="article-author-box" style={{ background: '#F4EFE6', borderRadius: 12, padding: 24, marginTop: 32, direction: layoutDirection, textAlign: tekRag ? 'left' : 'right', display: 'flex', gap: 16 }}>
<AuthorAvatar author={author} size={64} />
<div>
<div style={{ fontSize: 11, color: '#5F6B6D', marginBottom: 4 }}>{tekRag ? 'AUTHOR' : 'نویسنده'}</div>
<button
onClick={() => article.authorUsername && navigate('/profile/@' + article.authorUsername)}
style={{ background: 'none', border: 'none', padding: 0, cursor: article.authorUsername ? 'pointer' : 'default', fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', fontSize: 16, fontWeight: 700, color: article.authorUsername ? '#0F6B73' : '#263238', marginBottom: 6 }}
>
{authorName}
{author.verifiedExpert && <span style={{ marginInlineStart: 6, fontSize: 11, color: '#0F6B73', background: '#fff', border: '1px solid #0F6B7330', borderRadius: 999, padding: '1px 6px' }}>{tekRag ? 'Verified' : 'تاییدشده'}</span>}
</button>
<div style={{ fontSize: 12, color: '#0F6B73', fontWeight: 600, marginBottom: 6, fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif' }}>{authorSpecialty}</div>
<div style={{ fontSize: 13, color: '#5F6B6D', lineHeight: 1.7, fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif' }}>{authorBio}</div>
<div className="article-social-row" style={{ marginTop: 10, display: 'flex', gap: 8 }}>
{Object.entries(author.social || {}).map(([k, v]) => (
<a key={k} href={v && v !== '#' ? v : undefined} target={v && v !== '#' ? '_blank' : undefined} rel="noopener noreferrer"
style={{ ...pillBtn, background: '#fff', color: '#0F6B73', border: '1px solid #0F6B7330', fontSize: 12, padding: '4px 12px', textDecoration: 'none', fontFamily: tekRag ? 'Inter, Arial, sans-serif' : 'Vazirmatn,sans-serif', cursor: 'pointer' }}>{k}</a>
))}
</div>
</div>
</div>
)}
{!tekRag && related.length > 0 && (
<div className="article-related" style={{ marginTop: 48, direction: 'rtl' }}>
<h3 style={{ fontSize: 18, fontWeight: 700, color: '#263238', marginBottom: 20 }}>مقاله‌های مرتبط</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(220px,1fr))', gap: 16 }}>
{related.map(a => <ArticleCard key={a.id} article={a} />)}
</div>
</div>
)}
{!tekRag && (() => {
const relatedComparisons = (TeknavData.comparisons ?? []).filter((c) =>
(c.relatedArticles || []).includes(article.slug) ||
(article.tags || []).some((t) => [c.item1.slug, c.item2.slug, ...(c.tags || [])].some((ct) => ct.toLowerCase() === t.toLowerCase())),
).slice(0, 3);
if (!relatedComparisons.length) return null;
return (
<div style={{ marginTop: 40, direction: 'rtl' }}>
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#263238', marginBottom: 14 }}>مقایسه‌های مرتبط</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{relatedComparisons.map((c) => (
<div
key={c.slug}
onClick={() => navigate(`/compare/${c.slug}`)}
style={{ background: '#F8F5F0', border: '1px solid #E4DDD2', borderRadius: 8, padding: '12px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12 }}
>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: '#0C1E22', marginBottom: 4 }}>{c.title}</div>
<div style={{ display: 'flex', gap: 8, fontSize: 12 }}>
<span style={{ background: '#E8F5E9', color: '#2E7D32', padding: '1px 8px', borderRadius: 20, fontWeight: 600 }}>{c.item1.name}</span>
<span style={{ color: '#5F6B6D' }}>در برابر</span>
<span style={{ background: '#FFF3E0', color: '#E65100', padding: '1px 8px', borderRadius: 20, fontWeight: 600 }}>{c.item2.name}</span>
</div>
</div>
<span style={{ color: '#0F6B73', fontSize: 18 }}>←</span>
</div>
))}
</div>
</div>
);
})()}
</article>
</div>
<style>{`
.article-layout { grid-template-columns: 200px 1fr; }
.toc-sidebar { display: block; }
.tekrag-article-page { scroll-behavior: smooth; }
.tekrag-article-page .toc-sidebar { position: sticky; top: 88px; align-self: start; max-height: calc(100vh - 110px); overflow: auto; scrollbar-width: thin; }
.tekrag-article-page .article-main { width: 100%; }
.tekrag-article-page .article-content { direction: ltr !important; text-align: left !important; }
.tekrag-article-page .article-content h2,
.tekrag-article-page .article-content h3,
.tekrag-article-page .article-content p,
.tekrag-article-page .article-content li,
.tekrag-article-page .article-content td,
.tekrag-article-page .article-content th { direction: ltr !important; text-align: left !important; }
.tekrag-article-page .article-content ul,
.tekrag-article-page .article-content ol { padding-left: 24px !important; padding-right: 0 !important; }
.tekrag-article-page .article-content ul li,
.tekrag-article-page .article-content ol li { padding-left: 28px !important; padding-right: 0 !important; }
.tekrag-article-page .article-content ul li::before { left: 4px !important; right: auto !important; }
.tekrag-article-page .article-content ol li::before { left: 0 !important; right: auto !important; }
.tekrag-article-page .article-content blockquote { border-left: 4px solid #D49A2A !important; border-right: 0 !important; border-radius: 8px 0 0 8px !important; }
.tekrag-article-page .tekrag-diagram-section { display: grid; gap: 28px; margin: 46px auto 0; width: min(100%, 860px); justify-items: center; }
.tekrag-article-page .tekrag-diagram-card { width: min(100%, 820px); margin-inline: auto; padding: 24px; border-radius: 10px; }
.tekrag-article-page .tekrag-3d-canvas { min-height: clamp(280px, 44vw, 420px) !important; touch-action: pan-y; }
.tekrag-article-page .tekrag-3d-overlay { justify-content: center !important; text-align: center; }
.tekrag-article-page .tekrag-3d-overlay > div { justify-content: center; }
.tekrag-article-page .article-action-row,
.tekrag-article-page .article-reaction-row { justify-content: flex-start; }
@media (max-width: 900px) {
.article-layout { grid-template-columns: 1fr !important; }
.toc-sidebar { display: none !important; }
.tekrag-article-page .article-layout { padding: 28px 16px 48px !important; }
.tekrag-article-page .article-main { max-width: 100% !important; }
.tekrag-article-page .tekrag-diagram-section { gap: 22px; margin-top: 34px; }
.tekrag-article-page .tekrag-diagram-card { padding: 16px; }
.tekrag-article-page .tekrag-3d-canvas { min-height: 300px !important; }
}
@media (max-width: 560px) {
.tekrag-article-page .article-breadcrumb { padding-inline: 16px !important; }
.tekrag-article-page .article-byline-row,
.tekrag-article-page .article-meta-line,
.tekrag-article-page .article-action-row { align-items: flex-start !important; }
.tekrag-article-page .article-meta-line { gap: 8px !important; flex-wrap: wrap; }
.tekrag-article-page .article-author-box { flex-direction: column; }
.tekrag-article-page .tekrag-3d-canvas { min-height: 330px !important; }
}
`}</style>
{listModalOpen && <ReadingListModal articleId={article.id} onDone={() => setListModalOpen(false)} />}
</div>
);
}
const articleContentStyle = {
fontSize: 16, lineHeight: 2, color: '#263238', direction: 'rtl',
fontFamily: 'Vazirmatn,sans-serif',
};
const articleCSS = `
article h2 { font-size: 22px; font-weight: 800; color: #263238; margin: 40px 0 16px; padding-bottom: 8px; border-bottom: 2px solid #E4DDD2; font-family: Vazirmatn,sans-serif; }
article h3 { font-size: 18px; font-weight: 700; color: #263238; margin: 32px 0 12px; font-family: Vazirmatn,sans-serif; }
article p { margin: 0 0 20px; }
article .lead { font-size: 18px; line-height: 2.05; color: #1A3035; font-weight: 500; background: #FFFFFF; border: 1px solid #E4DDD2; border-right: 4px solid #0F6B73; border-radius: 12px; padding: 20px 22px; margin: 0 0 30px; }
article blockquote { border-right: 4px solid #D49A2A; margin: 28px 0; padding: 16px 20px; background: #D49A2A0A; border-radius: 0 8px 8px 0; font-size: 16px; color: #263238; font-style: normal; }
article blockquote.security-quote { border-right-color: #C94C4D; background: #C94C4D08; }
article strong { color: #263238; font-weight: 700; }
article em { color: #0F6B73; font-style: normal; font-weight: 600; }
article pre { background: #131F22; border-radius: 10px; padding: 40px 18px 18px; margin: 24px 0; overflow-x: auto; position: relative; border: 1px solid #1E3035; direction: ltr; }
article pre::before { content: attr(data-lang); position: absolute; top: 10px; right: 14px; font-size: 10px; color: #4A7A82; font-family: monospace; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600; }
article pre code { background: none !important; border: none !important; padding: 0 !important; color: #A8D8DC; font-family: 'Courier New', Consolas, monospace; font-size: 13.5px; line-height: 1.85; display: block; direction: ltr; text-align: left; white-space: pre; }
article code:not(pre code) { background: #0F6B7316; border: 1px solid #0F6B7328; border-radius: 4px; padding: 1px 6px; font-family: 'Courier New', Consolas, monospace; font-size: 13px; color: #0A5060; direction: ltr; unicode-bidi: isolate; display: inline; white-space: nowrap; }
.code-copy-btn { position: absolute; top: 8px; left: 10px; background: #1E3035; border: 1px solid #2A4048; color: #6AACB5; border-radius: 6px; padding: 4px 10px; font-size: 11px; cursor: pointer; font-family: Vazirmatn, sans-serif; z-index: 1; transition: all 0.15s; line-height: 1.4; }
.code-copy-btn:hover { background: #2A4048; color: #A8D8DC; }
article ul { padding-right: 0; list-style: none; margin: 16px 0 24px; display: grid; gap: 6px; }
article ul li { position: relative; padding-right: 22px; color: #263238; line-height: 1.9; }
article ul li::before { content: '›'; position: absolute; right: 4px; color: #0F6B73; font-weight: 900; font-size: 17px; line-height: 1.6; }
article ol { padding-right: 0; counter-reset: art-counter; list-style: none; margin: 16px 0 24px; display: grid; gap: 8px; }
article ol li { counter-increment: art-counter; position: relative; padding-right: 38px; color: #263238; line-height: 1.9; padding-top: 2px; }
article ol li::before { content: counter(art-counter); position: absolute; right: 0; top: 2px; width: 24px; height: 24px; background: #0F6B73; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; }
article ul li code, article ol li code { font-size: 12px; }
article ul li strong, article ol li strong { color: #0A5060; }
article .ltr { direction: ltr; unicode-bidi: isolate; display: inline-block; font-family: Inter, Arial, sans-serif; letter-spacing: 0; }
article .metric-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(150px,1fr)); gap: 12px; margin: 28px 0; }
article .metric-card { background: #fff; border: 1px solid #E4DDD2; border-top: 3px solid #0F6B73; border-radius: 10px; padding: 14px 16px; }
article .metric-value { color: #0F6B73; font-weight: 900; font-size: 22px; line-height: 1.2; }
article .metric-label { color: #5F6B6D; font-size: 12px; line-height: 1.7; margin-top: 6px; }
article table { width: 100%; border-collapse: collapse; margin: 24px 0; background: #fff; border: 1px solid #E4DDD2; border-radius: 10px; overflow: hidden; display: block; max-width: 100%; overflow-x: auto; }
article th, article td { border-bottom: 1px solid #E4DDD2; padding: 12px 14px; text-align: right; vertical-align: top; min-width: 130px; }
article th { background: #F4EFE6; color: #263238; font-weight: 800; }
article tr:last-child td { border-bottom: 0; }
article .comparison-template { background: #fffaf1; border: 1px solid #E4DDD2; border-radius: 12px; padding: 18px; margin: 24px 0; }
article .risk-box { background: #C46A4D10; border: 1px solid #C46A4D35; border-radius: 12px; padding: 18px 22px; margin: 28px 0; }
article .agent-steps { counter-reset: agentStep; display: grid; gap: 10px; margin: 24px 0; }
article .agent-step { counter-increment: agentStep; background: #fff; border: 1px solid #E4DDD2; border-radius: 10px; padding: 14px 48px 14px 16px; position: relative; }
article .agent-step::before { content: counter(agentStep); position: absolute; top: 14px; right: 14px; width: 24px; height: 24px; border-radius: 50%; background: #0F6B73; color: #fff; display: grid; place-items: center; font-size: 12px; font-weight: 800; }
.article-diagram-card { background: linear-gradient(135deg,#F4EFE6 0%,#FFFFFF 100%); border: 1px solid #E4DDD2; border-radius: 12px; padding: 22px; overflow: hidden; box-shadow: 0 12px 34px rgba(15,42,46,0.06); }
.tekrag-diagram-card { background: #FAFAFA; border: 1px solid #E8E8E8; border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.insight-box { background: #0F6B7310; border: 1px solid #0F6B7330; border-radius: 10px; padding: 20px 24px; margin: 28px 0; }
.insight-box .insight-title { font-size: 13px; font-weight: 700; color: #0F6B73; margin-bottom: 8px; }
.insight-box p { margin: 0; font-size: 14px; line-height: 1.8; }
.footnote-box { background: #F4EFE6; border-radius: 8px; padding: 16px 20px; margin-top: 32px; font-size: 12px; color: #5F6B6D; line-height: 1.7; }
.cve-summary-card { background: linear-gradient(135deg, #EEF6F7 0%, #E8F3F4 100%); border: 1px solid rgba(15,107,115,0.18); border-radius: 12px; padding: 20px 24px; margin: 0 0 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; direction: rtl; }
.cve-summary-card .cve-item { display: flex; flex-direction: column; gap: 5px; border-left: 1px solid rgba(15,107,115,0.15); padding-left: 16px; }
.cve-summary-card .cve-item:last-child { border-left: none; padding-left: 0; }
.cve-summary-card .cve-label { font-size: 10px; color: #0F6B73; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
.cve-summary-card .cve-value { font-size: 14px; font-weight: 800; color: #1A3035; }
.cve-summary-card .cve-score { font-size: 28px; font-weight: 900; color: #C0392B; line-height: 1; }
.cve-summary-card .cve-critical { color: #C0392B; }
.cve-summary-card .cve-patched { color: #1A7A50; font-size: 13px; }
.security-alert { background: #C94C4D0A; border: 1px solid #C94C4D30; border-right: 4px solid #C94C4D; border-radius: 0 10px 10px 0; padding: 16px 20px; margin: 28px 0; }
.security-alert .alert-title { font-size: 13px; font-weight: 800; color: #C94C4D; margin-bottom: 6px; }
.security-alert p, .security-alert li { font-size: 14px; line-height: 1.8; color: #3A1A1A; margin: 0; }
@media (max-width: 640px) {
article pre {
padding: 32px 12px 12px !important;
margin: 20px 0 !important;
font-size: 12px !important;
max-width: calc(100vw - 32px) !important;
}
article code:not(pre code) {
font-size: 11px !important;
padding: 1px 4px !important;
word-break: break-all !important;
}
.cve-summary-card {
grid-template-columns: 1fr !important;
padding: 16px !important;
gap: 12px !important;
}
.cve-summary-card .cve-item {
border-left: none !important;
border-bottom: 1px solid rgba(15,107,115,0.15) !important;
padding-left: 0 !important;
padding-bottom: 12px !important;
}
.cve-summary-card .cve-item:last-child {
border-bottom: none !important;
padding-bottom: 0 !important;
}
article table { font-size: 13px !important; }
article table td, article table th { padding: 6px 8px !important; min-width: 80px !important; }
article .metric-grid { grid-template-columns: 1fr !important; }
article .lead { font-size: 16px !important; padding: 16px !important; }
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = articleCSS;
document.head.appendChild(styleEl);
export { ArticleRow, FilterBar, ArticleListPage, TableOfContents, ArticleDetail };