From 0735074a208ea1b806f013502709cdb5313db764 Mon Sep 17 00:00:00 2001
From: tianyaxiang <tianyaxiang@qq.com>
Date: Wed, 29 Jan 2025 20:22:05 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E7=AB=A0=E5=88=97?=
 =?UTF-8?q?=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                           |   1 +
 pnpm-lock.yaml                         |  33 ++
 src/app/layout.tsx                     |  32 +-
 src/app/wechat/page.tsx                |  75 +++-
 src/components/editor/ArticleList.tsx  | 190 +++++++++
 src/components/editor/WechatEditor.tsx | 535 +++++++++++++++++--------
 src/components/theme/ThemeProvider.tsx |   9 +
 src/components/theme/ThemeToggle.tsx   |  22 +
 src/components/ui/scroll-area.tsx      |  48 +++
 src/components/ui/sheet.tsx            | 140 +++++++
 src/config/wechat-templates.ts         |   6 +-
 11 files changed, 900 insertions(+), 191 deletions(-)
 create mode 100644 src/components/editor/ArticleList.tsx
 create mode 100644 src/components/theme/ThemeProvider.tsx
 create mode 100644 src/components/theme/ThemeToggle.tsx
 create mode 100644 src/components/ui/scroll-area.tsx
 create mode 100644 src/components/ui/sheet.tsx

diff --git a/package.json b/package.json
index e7b5b82..f62bff8 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "@radix-ui/react-dialog": "^1.1.5",
     "@radix-ui/react-dropdown-menu": "^2.1.5",
     "@radix-ui/react-label": "^2.1.1",
+    "@radix-ui/react-scroll-area": "^1.2.2",
     "@radix-ui/react-select": "^2.1.5",
     "@radix-ui/react-separator": "^1.0.3",
     "@radix-ui/react-slot": "^1.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index daa8bbe..6be8d7f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
       '@radix-ui/react-label':
         specifier: ^2.1.1
         version: 2.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@radix-ui/react-scroll-area':
+        specifier: ^1.2.2
+        version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@radix-ui/react-select':
         specifier: ^2.1.5
         version: 2.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -517,6 +520,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-scroll-area@1.2.2':
+    resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-select@2.1.5':
     resolution: {integrity: sha512-eVV7N8jBXAXnyrc+PsOF89O9AfVgGnbLxUtBb0clJ8y8ENMWLARGMI/1/SBRLz7u4HqxLgN71BJ17eono3wcjA==}
     peerDependencies:
@@ -2726,6 +2742,23 @@ snapshots:
       '@types/react': 18.3.18
       '@types/react-dom': 18.3.5(@types/react@18.3.18)
 
+  '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@radix-ui/number': 1.1.0
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
+      '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
+      '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1)
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+    optionalDependencies:
+      '@types/react': 18.3.18
+      '@types/react-dom': 18.3.5(@types/react@18.3.18)
+
   '@radix-ui/react-select@2.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
       '@radix-ui/number': 1.1.0
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 5f33c5c..d0f217f 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,24 +1,42 @@
-import type { Metadata } from "next";
+import type { Metadata, Viewport } from "next";
 import { Inter } from "next/font/google";
 import "./globals.css";
 import { cn } from "@/lib/utils";
 import { Toaster } from "@/components/ui/toaster";
