- Add 'Twilight Velvet' color palette to globals.css with OKLCH values - Update SettingsPage headers, cards, and dialogs to use semantic theme variables - Update HistoryCard, HistoryFeed, and DraftContent to support dark mode - Update ProviderSelector and ProviderList to use custom card background (#2A2A3D) - Add ThemeToggle component with improved visibility - Ensure consistent use of 'bg-card', 'text-foreground', and 'text-muted-foreground'
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import rehypeHighlight from 'rehype-highlight';
|
|
import rehypeRaw from 'rehype-raw';
|
|
import type { Draft } from '@/lib/db/draft-service';
|
|
|
|
interface DraftContentProps {
|
|
draft: Draft;
|
|
}
|
|
|
|
/**
|
|
* DraftContent Component - Markdown renderer for draft content
|
|
*
|
|
* Renders draft content with:
|
|
* - Merriweather serif font for "published" feel
|
|
* - Markdown parsing with GFM support
|
|
* - Syntax highlighting for code blocks
|
|
* - Tag display
|
|
* - Generous whitespace for comfortable reading
|
|
*/
|
|
export function DraftContent({ draft }: DraftContentProps) {
|
|
// Strip the first heading from content if it matches the title
|
|
const processedContent = (() => {
|
|
const lines = draft.content.split('\n');
|
|
const firstLine = lines[0]?.trim() || '';
|
|
|
|
// Check if first line is a heading that matches the title
|
|
const headingMatch = firstLine.match(/^#+\s*(.+)$/);
|
|
if (headingMatch) {
|
|
const headingText = headingMatch[1].trim();
|
|
if (headingText.toLowerCase() === draft.title.toLowerCase()) {
|
|
// Remove the first line (and any immediate blank lines after it)
|
|
let startIndex = 1;
|
|
while (startIndex < lines.length && lines[startIndex].trim() === '') {
|
|
startIndex++;
|
|
}
|
|
return lines.slice(startIndex).join('\n');
|
|
}
|
|
}
|
|
return draft.content;
|
|
})();
|
|
|
|
return (
|
|
<article className="draft-content px-4 sm:px-6 py-6 bg-card">
|
|
{/* Title - using Merriweather serif font */}
|
|
<h2 className="draft-title text-2xl sm:text-3xl font-bold text-foreground mb-6 font-serif leading-tight">
|
|
{draft.title}
|
|
</h2>
|
|
|
|
{/* Body content - Markdown with prose styling */}
|
|
<div className="draft-body prose prose-slate dark:prose-invert max-w-none font-serif">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={[rehypeHighlight, rehypeRaw]}
|
|
components={{
|
|
// Custom heading styles
|
|
h1: ({ node, ...props }) => (
|
|
<h1 className="text-2xl font-bold text-foreground mt-8 mb-4 first:mt-0" {...props} />
|
|
),
|
|
h2: ({ node, ...props }) => (
|
|
<h2 className="text-xl font-bold text-foreground mt-6 mb-3" {...props} />
|
|
),
|
|
h3: ({ node, ...props }) => (
|
|
<h3 className="text-lg font-semibold text-foreground mt-5 mb-2" {...props} />
|
|
),
|
|
// Paragraph styling
|
|
p: ({ node, ...props }) => (
|
|
<p className="text-base leading-relaxed text-muted-foreground mb-4" {...props} />
|
|
),
|
|
// Code blocks
|
|
code: ({ node, inline, className, children, ...props }: any) => {
|
|
if (inline) {
|
|
return (
|
|
<code
|
|
className="px-1.5 py-0.5 bg-muted text-foreground rounded text-sm font-mono"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</code>
|
|
);
|
|
}
|
|
return (
|
|
<code
|
|
className={`block bg-muted text-foreground p-4 rounded-lg text-sm font-mono overflow-x-auto ${className || ''}`}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</code>
|
|
);
|
|
},
|
|
// Pre tags
|
|
pre: ({ node, ...props }) => (
|
|
<pre className="bg-muted p-4 rounded-lg overflow-x-auto mb-4" {...props} />
|
|
),
|
|
// Links
|
|
a: ({ node, ...props }) => (
|
|
<a className="text-primary hover:underline" {...props} />
|
|
),
|
|
// Lists
|
|
ul: ({ node, ...props }) => (
|
|
<ul className="list-disc list-inside mb-4 text-muted-foreground space-y-1" {...props} />
|
|
),
|
|
ol: ({ node, ...props }) => (
|
|
<ol className="list-decimal list-inside mb-4 text-muted-foreground space-y-1" {...props} />
|
|
),
|
|
// Blockquotes
|
|
blockquote: ({ node, ...props }) => (
|
|
<blockquote className="border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground my-4" {...props} />
|
|
),
|
|
}}
|
|
>
|
|
{processedContent}
|
|
</ReactMarkdown>
|
|
</div>
|
|
|
|
{/* Tags section */}
|
|
{draft.tags && draft.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-border">
|
|
{draft.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="tag-chip px-3 py-1 bg-secondary text-secondary-foreground rounded-full text-sm font-sans"
|
|
>
|
|
#{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</article>
|
|
);
|
|
}
|