feat: optimize docs pages and add 4 new doc articles (en + zh)
- Rewrote DocsListPage and DocDetailPage with landing.css aesthetic (icon cards, skeleton loader, prose styles, CTA box) - Added docs-specific CSS to landing.css - Created image-to-latex, copy-to-word, ocr-accuracy, pdf-extraction articles in both English and Chinese - Updated DocsSeoSection guide cards to link to real doc slugs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const GUIDES = [
|
||||
{
|
||||
slug: 'image-to-latex',
|
||||
svgPaths: (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
@@ -15,6 +16,7 @@ const GUIDES = [
|
||||
metaZh: '5 分钟 · 最受欢迎',
|
||||
},
|
||||
{
|
||||
slug: 'copy-to-word',
|
||||
svgPaths: (
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
@@ -27,6 +29,7 @@ const GUIDES = [
|
||||
metaZh: '4 分钟 · 扩展用户',
|
||||
},
|
||||
{
|
||||
slug: 'ocr-accuracy',
|
||||
svgPaths: (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
@@ -39,6 +42,7 @@ const GUIDES = [
|
||||
metaZh: '6 分钟 · 进阶用户',
|
||||
},
|
||||
{
|
||||
slug: 'pdf-extraction',
|
||||
svgPaths: (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
@@ -71,7 +75,7 @@ export default function DocsSeoSection() {
|
||||
|
||||
<div className="doc-cards reveal">
|
||||
{GUIDES.map((g, i) => (
|
||||
<Link key={i} to="/docs" className="doc-card">
|
||||
<Link key={i} to={`/docs/${g.slug}`} className="doc-card">
|
||||
<div className="doc-card-left">
|
||||
<div className="doc-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
|
||||
@@ -1,35 +1,78 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import SEOHead from '../components/seo/SEOHead';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { loadContent, type ContentItem } from '../lib/content';
|
||||
|
||||
function estimateReadTime(html: string): number {
|
||||
const text = html.replace(/<[^>]+>/g, '');
|
||||
const words = text.split(/\s+/).filter(Boolean).length;
|
||||
return Math.max(2, Math.round(words / 200));
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string, lang: string): string {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export default function DocDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { language } = useLanguage();
|
||||
const [content, setContent] = useState<ContentItem | null>(null);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setContent(null);
|
||||
setNotFound(false);
|
||||
if (slug) {
|
||||
loadContent('docs', language, slug)
|
||||
.then(setContent)
|
||||
.catch(() => setContent(null));
|
||||
.catch(() => setNotFound(true));
|
||||
}
|
||||
}, [slug, language]);
|
||||
|
||||
if (!content) {
|
||||
const zh = language === 'zh';
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto py-20 px-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-cream-200 rounded w-24" />
|
||||
<div className="h-8 bg-cream-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-cream-200 rounded w-full" />
|
||||
<div className="docs-detail">
|
||||
<Link to="/docs" className="docs-back-link">
|
||||
<svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6" /></svg>
|
||||
{zh ? '所有文档' : 'All docs'}
|
||||
</Link>
|
||||
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-muted)' }}>
|
||||
<p style={{ fontSize: '18px', marginBottom: '8px' }}>
|
||||
{zh ? '文档未找到' : 'Doc not found'}
|
||||
</p>
|
||||
<Link to="/docs" style={{ color: 'var(--primary)', fontSize: '14px' }}>
|
||||
{zh ? '返回文档中心 →' : 'Back to docs →'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return (
|
||||
<div className="docs-skeleton-wrap">
|
||||
<div className="skeleton-line" style={{ width: '80px', height: '14px', marginBottom: '44px' }} />
|
||||
<div className="skeleton-line" style={{ width: '60%', height: '44px', marginBottom: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '40%', height: '16px', marginBottom: '48px' }} />
|
||||
<div className="skeleton-line" style={{ width: '100%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '92%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '96%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '80%', height: '16px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const readTime = estimateReadTime(content.html);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead
|
||||
@@ -37,29 +80,54 @@ export default function DocDetailPage() {
|
||||
description={content.meta.description}
|
||||
path={`/docs/${slug}`}
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto py-16 lg:py-20 px-6">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
to="/docs"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-ink transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
{language === 'en' ? 'All docs' : '所有文档'}
|
||||
|
||||
<div className="docs-detail">
|
||||
{/* Back */}
|
||||
<Link to="/docs" className="docs-back-link">
|
||||
<svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6" /></svg>
|
||||
{zh ? '所有文档' : 'All docs'}
|
||||
</Link>
|
||||
|
||||
{/* Doc body */}
|
||||
<article
|
||||
className="prose prose-lg prose-warm max-w-none
|
||||
prose-headings:font-display prose-headings:tracking-tight
|
||||
prose-h1:text-3xl prose-h1:font-bold
|
||||
prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10
|
||||
prose-a:text-sage-700 prose-a:no-underline hover:prose-a:underline
|
||||
prose-code:text-sage-700 prose-code:bg-sage-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-sm
|
||||
prose-pre:bg-ink prose-pre:text-cream-100 prose-pre:rounded-xl
|
||||
prose-img:rounded-xl
|
||||
prose-blockquote:border-sage-300 prose-blockquote:bg-sage-50/30 prose-blockquote:rounded-r-xl prose-blockquote:py-1"
|
||||
{/* Article header */}
|
||||
<div className="docs-article-header">
|
||||
{content.meta.tags.length > 0 && (
|
||||
<div className="docs-article-tags">
|
||||
{content.meta.tags.map(tag => (
|
||||
<span key={tag} className="docs-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<h1 className="docs-article-h1">{content.meta.title}</h1>
|
||||
<div className="docs-meta-row">
|
||||
<span>{formatDate(content.meta.date, language)}</span>
|
||||
<span className="docs-meta-sep">·</span>
|
||||
<span>{readTime} {zh ? '分钟阅读' : 'min read'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article body */}
|
||||
<div
|
||||
className="docs-prose"
|
||||
dangerouslySetInnerHTML={{ __html: content.html }}
|
||||
/>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="docs-cta-box">
|
||||
<div className="docs-cta-title">
|
||||
{zh ? '准备好试试了吗?' : 'Ready to try it yourself?'}
|
||||
</div>
|
||||
<p className="docs-cta-desc">
|
||||
{zh
|
||||
? '上传一张公式图片,秒级获得 LaTeX 输出——无需注册。'
|
||||
: 'Upload a formula image and get LaTeX output in under a second — no sign-up needed.'}
|
||||
</p>
|
||||
<Link to="/app" className="btn btn-primary" style={{ display: 'inline-flex' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M5 3l14 9-14 9V3z" />
|
||||
</svg>
|
||||
{zh ? '免费试用 TexPixel' : 'Try TexPixel Free'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,69 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowRight, BookOpen, FileText } from 'lucide-react';
|
||||
import SEOHead from '../components/seo/SEOHead';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { loadManifest, type ContentMeta } from '../lib/content';
|
||||
|
||||
function estimateReadTime(desc: string): number {
|
||||
return Math.max(2, Math.round(desc.split(/\s+/).length / 3));
|
||||
}
|
||||
|
||||
const DOC_ICONS: Record<string, JSX.Element> = {
|
||||
'getting-started': (
|
||||
<>
|
||||
<path d="M5 3l14 9-14 9V3z" />
|
||||
</>
|
||||
),
|
||||
'image-to-latex': (
|
||||
<>
|
||||
<rect x="3" y="3" width="18" height="14" rx="2" />
|
||||
<path d="M3 9h18" />
|
||||
<path d="M9 21l3-3 3 3" />
|
||||
</>
|
||||
),
|
||||
'supported-formats': (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M8 13h8M8 17h5" />
|
||||
</>
|
||||
),
|
||||
'copy-to-word': (
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<path d="M8 9h8M8 12h6M8 15h4" />
|
||||
</>
|
||||
),
|
||||
'ocr-accuracy': (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</>
|
||||
),
|
||||
'pdf-extraction': (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<path d="M14 2v6h6M10 12l2 2 4-4" />
|
||||
</>
|
||||
),
|
||||
faq: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
function DocIcon({ slug }: { slug: string }) {
|
||||
const paths = DOC_ICONS[slug] ?? DOC_ICONS['supported-formats'];
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
{paths}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocsListPage() {
|
||||
const { language } = useLanguage();
|
||||
const [docs, setDocs] = useState<ContentMeta[]>([]);
|
||||
@@ -15,57 +74,63 @@ export default function DocsListPage() {
|
||||
});
|
||||
}, [language]);
|
||||
|
||||
const zh = language === 'zh';
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
|
||||
<SEOHead
|
||||
title={zh ? 'TexPixel 文档中心' : 'TexPixel Documentation'}
|
||||
description={zh ? '公式识别入门指南、格式说明与常见问题。' : 'Guides, format references, and answers for getting the most out of TexPixel.'}
|
||||
path="/docs"
|
||||
/>
|
||||
|
||||
<div className="max-w-5xl mx-auto py-16 lg:py-20 px-6">
|
||||
<div className="docs-page">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-sage-50 border border-sage-100 rounded-full text-xs font-medium text-sage-700 mb-6">
|
||||
<BookOpen size={13} />
|
||||
{language === 'en' ? 'Documentation' : '文档中心'}
|
||||
</div>
|
||||
<h1 className="font-display text-4xl lg:text-5xl font-bold text-ink tracking-tight mb-4">
|
||||
{language === 'en' ? 'Learn TexPixel' : '了解 TexPixel'}
|
||||
<div className="docs-page-header">
|
||||
<div className="eyebrow">{zh ? '文档中心' : 'Documentation'}</div>
|
||||
<h1 className="docs-page-title">
|
||||
{zh ? '学习 TexPixel' : 'Learn TexPixel'}
|
||||
</h1>
|
||||
<p className="text-ink-secondary text-lg max-w-xl">
|
||||
{language === 'en'
|
||||
? 'Everything you need to get started with formula recognition.'
|
||||
: '公式识别入门所需的一切。'}
|
||||
<p className="docs-page-subtitle">
|
||||
{zh
|
||||
? '公式识别入门所需的一切——上传技巧、格式说明与常见问题。'
|
||||
: 'Everything you need to get the most out of formula recognition — upload tips, format guides, and FAQs.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Docs grid */}
|
||||
<div className="space-y-4">
|
||||
{/* List */}
|
||||
<div className="docs-list">
|
||||
{docs.map((doc) => (
|
||||
<Link
|
||||
key={doc.slug}
|
||||
to={`/docs/${doc.slug}`}
|
||||
className="card p-6 flex items-start gap-5 group hover:border-sage-200 block"
|
||||
>
|
||||
<div className="w-11 h-11 bg-sage-50 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
|
||||
<FileText size={20} className="text-sage-600" />
|
||||
<Link key={doc.slug} to={`/docs/${doc.slug}`} className="docs-article-card">
|
||||
<div className="docs-article-card-inner">
|
||||
<div className="docs-article-icon">
|
||||
<DocIcon slug={doc.slug} />
|
||||
</div>
|
||||
<div className="docs-article-body">
|
||||
<div className="docs-article-title">{doc.title}</div>
|
||||
<div className="docs-article-desc">{doc.description}</div>
|
||||
<div className="docs-article-meta">
|
||||
{doc.tags.slice(0, 2).map(tag => (
|
||||
<span key={tag} className="docs-tag">{tag}</span>
|
||||
))}
|
||||
<span className="docs-read-time">
|
||||
{estimateReadTime(doc.description)} {zh ? '分钟阅读' : 'min read'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg className="docs-article-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="font-display text-lg font-semibold text-ink mb-1 group-hover:text-sage-700 transition-colors">
|
||||
{doc.title}
|
||||
</h2>
|
||||
<p className="text-ink-secondary text-sm leading-relaxed">{doc.description}</p>
|
||||
</div>
|
||||
<ArrowRight size={16} className="text-ink-muted group-hover:text-sage-600 transition-colors mt-1 flex-shrink-0" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{docs.length === 0 && (
|
||||
<div className="card p-12 text-center">
|
||||
<p className="text-ink-muted text-sm">
|
||||
{language === 'en' ? 'Documentation coming soon.' : '文档即将发布。'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{docs.length === 0 && (
|
||||
<div className="docs-empty">
|
||||
{zh ? '文档即将发布。' : 'Documentation coming soon.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1571,3 +1571,402 @@
|
||||
.reveal-delay-3 { transition-delay: 0.30s; }
|
||||
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
DOCS PAGES
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Docs list page ── */
|
||||
.docs-page {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 64px 24px 96px;
|
||||
}
|
||||
|
||||
.docs-page-header {
|
||||
margin-bottom: 52px;
|
||||
}
|
||||
|
||||
.docs-page-header .eyebrow {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.docs-page-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-strong);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.docs-page-subtitle {
|
||||
font-size: 17px;
|
||||
color: var(--text-body);
|
||||
line-height: 1.65;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.docs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.docs-article-card {
|
||||
background: var(--elevated);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--r-l);
|
||||
padding: 28px 32px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.docs-article-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--primary-light);
|
||||
box-shadow: var(--shadow-float);
|
||||
}
|
||||
|
||||
.docs-article-card-inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.docs-article-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: var(--warm-wash);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docs-article-icon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
stroke: var(--primary);
|
||||
fill: none;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.docs-article-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.docs-article-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: -0.01em;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.docs-article-card:hover .docs-article-title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.docs-article-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-body);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.docs-article-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.docs-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--warm-wash);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.docs-read-time {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.docs-article-arrow {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
transition: color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.docs-article-card:hover .docs-article-arrow {
|
||||
color: var(--primary);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.docs-empty {
|
||||
background: var(--elevated);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--r-l);
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* ── Docs detail page ── */
|
||||
.docs-detail {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 96px;
|
||||
}
|
||||
|
||||
.docs-back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
margin-bottom: 44px;
|
||||
transition: color 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.docs-back-link:hover { color: var(--primary); }
|
||||
|
||||
.docs-back-link svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.docs-article-header {
|
||||
margin-bottom: 44px;
|
||||
padding-bottom: 36px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docs-article-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.docs-article-h1 {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-strong);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.docs-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.docs-meta-sep { opacity: 0.4; }
|
||||
|
||||
/* ── Docs prose body ── */
|
||||
.docs-prose {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.docs-prose > h1:first-child { display: none; }
|
||||
|
||||
.docs-prose h2 {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
margin: 52px 0 16px;
|
||||
letter-spacing: -0.01em;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docs-prose h3 {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
margin: 32px 0 10px;
|
||||
}
|
||||
|
||||
.docs-prose h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
margin: 24px 0 8px;
|
||||
}
|
||||
|
||||
.docs-prose p { margin-bottom: 20px; }
|
||||
|
||||
.docs-prose ul,
|
||||
.docs-prose ol {
|
||||
padding-left: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.docs-prose li { margin-bottom: 8px; }
|
||||
|
||||
.docs-prose strong {
|
||||
color: var(--text-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docs-prose a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--primary-light);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.docs-prose a:hover { text-decoration-color: var(--primary); }
|
||||
|
||||
.docs-prose code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: var(--warm-wash);
|
||||
color: var(--primary);
|
||||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.docs-prose pre {
|
||||
background: var(--text-strong);
|
||||
color: #f1e6d8;
|
||||
border-radius: var(--r-m);
|
||||
padding: 20px 24px;
|
||||
overflow-x: auto;
|
||||
margin: 28px 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.docs-prose pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.docs-prose blockquote {
|
||||
border-left: 3px solid var(--primary-light);
|
||||
background: var(--warm-wash);
|
||||
padding: 16px 20px;
|
||||
border-radius: 0 var(--r-s) var(--r-s) 0;
|
||||
margin: 28px 0;
|
||||
color: var(--text-body);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.docs-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 28px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.docs-prose th {
|
||||
background: var(--bg);
|
||||
color: var(--text-strong);
|
||||
font-weight: 700;
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docs-prose td {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-body);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.docs-prose tr:nth-child(even) td { background: var(--bg); }
|
||||
|
||||
.docs-prose .katex-display {
|
||||
margin: 32px 0;
|
||||
overflow-x: auto;
|
||||
padding: 20px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-m);
|
||||
}
|
||||
|
||||
/* ── Docs CTA box ── */
|
||||
.docs-cta-box {
|
||||
margin-top: 64px;
|
||||
padding: 44px 48px;
|
||||
background: linear-gradient(135deg, var(--warm-wash) 0%, var(--bg) 100%);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--r-xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.docs-cta-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.docs-cta-desc {
|
||||
font-size: 15px;
|
||||
color: var(--text-body);
|
||||
margin-bottom: 28px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Skeleton loader ── */
|
||||
.docs-skeleton-wrap {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
background: var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 14px;
|
||||
animation: skeleton-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-pulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user