feat(M1): complete remaining editor features
- Replace StarterKit codeBlock with CodeBlockLowlight (lowlight/common) for syntax highlighting - Add Image extension with URL slash command, drag-and-drop and paste support - Show reading time estimate in editor footer (wordCount / 250 min) - Implement focus mode: sidebar collapses with CSS transition, toggle button in title bar, Esc to exit - Add focusMode state + toggleFocusMode action to Zustand store - Add highlight.js CSS theme (light/dark) and image/sidebar-transition styles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bf2b16c78c
commit
54366cb60c
@ -5,7 +5,7 @@ import { useAppStore } from './stores/appStore'
|
|||||||
import { seedIfEmpty, deduplicateDB } from './db'
|
import { seedIfEmpty, deduplicateDB } from './db'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { loadAll, theme } = useAppStore()
|
const { loadAll, theme, focusMode } = useAppStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme)
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
@ -19,7 +19,9 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full overflow-hidden" style={{ background: 'var(--bg)' }}>
|
<div className="flex h-screen w-full overflow-hidden" style={{ background: 'var(--bg)' }}>
|
||||||
|
<div className={`sidebar-panel${focusMode ? ' hidden' : ''}`} style={{ width: 260 }}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
</div>
|
||||||
<Editor />
|
<Editor />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import TaskList from '@tiptap/extension-task-list'
|
|||||||
import TaskItem from '@tiptap/extension-task-item'
|
import TaskItem from '@tiptap/extension-task-item'
|
||||||
import { Table } from '@tiptap/extension-table'
|
import { Table } from '@tiptap/extension-table'
|
||||||
import TableRow from '@tiptap/extension-table-row'
|
import TableRow from '@tiptap/extension-table-row'
|
||||||
import { WelcomeView } from './WelcomeView'
|
|
||||||
import TableCell from '@tiptap/extension-table-cell'
|
import TableCell from '@tiptap/extension-table-cell'
|
||||||
import TableHeader from '@tiptap/extension-table-header'
|
import TableHeader from '@tiptap/extension-table-header'
|
||||||
import CharacterCount from '@tiptap/extension-character-count'
|
import CharacterCount from '@tiptap/extension-character-count'
|
||||||
@ -16,14 +15,20 @@ import Typography from '@tiptap/extension-typography'
|
|||||||
import Link from '@tiptap/extension-link'
|
import Link from '@tiptap/extension-link'
|
||||||
import Highlight from '@tiptap/extension-highlight'
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
import { TextStyle } from '@tiptap/extension-text-style'
|
import { TextStyle } from '@tiptap/extension-text-style'
|
||||||
|
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'
|
||||||
|
import { Image } from '@tiptap/extension-image'
|
||||||
|
import { createLowlight, common } from 'lowlight'
|
||||||
import {
|
import {
|
||||||
Bold, Italic, Underline as UnderlineIcon, Strikethrough, Code,
|
Bold, Italic, Underline as UnderlineIcon, Strikethrough, Code,
|
||||||
Highlighter, List, ListOrdered, Quote,
|
Highlighter, List, ListOrdered, Quote,
|
||||||
Heading1, Heading2, Heading3, Minus, CheckSquare, Table as TableIcon,
|
Heading1, Heading2, Heading3, Minus, CheckSquare, Table as TableIcon,
|
||||||
Type, Star,
|
Type, Star, ImageIcon, Maximize2, Minimize2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAppStore } from '../../stores/appStore'
|
import { useAppStore } from '../../stores/appStore'
|
||||||
import { countWords } from '../../lib/utils'
|
import { countWords } from '../../lib/utils'
|
||||||
|
import { WelcomeView } from './WelcomeView'
|
||||||
|
|
||||||
|
const lowlight = createLowlight(common)
|
||||||
|
|
||||||
// Slash command items
|
// Slash command items
|
||||||
const SLASH_ITEMS = [
|
const SLASH_ITEMS = [
|
||||||
@ -35,7 +40,11 @@ const SLASH_ITEMS = [
|
|||||||
{ label: '有序列表', desc: '1. 项目', icon: <ListOrdered size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleOrderedList().run() },
|
{ label: '有序列表', desc: '1. 项目', icon: <ListOrdered size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleOrderedList().run() },
|
||||||
{ label: '任务列表', desc: '☑ 待办', icon: <CheckSquare size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleTaskList().run() },
|
{ label: '任务列表', desc: '☑ 待办', icon: <CheckSquare size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleTaskList().run() },
|
||||||
{ label: '引用', desc: '引用文本', icon: <Quote size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleBlockquote().run() },
|
{ label: '引用', desc: '引用文本', icon: <Quote size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleBlockquote().run() },
|
||||||
{ label: '代码块', desc: '多行代码', icon: <Code size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleCodeBlock().run() },
|
{ label: '代码块', desc: '含语法高亮', icon: <Code size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().toggleCodeBlock().run() },
|
||||||
|
{ label: '图片', desc: '插入图片 URL', icon: <ImageIcon size={14} />, action: (e: ReturnType<typeof useEditor>) => {
|
||||||
|
const url = window.prompt('图片地址(URL)')
|
||||||
|
if (url) e?.chain().focus().setImage({ src: url }).run()
|
||||||
|
}},
|
||||||
{ label: '分割线', desc: '水平线', icon: <Minus size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().setHorizontalRule().run() },
|
{ label: '分割线', desc: '水平线', icon: <Minus size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().setHorizontalRule().run() },
|
||||||
{ label: '表格', desc: '插入表格', icon: <TableIcon size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
{ label: '表格', desc: '插入表格', icon: <TableIcon size={14} />, action: (e: ReturnType<typeof useEditor>) => e?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
||||||
]
|
]
|
||||||
@ -67,7 +76,7 @@ function SlashMenu({ items, selectedIndex, onSelect }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Editor() {
|
export function Editor() {
|
||||||
const { activeNoteId, notes, updateNote, toggleStar } = useAppStore()
|
const { activeNoteId, notes, updateNote, toggleStar, focusMode, toggleFocusMode } = useAppStore()
|
||||||
const activeNote = notes.find(n => n.id === activeNoteId)
|
const activeNote = notes.find(n => n.id === activeNoteId)
|
||||||
|
|
||||||
const [title, setTitle] = useState(activeNote?.title ?? '')
|
const [title, setTitle] = useState(activeNote?.title ?? '')
|
||||||
@ -88,6 +97,15 @@ export function Editor() {
|
|||||||
item.label.toLowerCase().includes(slashQuery.toLowerCase())
|
item.label.toLowerCase().includes(slashQuery.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Exit focus mode with Escape
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && focusMode) toggleFocusMode()
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', handler)
|
||||||
|
return () => window.removeEventListener('keydown', handler)
|
||||||
|
}, [focusMode, toggleFocusMode])
|
||||||
|
|
||||||
const scheduleNoteSave = useCallback((content: string, wc: number) => {
|
const scheduleNoteSave = useCallback((content: string, wc: number) => {
|
||||||
if (!activeNoteId || isLoadingRef.current) return
|
if (!activeNoteId || isLoadingRef.current) return
|
||||||
setSaveStatus('unsaved')
|
setSaveStatus('unsaved')
|
||||||
@ -101,7 +119,7 @@ export function Editor() {
|
|||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({ codeBlock: false }),
|
||||||
Placeholder.configure({ placeholder: '开始写作,或输入 / 呼出命令菜单…' }),
|
Placeholder.configure({ placeholder: '开始写作,或输入 / 呼出命令菜单…' }),
|
||||||
Underline,
|
Underline,
|
||||||
TaskList,
|
TaskList,
|
||||||
@ -116,6 +134,8 @@ export function Editor() {
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
Link.configure({ openOnClick: false }),
|
Link.configure({ openOnClick: false }),
|
||||||
BubbleMenu.configure({ pluginKey: 'bubbleMenu' }),
|
BubbleMenu.configure({ pluginKey: 'bubbleMenu' }),
|
||||||
|
CodeBlockLowlight.configure({ lowlight }),
|
||||||
|
Image.configure({ allowBase64: true }),
|
||||||
],
|
],
|
||||||
content: activeNote?.content ? JSON.parse(activeNote.content) : '',
|
content: activeNote?.content ? JSON.parse(activeNote.content) : '',
|
||||||
onUpdate({ editor: ed }) {
|
onUpdate({ editor: ed }) {
|
||||||
@ -150,6 +170,43 @@ export function Editor() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
// Support drag-and-drop image files
|
||||||
|
handleDrop(_view, event) {
|
||||||
|
const files = event.dataTransfer?.files
|
||||||
|
if (!files?.length) return false
|
||||||
|
const file = files[0]
|
||||||
|
if (!file.type.startsWith('image/')) return false
|
||||||
|
event.preventDefault()
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
if (editor && typeof reader.result === 'string') {
|
||||||
|
editor.chain().focus().setImage({ src: reader.result }).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
// Support paste image
|
||||||
|
handlePaste(_view, event) {
|
||||||
|
const items = event.clipboardData?.items
|
||||||
|
if (!items) return false
|
||||||
|
for (const item of Array.from(items)) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
const file = item.getAsFile()
|
||||||
|
if (!file) continue
|
||||||
|
event.preventDefault()
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
if (editor && typeof reader.result === 'string') {
|
||||||
|
editor.chain().focus().setImage({ src: reader.result }).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -224,11 +281,13 @@ export function Editor() {
|
|||||||
return <WelcomeView />
|
return <WelcomeView />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readingTime = Math.max(1, Math.ceil(wordCount / 250))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col min-w-0 h-full" style={{ background: 'var(--bg)' }}>
|
<div className="flex-1 flex flex-col min-w-0 h-full" style={{ background: 'var(--bg)' }}>
|
||||||
{/* Title */}
|
{/* Title row */}
|
||||||
<div className="px-12 pt-10 pb-0 max-w-3xl mx-auto w-full">
|
<div className="px-12 pt-10 pb-0 max-w-3xl mx-auto w-full">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-2">
|
||||||
<input
|
<input
|
||||||
value={title}
|
value={title}
|
||||||
onChange={e => handleTitleChange(e.target.value)}
|
onChange={e => handleTitleChange(e.target.value)}
|
||||||
@ -240,7 +299,6 @@ export function Editor() {
|
|||||||
onClick={() => activeNoteId && toggleStar(activeNoteId)}
|
onClick={() => activeNoteId && toggleStar(activeNoteId)}
|
||||||
title={activeNote.starred ? '取消收藏' : '收藏'}
|
title={activeNote.starred ? '取消收藏' : '收藏'}
|
||||||
className="toolbar-btn shrink-0 mt-2"
|
className="toolbar-btn shrink-0 mt-2"
|
||||||
style={{ width: 28, height: 28 }}
|
|
||||||
>
|
>
|
||||||
<Star
|
<Star
|
||||||
size={16}
|
size={16}
|
||||||
@ -248,19 +306,21 @@ export function Editor() {
|
|||||||
style={{ color: activeNote.starred ? '#f59e0b' : 'var(--text-faint)' }}
|
style={{ color: activeNote.starred ? '#f59e0b' : 'var(--text-faint)' }}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleFocusMode}
|
||||||
|
title={focusMode ? '退出专注模式 (Esc)' : '专注模式'}
|
||||||
|
className="toolbar-btn shrink-0 mt-2"
|
||||||
|
>
|
||||||
|
{focusMode
|
||||||
|
? <Minimize2 size={15} style={{ color: 'var(--accent)' }} />
|
||||||
|
: <Maximize2 size={15} style={{ color: 'var(--text-faint)' }} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating bubble menu rendered via portal */}
|
{/* Floating toolbar */}
|
||||||
{editor && (
|
{editor && (
|
||||||
<div
|
<div style={{ position: 'fixed', zIndex: 100, pointerEvents: 'none' }}>
|
||||||
id="bubble-menu-portal"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
zIndex: 100,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FloatingToolbar editor={editor} />
|
<FloatingToolbar editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -271,15 +331,8 @@ export function Editor() {
|
|||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
|
||||||
{slashOpen && filteredSlash.length > 0 && (
|
{slashOpen && filteredSlash.length > 0 && (
|
||||||
<div
|
<div className="absolute z-50" style={{ top: slashPos.top, left: Math.max(0, slashPos.left) }}>
|
||||||
className="absolute z-50"
|
<SlashMenu items={filteredSlash} selectedIndex={slashIndex} onSelect={executeSlash} />
|
||||||
style={{ top: slashPos.top, left: Math.max(0, slashPos.left) }}
|
|
||||||
>
|
|
||||||
<SlashMenu
|
|
||||||
items={filteredSlash}
|
|
||||||
selectedIndex={slashIndex}
|
|
||||||
onSelect={executeSlash}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -288,13 +341,9 @@ export function Editor() {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-12 py-2 text-xs shrink-0"
|
className="flex items-center justify-between px-12 py-2 text-xs shrink-0"
|
||||||
style={{
|
style={{ borderTop: '1px solid var(--border)', color: 'var(--text-faint)', background: 'var(--bg)' }}
|
||||||
borderTop: '1px solid var(--border)',
|
|
||||||
color: 'var(--text-faint)',
|
|
||||||
background: 'var(--bg)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span>{wordCount} 字</span>
|
<span>{wordCount} 字 · 约 {readingTime} 分钟阅读</span>
|
||||||
<span style={{ color: saveStatus === 'saved' ? 'var(--text-faint)' : 'var(--accent)' }}>
|
<span style={{ color: saveStatus === 'saved' ? 'var(--text-faint)' : 'var(--accent)' }}>
|
||||||
{saveStatus === 'saving' ? '保存中…' : saveStatus === 'unsaved' ? '未保存' : '已自动保存'}
|
{saveStatus === 'saving' ? '保存中…' : saveStatus === 'unsaved' ? '未保存' : '已自动保存'}
|
||||||
</span>
|
</span>
|
||||||
@ -316,24 +365,19 @@ function FloatingToolbar({ editor }: { editor: NonNullable<ReturnType<typeof use
|
|||||||
const start = view.coordsAtPos(from)
|
const start = view.coordsAtPos(from)
|
||||||
const end = view.coordsAtPos(to)
|
const end = view.coordsAtPos(to)
|
||||||
const top = Math.min(start.top, end.top) - 48
|
const top = Math.min(start.top, end.top) - 48
|
||||||
const left = (start.left + end.left) / 2 - 100
|
const left = (start.left + end.left) / 2 - 110
|
||||||
setPos({ top: Math.max(8, top), left: Math.max(8, left) })
|
setPos({ top: Math.max(8, top), left: Math.max(8, left) })
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
}
|
}
|
||||||
editor.on('selectionUpdate', update)
|
editor.on('selectionUpdate', update)
|
||||||
editor.on('blur', () => setVisible(false))
|
editor.on('blur', () => setVisible(false))
|
||||||
return () => {
|
return () => { editor.off('selectionUpdate', update) }
|
||||||
editor.off('selectionUpdate', update)
|
|
||||||
}
|
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
if (!visible) return null
|
if (!visible) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="floating-toolbar" style={{ position: 'fixed', top: pos.top, left: pos.left, pointerEvents: 'auto', zIndex: 100 }}>
|
||||||
className="floating-toolbar"
|
|
||||||
style={{ position: 'fixed', top: pos.top, left: pos.left, pointerEvents: 'auto', zIndex: 100 }}
|
|
||||||
>
|
|
||||||
<ToolbarBtn title="粗体" active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()}><Bold size={13} /></ToolbarBtn>
|
<ToolbarBtn title="粗体" active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()}><Bold size={13} /></ToolbarBtn>
|
||||||
<ToolbarBtn title="斜体" active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()}><Italic size={13} /></ToolbarBtn>
|
<ToolbarBtn title="斜体" active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()}><Italic size={13} /></ToolbarBtn>
|
||||||
<ToolbarBtn title="下划线" active={editor.isActive('underline')} onClick={() => editor.chain().focus().toggleUnderline().run()}><UnderlineIcon size={13} /></ToolbarBtn>
|
<ToolbarBtn title="下划线" active={editor.isActive('underline')} onClick={() => editor.chain().focus().toggleUnderline().run()}><UnderlineIcon size={13} /></ToolbarBtn>
|
||||||
@ -350,17 +394,10 @@ function FloatingToolbar({ editor }: { editor: NonNullable<ReturnType<typeof use
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarBtn({ children, active, title, onClick }: {
|
function ToolbarBtn({ children, active, title, onClick }: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode; active: boolean; title?: string; onClick: () => void
|
||||||
active: boolean
|
|
||||||
title?: string
|
|
||||||
onClick: () => void
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button title={title} onMouseDown={(e) => { e.preventDefault(); onClick() }} className={`toolbar-btn ${active ? 'active' : ''}`}>
|
||||||
title={title}
|
|
||||||
onMouseDown={(e) => { e.preventDefault(); onClick() }}
|
|
||||||
className={`toolbar-btn ${active ? 'active' : ''}`}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -212,3 +212,60 @@
|
|||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||||
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
|
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
|
||||||
|
|
||||||
|
/* ── Sidebar focus-mode transition ── */
|
||||||
|
.sidebar-panel {
|
||||||
|
transition: width 0.25s ease, opacity 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sidebar-panel.hidden {
|
||||||
|
width: 0 !important;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Image in editor ── */
|
||||||
|
.ProseMirror img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ProseMirror img.ProseMirror-selectednode {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Syntax highlighting (highlight.js / lowlight) ── */
|
||||||
|
/* Light theme */
|
||||||
|
:root {
|
||||||
|
--hl-comment: #6a737d;
|
||||||
|
--hl-keyword: #d73a49;
|
||||||
|
--hl-string: #032f62;
|
||||||
|
--hl-number: #005cc5;
|
||||||
|
--hl-function: #6f42c1;
|
||||||
|
--hl-type: #22863a;
|
||||||
|
--hl-meta: #e36209;
|
||||||
|
--hl-builtin: #005cc5;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--hl-comment: #8b949e;
|
||||||
|
--hl-keyword: #ff7b72;
|
||||||
|
--hl-string: #a5d6ff;
|
||||||
|
--hl-number: #79c0ff;
|
||||||
|
--hl-function: #d2a8ff;
|
||||||
|
--hl-type: #7ee787;
|
||||||
|
--hl-meta: #ffa657;
|
||||||
|
--hl-builtin: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment, .hljs-quote { color: var(--hl-comment); font-style: italic; }
|
||||||
|
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: var(--hl-keyword); font-weight: 500; }
|
||||||
|
.hljs-string, .hljs-attr, .hljs-doctag { color: var(--hl-string); }
|
||||||
|
.hljs-number, .hljs-literal, .hljs-boolean { color: var(--hl-number); }
|
||||||
|
.hljs-title, .hljs-function, .hljs-name { color: var(--hl-function); }
|
||||||
|
.hljs-type, .hljs-class, .hljs-tag { color: var(--hl-type); }
|
||||||
|
.hljs-built_in, .hljs-variable, .hljs-params { color: var(--hl-builtin); }
|
||||||
|
.hljs-meta, .hljs-deletion { color: var(--hl-meta); }
|
||||||
|
.hljs-emphasis { font-style: italic; }
|
||||||
|
.hljs-strong { font-weight: bold; }
|
||||||
|
|||||||
@ -9,6 +9,7 @@ interface AppState {
|
|||||||
activeFolderId: string | null | 'all' | 'starred'
|
activeFolderId: string | null | 'all' | 'starred'
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
theme: 'light' | 'dark'
|
theme: 'light' | 'dark'
|
||||||
|
focusMode: boolean
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
loadAll: () => Promise<void>
|
loadAll: () => Promise<void>
|
||||||
@ -25,6 +26,7 @@ interface AppState {
|
|||||||
setActiveFolder: (id: string | null | 'all' | 'starred') => void
|
setActiveFolder: (id: string | null | 'all' | 'starred') => void
|
||||||
setSearch: (q: string) => void
|
setSearch: (q: string) => void
|
||||||
toggleTheme: () => void
|
toggleTheme: () => void
|
||||||
|
toggleFocusMode: () => void
|
||||||
|
|
||||||
filteredNotes: () => Note[]
|
filteredNotes: () => Note[]
|
||||||
}
|
}
|
||||||
@ -36,6 +38,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
activeFolderId: 'all',
|
activeFolderId: 'all',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
theme: (localStorage.getItem('theme') as 'light' | 'dark') || 'light',
|
theme: (localStorage.getItem('theme') as 'light' | 'dark') || 'light',
|
||||||
|
focusMode: false,
|
||||||
|
|
||||||
loadAll: async () => {
|
loadAll: async () => {
|
||||||
const [notes, folders] = await Promise.all([
|
const [notes, folders] = await Promise.all([
|
||||||
@ -128,6 +131,8 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
set({ theme: next })
|
set({ theme: next })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleFocusMode: () => set(s => ({ focusMode: !s.focusMode })),
|
||||||
|
|
||||||
filteredNotes: () => {
|
filteredNotes: () => {
|
||||||
const { notes, activeFolderId, searchQuery } = get()
|
const { notes, activeFolderId, searchQuery } = get()
|
||||||
let result = notes
|
let result = notes
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user