-import { ThemeProvider } from "@/components/theme-provider";
+import { ThemeProvider } from "@/components/theme/ThemeProvider";
 
 const inter = Inter({ subsets: ["latin"] });
 
 export const metadata: Metadata = {
-  title: "NeuraPress - 专业的内容转换工具",
-  description: "将 Markdown 转换为微信公众号和小红书样式的专业工具",
+  title: "NeuraPress",
+  description: "一个现代化的内容创作平台",
+};
+
+export const viewport: Viewport = {
+  width: "device-width",
+  initialScale: 1,
+  maximumScale: 1,
+  userScalable: false,
+  viewportFit: "cover",
+  themeColor: [
+    { media: "(prefers-color-scheme: light)", color: "white" },
+    { media: "(prefers-color-scheme: dark)", color: "#020817" }
+  ]
 };
 
 export default function RootLayout({
   children,
-}: Readonly<{
-  children: React.ReactNode;
-}>) {
+}: {
+  children: React.ReactNode
+}) {
   return (
     <html lang="zh-CN" className="h-full" suppressHydrationWarning>
+      <head>
+        <meta name="apple-mobile-web-app-capable" content="yes" />
+        <meta name="apple-mobile-web-app-status-bar-style" content="default" />
+        <meta name="format-detection" content="telephone=no" />
+        <meta name="mobile-web-app-capable" content="yes" />
+      </head>
       <body className={cn(
         inter.className,
         "h-full bg-background text-foreground antialiased"
diff --git a/src/app/wechat/page.tsx b/src/app/wechat/page.tsx
index e9ba8a6..b28f6fc 100644
--- a/src/app/wechat/page.tsx
+++ b/src/app/wechat/page.tsx
@@ -1,18 +1,73 @@
-import { MainNav } from '@/components/nav/MainNav'
 import WechatEditor from '@/components/editor/WechatEditor'
+import { Menu } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import {
+  Sheet,
+  SheetContent,
+  SheetTrigger,
+} from "@/components/ui/sheet"
+import { ThemeToggle } from '@/components/theme/ThemeToggle'
 
 export default function WechatPage() {
   return (
-    <div className="min-h-full">
-      <MainNav />
-      <main className="py-6">
-        <div className="container mx-auto px-4">
-
-          <div className="bg-white rounded-lg shadow-sm border">
-            <WechatEditor />
+    <main className="min-h-screen bg-background">
+      <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
+        <div className="container flex h-14 max-w-screen-2xl items-center px-4">
+          <div className="flex items-center flex-1 gap-2">
+            <div className="md:hidden">
+              <Sheet>
+                <SheetTrigger asChild>
+                  <Button variant="ghost" size="icon" className="mr-2">
+                    <Menu className="h-5 w-5" />
+                    <span className="sr-only">Toggle menu</span>
+                  </Button>
+                </SheetTrigger>
+                <SheetContent side="left" className="w-[240px] sm:w-[280px] p-0">
+                  <nav className="flex flex-col">
+                    <a
+                      href="/wechat"
+                      className="flex h-12 items-center border-b px-4 text-sm font-medium text-foreground"
+                    >
+                      微信编辑器
+                    </a>
+                    <a
+                      href="/xiaohongshu"
+                      className="flex h-12 items-center border-b px-4 text-sm font-medium text-foreground/60 hover:text-foreground/80"
+                    >
+                      小红书编辑器
+                    </a>
+                  </nav>
+                </SheetContent>
+              </Sheet>
+            </div>
+            <a className="flex items-center space-x-2" href="/">
+              <span className="font-bold inline-block">
+                NeuraPress
+              </span>
+            </a>
           </div>
+          <nav className="flex items-center space-x-6">
+            <div className="hidden md:flex items-center space-x-6 text-sm font-medium">
+              <a
+                href="/wechat"
+                className="transition-colors hover:text-foreground/80 text-foreground"
+              >
+                微信编辑器
+              </a>
+              <a
+                href="/xiaohongshu"
+                className="text-foreground/60 transition-colors hover:text-foreground/80"
+              >
+                小红书编辑器
+              </a>
+            </div>
+            <ThemeToggle />
+          </nav>
         </div>
-      </main>
-    </div>
+      </header>
+      <div className="relative h-[calc(100vh-3.5rem)]">
+        <WechatEditor />
+      </div>
+    </main>
   )
 } 
\ No newline at end of file
diff --git a/src/components/editor/ArticleList.tsx b/src/components/editor/ArticleList.tsx
new file mode 100644
index 0000000..ff5b888
--- /dev/null
+++ b/src/components/editor/ArticleList.tsx
@@ -0,0 +1,190 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from '@/components/ui/sheet'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { FileText, Trash2, Menu, Plus, Save } from 'lucide-react'
+import { useToast } from '@/components/ui/use-toast'
+import { ToastAction } from '@/components/ui/toast'
+
+interface Article {
+  id: string
+  title: string
+  content: string
+  template: string
+  createdAt: number
+  updatedAt: number
+}
+
+interface ArticleListProps {
+  onSelect: (article: Article) => void
+  currentContent?: string
+  onNew?: () => void
+}
+
+export function ArticleList({ onSelect, currentContent, onNew }: ArticleListProps) {
+  const { toast } = useToast()
+  const [articles, setArticles] = useState<Article[]>([])
+
+  // 加载文章列表
+  useEffect(() => {
+    const savedArticles = localStorage.getItem('wechat_articles')
+    if (savedArticles) {
+      try {
+        const parsed = JSON.parse(savedArticles)
+        setArticles(Array.isArray(parsed) ? parsed : [])
+      } catch (error) {
+        console.error('Failed to parse saved articles:', error)
+      }
+    }
+  }, [])
+
+  // 保存当前文章
+  const saveCurrentArticle = () => {
+    if (!currentContent) {
+      toast({
+        variant: "destructive",
+        title: "保存失败",
+        description: "当前没有可保存的内容",
+        duration: 2000
+      })
+      return
+    }
+
+    const title = currentContent.split('\n')[0]?.replace(/^#*\s*/, '') || '未命名文章'
+    const newArticle: Article = {
+      id: Date.now().toString(),
+      title,
+      content: currentContent,
+      template: 'creative', // 默认模板
+      createdAt: Date.now(),
+      updatedAt: Date.now()
+    }
+
+    const updatedArticles = [newArticle, ...articles]
+    setArticles(updatedArticles)
+    localStorage.setItem('wechat_articles', JSON.stringify(updatedArticles))
+
+    toast({
+      title: "保存成功",
+      description: `已保存文章:${title}`,
+      duration: 2000
+    })
+  }
+
+  // 删除文章
+  const deleteArticle = (id: string) => {
+    const updatedArticles = articles.filter(article => article.id !== id)
+    setArticles(updatedArticles)
+    localStorage.setItem('wechat_articles', JSON.stringify(updatedArticles))
+
+    toast({
+      title: "删除成功",
+      description: "文章已删除",
+      duration: 2000
+    })
+  }
+
+  // 新建文章
+  const createNewArticle = () => {
+    // 如果有外部传入的新建处理函数,优先使用
+    if (onNew) {
+      onNew()
+      return
+    }
+
+    // 默认的新建文章处理
+    const newArticle: Article = {
+      id: Date.now().toString(),
+      title: '新文章',
+      content: '# 新文章\n\n开始写作...',
+      template: 'creative',
+      createdAt: Date.now(),
+      updatedAt: Date.now()
+    }
+
+    onSelect(newArticle)
+    toast({
+      title: "新建成功",
+      description: "已创建新文章",
+      duration: 2000
+    })
+  }
+
+  return (
+    <Sheet>
+      <SheetTrigger asChild>
+        <Button variant="ghost" size="icon" className="relative">
+          <Menu className="h-5 w-5" />
+          <span className="sr-only">文章列表</span>
+          {articles.length > 0 && (
+            <span className="absolute -top-1 -right-1 w-4 h-4 bg-primary text-[10px] text-primary-foreground rounded-full flex items-center justify-center">
+              {articles.length}
+            </span>
+          )}
+        </Button>
+      </SheetTrigger>
+      <SheetContent side="left" className="w-[300px] sm:w-[400px]">
+        <SheetHeader>
+          <SheetTitle>文章列表</SheetTitle>
+          <SheetDescription className="flex gap-2">
+            <Button onClick={createNewArticle} className="flex-1">
+              <Plus className="h-4 w-4 mr-2" />
+              新建文章
+            </Button>
+            <Button onClick={saveCurrentArticle} className="flex-1">
+              <Save className="h-4 w-4 mr-2" />
+              保存当前
+            </Button>
+          </SheetDescription>
+        </SheetHeader>
+        <ScrollArea className="h-[calc(100vh-8rem)] mt-4">
+          <div className="space-y-2">
+            {articles.map(article => (
+              <div
+                key={article.id}
+                className="flex items-center justify-between p-2 rounded-md hover:bg-muted group"
+              >
+                <button
+                  onClick={() => onSelect(article)}
+                  className="flex items-center gap-2 flex-1 text-left"
+                >
+                  <FileText className="h-4 w-4 text-muted-foreground" />
+                  <div className="flex-1 min-w-0">
+                    <div className="font-medium truncate">{article.title}</div>
+                    <div className="text-xs text-muted-foreground">
+                      {new Date(article.updatedAt).toLocaleString()}
+                    </div>
+                  </div>
+                </button>
+                <Button
+                  variant="ghost"
+                  size="icon"
+                  className="opacity-0 group-hover:opacity-100 transition-opacity"
+                  onClick={() => deleteArticle(article.id)}
+                >
+                  <Trash2 className="h-4 w-4 text-destructive" />
+                  <span className="sr-only">删除</span>
+                </Button>
+              </div>
+            ))}
+            {articles.length === 0 && (
+              <div className="text-center text-muted-foreground py-8">
+                暂无保存的文章
+              </div>
+            )}
+          </div>
+        </ScrollArea>
+      </SheetContent>
+    </Sheet>
+  )
+} 
\ No newline at end of file
diff --git a/src/components/editor/WechatEditor.tsx b/src/components/editor/WechatEditor.tsx
index bc04f49..a9d1f6e 100644
--- a/src/components/editor/WechatEditor.tsx
+++ b/src/components/editor/WechatEditor.tsx
@@ -10,7 +10,7 @@ import mermaid from '@bytemd/plugin-mermaid'
 import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
 import { templates } from '@/config/wechat-templates'
 import { cn } from '@/lib/utils'
-import { Copy, Smartphone, Loader2, Save } from 'lucide-react'
+import { Copy, Smartphone, Loader2, Save, Plus } from 'lucide-react'
 import { WechatStylePicker } from '../template/WechatStylePicker'
 import { StyleConfigDialog } from './StyleConfigDialog'
 import { convertToWechat } from '@/lib/markdown'
@@ -22,6 +22,7 @@ import 'bytemd/dist/index.css'
 import 'highlight.js/styles/github.css'
 import 'katex/dist/katex.css'
 import type { BytemdPlugin } from 'bytemd'
+import { ArticleList } from './ArticleList'
 
 const PREVIEW_SIZES = {
   small: { width: '360px', label: '小屏' },
@@ -40,7 +41,7 @@ export default function WechatEditor() {
   const editorRef = useRef<HTMLDivElement>(null)
   const previewRef = useRef<HTMLDivElement>(null)
   const [value, setValue] = useState('')
-  const [selectedTemplate, setSelectedTemplate] = useState<string>('')
+  const [selectedTemplate, setSelectedTemplate] = useState<string>('creative')
   const [showPreview, setShowPreview] = useState(true)
   const [styleOptions, setStyleOptions] = useState<RendererOptions>({})
   const [previewSize, setPreviewSize] = useState<PreviewSize>('medium')
@@ -234,7 +235,8 @@ export default function WechatEditor() {
   }
 
   const handleCopy = async () => {
-    const previewContent = document.querySelector('.preview-content')
+    // 使用 bytemd 编辑器的预览区域
+    const previewContent = editorRef.current?.querySelector('.bytemd-preview .markdown-body')
     if (!previewContent) {
       toast({
         variant: "destructive",
@@ -248,67 +250,120 @@ export default function WechatEditor() {
     try {
       // 创建一个临时容器
       const tempDiv = document.createElement('div')
-      
-      // 使用与预览相同的转换逻辑
-      const template = templates.find(t => t.id === selectedTemplate)    
+      tempDiv.innerHTML = previewContent.innerHTML
 
+      // 应用模板样式
+      const template = templates.find(t => t.id === selectedTemplate)
+      if (template) {
+        tempDiv.className = template.styles
+      }
+
+      // 处理图片
+      const images = tempDiv.querySelectorAll('img')
+      images.forEach(img => {
+        // 确保图片使用绝对路径
+        if (img.src.startsWith('/')) {
+          img.src = window.location.origin + img.src
+        }
+      })
+
+      // 处理代码块
+      const codeBlocks = tempDiv.querySelectorAll('pre code')
+      codeBlocks.forEach(code => {
+        if (code.parentElement) {
+          code.parentElement.style.whiteSpace = 'pre-wrap'
+          code.parentElement.style.wordWrap = 'break-word'
+        }
+      })
+
+      // 应用样式选项
       const mergedOptions = {
         ...styleOptions,
         ...(template?.options || {})
       }
-      // 使用相同的转换逻辑获取内容
-      const html = convertToWechat(value, mergedOptions)
-      let finalHtml = html
-    
-      if (template?.transform) {
-        try {
-          const transformed = template.transform(html)
-          if (transformed && typeof transformed === 'object') {
-            const result = transformed as { html?: string; content?: string }
-            if (result.html) finalHtml = result.html
-            else if (result.content) finalHtml = result.content
-            else finalHtml = JSON.stringify(transformed)
-          } else {
-            finalHtml = transformed || html
-          }
-        } catch (error) {
-          console.error('Template transformation error:', error)
-          finalHtml = html
+
+      // 应用标题样式
+      const headings = tempDiv.querySelectorAll('h1, h2, h3, h4, h5, h6')
+      headings.forEach(heading => {
+        const level = heading.tagName.toLowerCase()
+        const style = mergedOptions.block?.[level as keyof RendererOptions['block']]
+        if (style && heading instanceof HTMLElement) {
+          Object.assign(heading.style, style)
         }
-      }
-      
-      // 设置转换后的内容
-      tempDiv.innerHTML = finalHtml
+      })
 
-      // 应用模板样式类
-      if (template) {
-        tempDiv.className = template.styles
-      }
-      alert(tempDiv.innerHTML)
+      // 应用段落样式
+      const paragraphs = tempDiv.querySelectorAll('p')
+      paragraphs.forEach(p => {
+        if (mergedOptions.block?.p && p instanceof HTMLElement) {
+          Object.assign(p.style, mergedOptions.block.p)
+        }
+      })
+
+      // 应用引用样式
+      const blockquotes = tempDiv.querySelectorAll('blockquote')
+      blockquotes.forEach(quote => {
+        if (mergedOptions.block?.blockquote && quote instanceof HTMLElement) {
+          Object.assign(quote.style, mergedOptions.block.blockquote)
+        }
+      })
+
+      // 应用行内代码样式
+      const inlineCodes = tempDiv.querySelectorAll(':not(pre) > code')
+      inlineCodes.forEach(code => {
+        if (mergedOptions.inline?.codespan && code instanceof HTMLElement) {
+          Object.assign(code.style, mergedOptions.inline.codespan)
+        }
+      })
+
+      // 使用 Clipboard API 复制
+      const htmlBlob = new Blob([tempDiv.innerHTML], { type: 'text/html' })
+      const textBlob = new Blob([tempDiv.innerText], { type: 'text/plain' })
 
-      // 使用 Blob 和 Clipboard API 复制
-      const blob = new Blob([tempDiv.innerHTML], { type: 'text/html' })
       await navigator.clipboard.write([
         new ClipboardItem({
-          'text/html': blob
+          'text/html': htmlBlob,
+          'text/plain': textBlob
         })
       ])
 
       toast({
         title: "复制成功",
         description: template 
-          ? "已复制预览内容(使用当前模板样式)" 
-          : "已复制预览内容(无样式)",
+          ? "已复制预览内容(包含样式)" 
+          : "已复制预览内容",
         duration: 2000
       })
     } catch (err) {
-      toast({
-        variant: "destructive",
-        title: "复制失败",
-        description: "无法访问剪贴板,请检查浏览器权限",
-        action: <ToastAction altText="重试">重试</ToastAction>,
-      })
       console.error('Copy error:', err)
+      // 降级处理:尝试使用 execCommand
+      try {
+        const tempDiv = document.createElement('div')
+        tempDiv.innerHTML = previewContent.innerHTML
+        document.body.appendChild(tempDiv)
+        const range = document.createRange()
+        range.selectNode(tempDiv)
+        const selection = window.getSelection()
+        if (selection) {
+          selection.removeAllRanges()
+          selection.addRange(range)
+          document.execCommand('copy')
+          selection.removeAllRanges()
+        }
+        document.body.removeChild(tempDiv)
+        toast({
+          title: "复制成功",
+          description: "已复制预览内容(兼容模式)",
+          duration: 2000
+        })
+      } catch (fallbackErr) {
+        toast({
+          variant: "destructive",
+          title: "复制失败",
+          description: "无法访问剪贴板,请检查浏览器权限",
+          action: <ToastAction altText="重试">重试</ToastAction>,
+        })
+      }
     }
   }
 
@@ -377,7 +432,6 @@ export default function WechatEditor() {
           handler: {
             type: 'action',
             click: (ctx: any) => {
-              // 触发自定义事件
               const event = new CustomEvent('bytemd-save', { detail: ctx.editor.getValue() })
               window.dispatchEvent(event)
             }
@@ -388,46 +442,42 @@ export default function WechatEditor() {
           icon: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /></svg>',
           handler: {
             type: 'action',
-            click: async (ctx: any) => {
-              const previewContent = ctx.preview?.querySelector('.markdown-body')
-              if (!previewContent) {
-                toast({
-                  variant: "destructive",
-                  title: "复制失败",
-                  description: "未找到预览内容",
-                  duration: 2000
-                })
-                return
+            click: handleCopy
+          }
+        },
+        {
+          title: '复制源码',
+          icon: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>',
+          handler: {
+            type: 'action',
+            click: copyContent
+          }
+        },
+        {
+          title: '预览尺寸',
+          icon: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 3H3v18h18V3z"></path><path d="M21 13H3"></path><path d="M12 3v18"></path></svg>',
+          handler: {
+            type: 'dropdown',
+            actions: Object.entries(PREVIEW_SIZES).map(([key, { label }]) => ({
+              title: label,
+              handler: {
+                type: 'action',
+                click: () => {
+                  setPreviewSize(key as PreviewSize)
+                  const previewContent = editorRef.current?.querySelector('.bytemd-preview .markdown-body')
+                  if (previewContent) {
+                    const container = previewContent.parentElement
+                    if (container) {
+                      const size = PREVIEW_SIZES[key as PreviewSize]
+                      container.style.maxWidth = size.width
+                      container.style.margin = '0 auto'
+                      container.style.transition = 'max-width 0.3s ease'
+                      container.style.textAlign = 'center'
+                    }
+                  }
+                }
               }
-
-              try {
-                // 创建一个临时容器
-                const tempDiv = document.createElement('div')
-                tempDiv.innerHTML = previewContent.innerHTML
-                
-                // 使用 Blob 和 Clipboard API 复制
-                const blob = new Blob([tempDiv.innerHTML], { type: 'text/html' })
-                await navigator.clipboard.write([
-                  new ClipboardItem({
-                    'text/html': blob
-                  })
-                ])
-
-                toast({
-                  title: "复制成功",
-                  description: "已复制预览内容(包含样式)",
-                  duration: 2000
-                })
-              } catch (err) {
-                toast({
-                  variant: "destructive",
-                  title: "复制失败",
-                  description: "无法访问剪贴板,请检查浏览器权限",
-                  action: <ToastAction altText="重试">重试</ToastAction>,
-                })
-                console.error('Copy error:', err)
-              }
-            }
+            }))
           }
         }
       ],
@@ -460,9 +510,18 @@ export default function WechatEditor() {
         if (selectedTemplate) {
           applyTemplateStyles(markdownBody, selectedTemplate, styleOptions)
         }
+
+        // 设置预览容器宽度
+        const container = markdownBody.parentElement
+        if (container) {
+          const size = PREVIEW_SIZES[previewSize]
+          container.style.maxWidth = size.width
+          container.style.margin = '0 auto'
+          container.style.transition = 'max-width 0.3s ease'
+        }
       }
     }
-  }, [selectedTemplate, styleOptions])
+  }, [selectedTemplate, styleOptions, handleCopy, copyContent, previewSize, setPreviewSize, editorRef])
 
   // 使用创建的插件
   const plugins = useMemo(() => [
@@ -489,93 +548,179 @@ export default function WechatEditor() {
     createEditorPlugin()
   ], [createEditorPlugin])
 
+  // 检测是否为移动设备
+  const isMobile = useCallback(() => {
+    return window.innerWidth < 640
+  }, [])
+
+  // 自动切换预览模式
+  useEffect(() => {
+    const handleResize = () => {
+      if (isMobile()) {
+        setPreviewSize('full')
+      }
+    }
+
+    handleResize()
+    window.addEventListener('resize', handleResize)
+    return () => window.removeEventListener('resize', handleResize)
+  }, [isMobile])
+
+  // 处理文章选择
+  const handleArticleSelect = useCallback((article: { content: string, template: string }) => {
+    setValue(article.content)
+    setSelectedTemplate(article.template)
+    setIsDraft(false)
+    toast({
+      title: "加载成功",
+      description: "已加载选中的文章",
+      duration: 2000
+    })
+  }, [toast])
+
+  // 处理新建文章
+  const handleNewArticle = useCallback(() => {
+    // 如果有未保存的内容,提示用户
+    if (isDraft) {
+      toast({
+        title: "提示",
+        description: "当前文章未保存,是否继续?",
+        action: (
+          <ToastAction altText="继续" onClick={() => {
+            setValue('# 新文章\n\n开始写作...')
+            setIsDraft(false)
+          }}>
+            继续
+          </ToastAction>
+        ),
+        duration: 5000,
+      })
+      return
+    }
+
+    // 直接新建
+    setValue('# 新文章\n\n开始写作...')
+    setIsDraft(false)
+  }, [isDraft, toast])
+
   return (
-    <div className="h-[calc(100vh-4rem)]">
-      <div className="border-b bg-background sticky top-0 z-20">
-        <div className="p-4">
-          <div className="flex items-center justify-between">
-            <div className="flex items-center space-x-4">
-              <WechatStylePicker 
-                value={selectedTemplate} 
-                onSelect={setSelectedTemplate} 
-              />
-              <div className="h-6 w-px bg-border" />
-              <TemplateManager onTemplateChange={handleTemplateChange} />
-              <div className="h-6 w-px bg-border" />
-              <StyleConfigDialog
-                value={styleOptions}
-                onChange={setStyleOptions}
-              />
-              <div className="h-6 w-px bg-border" />
-              <button
-                onClick={() => setShowPreview(!showPreview)}
-                className={cn(
-                  "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors",
-                  showPreview 
-                    ? "bg-primary text-primary-foreground hover:bg-primary/90"
-                    : "bg-muted text-muted-foreground hover:bg-muted/90"
+    <div className="h-full flex flex-col">
+      <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-20">
+        <div className="container mx-auto">
+          <div className="p-4">
+            <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
+              <div className="flex flex-wrap items-center gap-4 w-full sm:w-auto">
+                <ArticleList 
+                  onSelect={handleArticleSelect}
+                  currentContent={value}
+                  onNew={handleNewArticle}
+                />
+                <button
+                  onClick={handleNewArticle}
+                  className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors w-full sm:w-auto justify-center bg-muted text-muted-foreground hover:bg-muted/90"
+                >
+                  <Plus className="h-4 w-4" />
+                  新建文章
+                </button>
+                <WechatStylePicker 
+                  value={selectedTemplate} 
+                  onSelect={setSelectedTemplate} 
+                />
+                <div className="hidden sm:block h-6 w-px bg-border" />
+                <TemplateManager onTemplateChange={handleTemplateChange} />
+                <div className="hidden sm:block h-6 w-px bg-border" />
+                <StyleConfigDialog
+                  value={styleOptions}
+                  onChange={setStyleOptions}
+                />
+                <div className="hidden sm:block h-6 w-px bg-border" />
+                <button
+                  onClick={() => setShowPreview(!showPreview)}
+                  className={cn(
+                    "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors w-full sm:w-auto justify-center",
+                    showPreview 
+                      ? "bg-primary text-primary-foreground hover:bg-primary/90"
+                      : "bg-muted text-muted-foreground hover:bg-muted/90"
+                  )}
+                >
+                  <Smartphone className="h-4 w-4" />
+                  {showPreview ? '编辑' : '预览'}
+                </button>
+              </div>
+              <div className="flex items-center gap-2 w-full sm:w-auto">
+                {isDraft && (
+                  <span className="text-sm text-muted-foreground">未保存</span>
                 )}
-              >
-                <Smartphone className="h-4 w-4" />
-                {showPreview ? '隐藏预览' : '显示预览'}
-              </button>
-            </div>
-            <div className="flex items-center space-x-2">
-              <button
-                onClick={handleSave}
-                className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-muted text-muted-foreground hover:bg-muted/90 text-sm transition-colors"
-              >
-                <Save className="h-4 w-4" />
-                保存
-              </button>
-              <button
-                onClick={copyContent}
-                className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-muted text-muted-foreground hover:bg-muted/90 text-sm transition-colors"
-              >
-                <Copy className="h-4 w-4" />
-                复制源码
-              </button>
-              <button
-                onClick={handleCopy}
-                className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 text-sm transition-colors"
-              >
-                <Copy className="h-4 w-4" />
-                复制预览
-              </button>
+                <button
+                  onClick={handleSave}
+                  className={cn(
+                    "inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors flex-1 sm:flex-none",
+                    isDraft 
+                      ? "bg-primary text-primary-foreground hover:bg-primary/90"
+                      : "bg-muted text-muted-foreground hover:bg-muted/90"
+                  )}
+                >
+                  <Save className="h-4 w-4" />
+                  <span>保存</span>
+                </button>
+                <button
+                  onClick={copyContent}
+                  className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md bg-muted text-muted-foreground hover:bg-muted/90 text-sm transition-colors flex-1 sm:flex-none"
+                >
+                  <Copy className="h-4 w-4" />
+                  <span>复制源码</span>
+                </button>
+                <button
+                  onClick={handleCopy}
+                  className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 text-sm transition-colors flex-1 sm:flex-none"
+                >
+                  <Copy className="h-4 w-4" />
+                  <span>复制预览</span>
+                </button>
+              </div>
             </div>
           </div>
         </div>
       </div>
       
-      <div className="flex flex-1 overflow-hidden">
+      <div className="flex-1 flex flex-col sm:flex-row overflow-hidden sm:pb-0 pb-16">
         <div 
           ref={editorRef}
           className={cn(
-            "editor-container border-r bg-background transition-all duration-300 ease-in-out overflow-hidden",
-            showPreview ? "w-1/2" : "w-full",
+            "editor-container bg-background transition-all duration-300 ease-in-out",
+            showPreview 
+              ? "h-[calc(50vh-4rem)] sm:h-[calc(100vh-7.5rem)] sm:w-1/2 border-b sm:border-r" 
+              : "h-[calc(100vh-10rem)] sm:h-[calc(100vh-7.5rem)] w-full",
             selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
           )}
+          style={{
+            display: 'flex',
+            flexDirection: 'column'
+          }}
         >
-          <Editor
-            value={value}
-            plugins={plugins}
-            onChange={handleEditorChange}
-            uploadImages={async (files: File[]) => {
-              return []
-            }}
-          />
+          <div className="flex-1 overflow-auto">
+            <Editor
+              value={value}
+              plugins={plugins}
+              onChange={handleEditorChange}
+              uploadImages={async (files: File[]) => {
+                return []
+              }}
+            />
+          </div>
         </div>
         
         {showPreview && (
           <div 
             ref={previewRef}
             className={cn(
-              "preview-container bg-background transition-all duration-300 ease-in-out w-1/2 flex flex-col",
+              "preview-container bg-background transition-all duration-300 ease-in-out flex flex-col",
+              "h-[calc(50vh-4rem)] sm:h-[calc(100vh-7.5rem)] sm:w-1/2",
               "markdown-body",
               selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
             )}
           >
-            <div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b px-4 py-2 flex items-center justify-between z-10">
+            <div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b px-4 py-2 flex items-center justify-between z-10 sticky top-0">
               <div className="text-sm text-muted-foreground">预览效果</div>
               <div className="flex items-center gap-2">
                 <select
@@ -591,31 +736,83 @@ export default function WechatEditor() {
             </div>
             
             <div className="flex-1 overflow-y-auto">
-              <div 
-                className={cn(
-                  "bg-background mx-auto",
-                  previewSize === 'full' ? '' : 'border shadow-sm'
-                )}
-                style={{ width: PREVIEW_SIZES[previewSize].width }}
-              >
-                {isConverting ? (
-                  <div className="flex items-center justify-center p-8">
-                    <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
-                  </div>
-                ) : (
-                  <div className={cn(
-                    "preview-content py-6 px-6",
-                    "prose prose-slate dark:prose-invert max-w-none",
-                    selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
-                  )}>
-                    <div dangerouslySetInnerHTML={{ __html: getPreviewContent() }} />
-                  </div>
-                )}
+              <div className="min-h-full py-8 px-4">
+                <div 
+                  className={cn(
+                    "bg-background mx-auto rounded-lg transition-all duration-300",
+                    previewSize === 'full' ? '' : 'border shadow-sm'
+                  )}
+                  style={{ 
+                    width: PREVIEW_SIZES[previewSize].width,
+                    maxWidth: '100%'
+                  }}
+                >
+                  {isConverting ? (
+                    <div className="flex items-center justify-center p-8">
+                      <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+                    </div>
+                  ) : (
+                    <div className={cn(
+                      "preview-content py-4",
+                      "prose prose-slate dark:prose-invert max-w-none",
+                      selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
+                    )}>
+                      <div className="px-6" dangerouslySetInnerHTML={{ __html: getPreviewContent() }} />
+                    </div>
+                  )}
+                </div>
               </div>
             </div>
           </div>
         )}
       </div>
+
+      {/* 移动端底部工具栏 */}
+      <div className="sm:hidden fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t p-2 flex justify-around">
+        <button
+          onClick={() => setShowPreview(!showPreview)}
+          className={cn(
+            "flex flex-col items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors relative",
+            showPreview 
+              ? "text-primary"
+              : "text-muted-foreground"
+          )}
+        >
+          <Smartphone className="h-5 w-5" />
+          {showPreview ? '编辑' : '预览'}
+        </button>
+        <button
+          onClick={handleSave}
+          className={cn(
+            "flex flex-col items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors relative",
+            isDraft 
+              ? "text-primary"
+              : "text-muted-foreground"
+          )}
+        >
+          <Save className="h-5 w-5" />
+          保存
+          {isDraft && <span className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />}
+        </button>
+        <button
+          onClick={copyContent}
+          className="flex flex-col items-center gap-1 px-2 py-1 rounded-md text-xs text-muted-foreground transition-colors relative"
+        >
+          <Copy className="h-5 w-5" />
+          源码
+        </button>
+        <button
+          onClick={handleCopy}
+          className={cn(
+            "flex flex-col items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors relative",
+            "hover:bg-primary/10 active:bg-primary/20",
+            showPreview ? "text-primary" : "text-muted-foreground"
+          )}
+        >
+          <Copy className="h-5 w-5" />
+          复制预览
+        </button>
+      </div>
     </div>
   )
 }
\ No newline at end of file
diff --git a/src/components/theme/ThemeProvider.tsx b/src/components/theme/ThemeProvider.tsx
new file mode 100644
index 0000000..220a1f8
--- /dev/null
+++ b/src/components/theme/ThemeProvider.tsx
@@ -0,0 +1,9 @@
+"use client"
+
+import * as React from "react"
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+import { type ThemeProviderProps } from "next-themes/dist/types"
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
+} 
\ No newline at end of file
diff --git a/src/components/theme/ThemeToggle.tsx b/src/components/theme/ThemeToggle.tsx
new file mode 100644
index 0000000..677a83c
--- /dev/null
+++ b/src/components/theme/ThemeToggle.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import * as React from "react"
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "next-themes"
+import { Button } from "@/components/ui/button"
+
+export function ThemeToggle() {
+  const { theme, setTheme } = useTheme()
+
+  return (
+    <Button
+      variant="ghost"
+      size="icon"
+      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
+    >
+      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
+      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
+      <span className="sr-only">切换主题</span>
+    </Button>
+  )
+} 
\ No newline at end of file
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..c9e741f
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
+>(({ className, children, ...props }, ref) => (
+  <ScrollAreaPrimitive.Root
+    ref={ref}
+    className={cn("relative overflow-hidden", className)}
+    {...props}
+  >
+    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
+      {children}
+    </ScrollAreaPrimitive.Viewport>
+    <ScrollBar />
+    <ScrollAreaPrimitive.Corner />
+  </ScrollAreaPrimitive.Root>
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
+  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
+>(({ className, orientation = "vertical", ...props }, ref) => (
+  <ScrollAreaPrimitive.ScrollAreaScrollbar
+    ref={ref}
+    orientation={orientation}
+    className={cn(
+      "flex touch-none select-none transition-colors",
+      orientation === "vertical" &&
+        "h-full w-2.5 border-l border-l-transparent p-[1px]",
+      orientation === "horizontal" &&
+        "h-2.5 flex-col border-t border-t-transparent p-[1px]",
+      className
+    )}
+    {...props}
+  >
+    <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
+  </ScrollAreaPrimitive.ScrollAreaScrollbar>
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar } 
\ No newline at end of file
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..46d45fb
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Overlay
+    className={cn(
+      "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+      className
+    )}
+    {...props}
+    ref={ref}
+  />
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+  {
+    variants: {
+      side: {
+        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+        bottom:
+          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+        right:
+          "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+      },
+    },
+    defaultVariants: {
+      side: "right",
+    },
+  }
+)
+
+interface SheetContentProps
+  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
+    VariantProps<typeof sheetVariants> {}
+
+const SheetContent = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Content>,
+  SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+  <SheetPortal>
+    <SheetOverlay />
+    <SheetPrimitive.Content
+      ref={ref}
+      className={cn(sheetVariants({ side }), className)}
+      {...props}
+    >
+      {children}
+      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
+        <X className="h-4 w-4" />
+        <span className="sr-only">Close</span>
+      </SheetPrimitive.Close>
+    </SheetPrimitive.Content>
+  </SheetPortal>
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col space-y-2 text-center sm:text-left",
+      className
+    )}
+    {...props}
+  />
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
+      className
+    )}
+    {...props}
+  />
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Title
+    ref={ref}
+    className={cn("text-lg font-semibold text-foreground", className)}
+    {...props}
+  />
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Description
+    ref={ref}
+    className={cn("text-sm text-muted-foreground", className)}
+    {...props}
+  />
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+  Sheet,
+  SheetPortal,
+  SheetOverlay,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+} 
\ No newline at end of file
diff --git a/src/config/wechat-templates.ts b/src/config/wechat-templates.ts
index b841264..cdcaa39 100644
--- a/src/config/wechat-templates.ts
+++ b/src/config/wechat-templates.ts
@@ -309,11 +309,7 @@ export const templates: Template[] = [
         }
       }
     },
-    transform: (html) => `
-      <section style="font-family: system-ui, sans-serif;">
-        ${html}
-      </section>
-    `
+    transform: (html) => html
   },
   {
     id: 'minimal',