chore: 更新子模块(农历和AI优化)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
11a7e2a06c
commit
e27bb64c7a
@ -1,25 +1,85 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
Textarea,
|
|
||||||
Group,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
Stack,
|
Stack,
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import rehypeSanitize from 'rehype-sanitize';
|
|
||||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
|
||||||
import { useAppStore } from '../../stores';
|
import { useAppStore } from '../../stores';
|
||||||
|
|
||||||
interface NoteEditorProps {
|
interface NoteEditorProps {
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewMode = 'edit' | 'preview';
|
// Simple Markdown to HTML converter
|
||||||
|
function parseMarkdown(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
let html = text;
|
||||||
|
|
||||||
|
// Escape HTML first (but preserve our line breaks)
|
||||||
|
html = html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
// Code blocks (must be before inline code)
|
||||||
|
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
html = html.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>');
|
||||||
|
html = html.replace(/^#####\s+(.*)$/gm, '<h5>$1</h5>');
|
||||||
|
html = html.replace(/^####\s+(.*)$/gm, '<h4>$1</h4>');
|
||||||
|
html = html.replace(/^###\s+(.*)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^##\s+(.*)$/gm, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^#\s+(.*)$/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// Bold (**text** or __text__)
|
||||||
|
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||||
|
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// Italic (*text* or _text_)
|
||||||
|
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||||
|
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// Strikethrough (~~text~~)
|
||||||
|
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||||
|
|
||||||
|
// Links [text](url)
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||||
|
|
||||||
|
// Unordered lists (- item or * item)
|
||||||
|
html = html.replace(/^[-*]\s+(.*)$/gm, '<li>$1</li>');
|
||||||
|
// Wrap consecutive <li> elements in <ul>
|
||||||
|
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => `<ul>${match}</ul>`);
|
||||||
|
|
||||||
|
// Ordered lists (1. item)
|
||||||
|
html = html.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
|
||||||
|
// Wrap consecutive <li> elements in <ol> (but not if already in <ul>)
|
||||||
|
html = html.replace(/(<\/ul>\n?)(<li>\d+\.)/g, '$1$2');
|
||||||
|
html = html.replace(/(<li>\d+\..*<\/li>\n?)+/g, (match) => {
|
||||||
|
// Only wrap if not already inside a ul
|
||||||
|
if (!match.includes('<ul>')) {
|
||||||
|
return `<ol>${match}</ol>`;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blockquotes (> quote)
|
||||||
|
html = html.replace(/^>\s+(.*)$/gm, '<blockquote>$1</blockquote>');
|
||||||
|
// Wrap consecutive blockquotes
|
||||||
|
html = html.replace(/(<blockquote>.*<\/blockquote>\n?)+/g, (match) => match);
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
html = html.replace(/^---$/gm, '<hr>');
|
||||||
|
|
||||||
|
// Line breaks (convert \n to <br>)
|
||||||
|
html = html.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
export function NoteEditor({ onSave }: NoteEditorProps) {
|
export function NoteEditor({ onSave }: NoteEditorProps) {
|
||||||
const notes = useAppStore((state) => state.notes);
|
const notes = useAppStore((state) => state.notes);
|
||||||
@ -28,255 +88,141 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
|
|||||||
|
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
const [lastSaved, setLastSaved] = useState<number | null>(null);
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [viewMode] = useState<ViewMode>('edit');
|
const [now, setNow] = useState(Date.now());
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
// Initialize content from notes
|
// Initialize content from notes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (notes) {
|
if (notes?.content) {
|
||||||
setContent(notes.content);
|
setContent(notes.content);
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
}
|
}
|
||||||
}, [notes]);
|
}, [notes]);
|
||||||
|
|
||||||
// Track unsaved changes
|
// Timer to update time display every second
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (notes && content !== notes.content) {
|
const interval = setInterval(() => {
|
||||||
setHasUnsavedChanges(true);
|
setNow(Date.now());
|
||||||
}
|
}, 1000);
|
||||||
}, [content, notes]);
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch notes on mount
|
// Fetch notes on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchNotes();
|
fetchNotes();
|
||||||
}, [fetchNotes]);
|
}, [fetchNotes]);
|
||||||
|
|
||||||
// Auto-save with 3 second debounce
|
// Debounce content for auto-save
|
||||||
const [debouncedContent] = useDebouncedValue(content, 3000);
|
const [debouncedContent] = useDebouncedValue(content, 2000);
|
||||||
|
|
||||||
// 滚动条样式 - 仅在悬停时显示
|
// Save function
|
||||||
const scrollbarStyle = `
|
const handleSave = useCallback(async (value: string) => {
|
||||||
.note-scroll::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
.note-scroll::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.note-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.note-scroll:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const handleSave = useCallback(
|
|
||||||
async (value: string) => {
|
|
||||||
// 即使 notes 为 null,saveNotes 也会自动创建便签
|
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
// 防止重复保存
|
|
||||||
if (saving) return;
|
if (saving) return;
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
// 注意:不要在这里调用 updateNotesContent
|
|
||||||
// 因为 saveNotes 会在成功后更新 store 中的 notes
|
|
||||||
// 如果这里先更新,会触发 notes useEffect,导致 content 被重置
|
|
||||||
await saveNotes(value);
|
await saveNotes(value);
|
||||||
setLastSaved(new Date());
|
setLastSaved(Date.now());
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
onSave?.();
|
onSave?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save note:', error);
|
console.error('Failed to save note:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
},
|
}, [saving, saveNotes, onSave]);
|
||||||
[saving, saveNotes, onSave]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Auto-save with debounce
|
// Auto-save when debounced content changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 即使 notes 为 null 也可以自动保存(会创建新便签)
|
if (debouncedContent && debouncedContent !== notes?.content) {
|
||||||
if (debouncedContent !== undefined && debouncedContent !== content) {
|
|
||||||
handleSave(debouncedContent);
|
handleSave(debouncedContent);
|
||||||
}
|
}
|
||||||
}, [debouncedContent, content, handleSave]);
|
}, [debouncedContent, handleSave, notes?.content]);
|
||||||
|
|
||||||
|
// Format saved time
|
||||||
const formatLastSaved = () => {
|
const formatLastSaved = () => {
|
||||||
if (saving) return '保存中...';
|
if (saving) return '保存中';
|
||||||
if (hasUnsavedChanges && !lastSaved) return '未保存';
|
|
||||||
if (!lastSaved) return '';
|
if (!lastSaved) return '';
|
||||||
const now = new Date();
|
const diff = now - lastSaved;
|
||||||
const diff = now.getTime() - lastSaved.getTime();
|
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前`;
|
||||||
if (diff < 1000) return '已保存';
|
return new Date(lastSaved).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||||
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`;
|
|
||||||
return lastSaved.toLocaleTimeString('zh-CN');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManualSave = () => {
|
// Enter edit mode
|
||||||
if (content) {
|
const handleClick = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
setTimeout(() => textareaRef.current?.focus(), 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exit edit mode and save
|
||||||
|
const handleBlur = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
// Blur 后立即保存
|
||||||
|
if (content !== notes?.content) {
|
||||||
handleSave(content);
|
handleSave(content);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// 处理滚轮事件,实现列表独立滚动
|
|
||||||
const handleWheel = (e: React.WheelEvent) => {
|
|
||||||
const container = scrollContainerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
||||||
// 使用 1px 缓冲避免浮点数精度问题
|
|
||||||
const isAtTop = scrollTop <= 0;
|
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
|
||||||
|
|
||||||
// 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡
|
|
||||||
if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
// 如果在滚动范围内,允许事件继续传递以实现正常滚动
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper p="md" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<Paper p="sm" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fafafa' }}>
|
||||||
<style>{scrollbarStyle}</style>
|
<Stack gap="xs" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||||
<Stack gap="sm" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group justify="space-between" style={{ flexShrink: 0 }}>
|
<Group justify="space-between" style={{ flexShrink: 0 }}>
|
||||||
<Group gap="sm">
|
<Text fw={500} size="sm" c="#333">便签</Text>
|
||||||
<Text fw={400} size="sm" style={{ letterSpacing: '0.1em', color: '#1a1a1a' }}>
|
<Text size="xs" c="#999">{formatLastSaved()}</Text>
|
||||||
便签
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="outline"
|
|
||||||
leftSection={<IconDeviceFloppy size={12} />}
|
|
||||||
onClick={handleManualSave}
|
|
||||||
loading={saving}
|
|
||||||
style={{
|
|
||||||
borderColor: '#ccc',
|
|
||||||
color: '#1a1a1a',
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<Text size="xs" c={saving ? '#666' : hasUnsavedChanges ? '#e6a23c' : '#999'}>
|
|
||||||
{formatLastSaved()}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Editor/Preview Area */}
|
{/* Content Area */}
|
||||||
<Box
|
<Box
|
||||||
ref={scrollContainerRef}
|
style={{ flex: 1, minHeight: 0, position: 'relative' }}
|
||||||
onWheel={handleWheel}
|
onClick={handleClick}
|
||||||
data-scroll-container="note"
|
|
||||||
className="note-scroll"
|
|
||||||
style={{ flex: 1, minHeight: 0, display: 'flex', overflowY: 'auto' }}
|
|
||||||
>
|
>
|
||||||
{viewMode === 'edit' && (
|
{isEditing ? (
|
||||||
<Textarea
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
placeholder="在这里记录你的想法... 支持 Markdown 语法: - # 一级标题 - **粗体** - *斜体* - [链接](url)"
|
onBlur={handleBlur}
|
||||||
autosize
|
placeholder="随手记录...(支持换行)"
|
||||||
minRows={8}
|
style={{
|
||||||
styles={{
|
width: '100%',
|
||||||
input: {
|
height: '100%',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
fontFamily: 'Monaco, Menlo, monospace',
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
background: 'transparent',
|
background: '#fff',
|
||||||
'&:focus': { outline: 'none' },
|
padding: '8px',
|
||||||
},
|
borderRadius: '4px',
|
||||||
}}
|
outline: 'none',
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
|
|
||||||
{viewMode === 'preview' && (
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '4px',
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
|
cursor: 'text',
|
||||||
|
fontFamily: 'Monaco, Menlo, monospace',
|
||||||
}}
|
}}
|
||||||
|
className="markdown-preview"
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{content ? (
|
{content ? (
|
||||||
<Box
|
<span
|
||||||
style={{
|
dangerouslySetInnerHTML={{ __html: parseMarkdown(content) }}
|
||||||
'& h1, & h2, & h3': {
|
style={{ display: 'block' }}
|
||||||
marginTop: '1em',
|
/>
|
||||||
marginBottom: '0.5em',
|
|
||||||
fontWeight: 500,
|
|
||||||
},
|
|
||||||
'& h1': { fontSize: '1.25em', borderBottom: '1px solid rgba(0,0,0,0.08)', paddingBottom: '0.3em' },
|
|
||||||
'& h2': { fontSize: '1.1em', borderBottom: '1px solid rgba(0,0,0,0.06)', paddingBottom: '0.3em' },
|
|
||||||
'& p': { marginBottom: '0.8em', color: '#333' },
|
|
||||||
'& ul, & ol': { paddingLeft: '1.5em', marginBottom: '0.8em' },
|
|
||||||
'& li': { marginBottom: '0.3em', color: '#333' },
|
|
||||||
'& blockquote': {
|
|
||||||
borderLeft: '3px solid rgba(0,0,0,0.1)',
|
|
||||||
paddingLeft: '1em',
|
|
||||||
marginLeft: 0,
|
|
||||||
color: '#888',
|
|
||||||
},
|
|
||||||
'& code': {
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
||||||
padding: '0.2em 0.4em',
|
|
||||||
borderRadius: '2px',
|
|
||||||
fontSize: '0.9em',
|
|
||||||
},
|
|
||||||
'& pre': {
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.03)',
|
|
||||||
padding: '1em',
|
|
||||||
borderRadius: '2px',
|
|
||||||
overflow: 'auto',
|
|
||||||
},
|
|
||||||
'& pre code': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
'& a': {
|
|
||||||
color: '#1a1a1a',
|
|
||||||
textDecoration: 'underline',
|
|
||||||
textUnderlineOffset: 2,
|
|
||||||
},
|
|
||||||
'& hr': {
|
|
||||||
border: 'none',
|
|
||||||
borderTop: '1px solid rgba(0,0,0,0.06)',
|
|
||||||
margin: '1em 0',
|
|
||||||
},
|
|
||||||
} as any}
|
|
||||||
>
|
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
|
|
||||||
{content}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<Text c="#999" ta="center" py="xl" size="sm" style={{ letterSpacing: '0.05em' }}>
|
<Text c="#aaa" size="sm">点击记录...</Text>
|
||||||
暂无内容
|
|
||||||
<br />
|
|
||||||
<Text size="xs" c="#bbb" mt={4}>
|
|
||||||
使用 Markdown 格式记录你的想法
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
108
src/index.css
108
src/index.css
@ -66,3 +66,111 @@
|
|||||||
.mantine-DatePicker-calendarHeader {
|
.mantine-DatePicker-calendarHeader {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 便签 Markdown 预览样式 */
|
||||||
|
.markdown-preview h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview a {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview li {
|
||||||
|
margin: 0.3em 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview ul li {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview ol li {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 3px solid #ddd;
|
||||||
|
background: #f9f9f9;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.8em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: Monaco, Menlo, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user