mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-02 03:42:38 +02:00
Tiptap integrated
This commit is contained in:
parent
8834238768
commit
23ae0d8be0
7 changed files with 1234 additions and 16 deletions
|
|
@ -25,6 +25,11 @@
|
|||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tiptap/extension-link": "^3.15.3",
|
||||
"@tiptap/extension-placeholder": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3",
|
||||
"@tiptap/react": "^3.15.3",
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@x/preload": "workspace:*",
|
||||
"@x/shared": "workspace:*",
|
||||
"ai": "^5.0.117",
|
||||
|
|
@ -39,6 +44,7 @@
|
|||
"streamdown": "^1.6.10",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"zod": "^4.2.1"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { workspace } from '@x/shared';
|
||||
import { RunEvent } from '@x/shared/src/runs.js';
|
||||
import type { ChatStatus, LanguageModelUsage, ToolUIPart } from 'ai';
|
||||
import './App.css'
|
||||
import z from 'zod';
|
||||
import { Button } from './components/ui/button';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { MessageSquare, CheckIcon, LoaderIcon } from 'lucide-react';
|
||||
import { MarkdownEditor } from './components/markdown-editor';
|
||||
import { useDebounce } from './hooks/use-debounce';
|
||||
import { SidebarIcon } from '@/components/sidebar-icon';
|
||||
import { SidebarContentPanel } from '@/components/sidebar-content';
|
||||
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
|
||||
|
|
@ -224,9 +226,16 @@ function App() {
|
|||
// File browser state (for Knowledge section)
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||
const [fileContent, setFileContent] = useState<string>('')
|
||||
const [editorContent, setEditorContent] = useState<string>('')
|
||||
const [tree, setTree] = useState<TreeNode[]>([])
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set())
|
||||
|
||||
// Auto-save state
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null)
|
||||
const debouncedContent = useDebounce(editorContent, 500)
|
||||
const initialContentRef = useRef<string>('')
|
||||
|
||||
// Chat state
|
||||
const [message, setMessage] = useState<string>('')
|
||||
const [conversation, setConversation] = useState<ConversationItem[]>([])
|
||||
|
|
@ -268,6 +277,9 @@ function App() {
|
|||
useEffect(() => {
|
||||
if (!selectedPath) {
|
||||
setFileContent('')
|
||||
setEditorContent('')
|
||||
initialContentRef.current = ''
|
||||
setLastSaved(null)
|
||||
return
|
||||
}
|
||||
(async () => {
|
||||
|
|
@ -276,16 +288,48 @@ function App() {
|
|||
if (stat.kind === 'file') {
|
||||
const result = await window.ipc.invoke('workspace:readFile', { path: selectedPath })
|
||||
setFileContent(result.data)
|
||||
setEditorContent(result.data)
|
||||
initialContentRef.current = result.data
|
||||
setLastSaved(null)
|
||||
} else {
|
||||
setFileContent('')
|
||||
setEditorContent('')
|
||||
initialContentRef.current = ''
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load file:', err)
|
||||
setFileContent('')
|
||||
setEditorContent('')
|
||||
initialContentRef.current = ''
|
||||
}
|
||||
})()
|
||||
}, [selectedPath])
|
||||
|
||||
// Auto-save when content changes
|
||||
useEffect(() => {
|
||||
if (!selectedPath || !selectedPath.endsWith('.md')) return
|
||||
if (debouncedContent === initialContentRef.current) return
|
||||
if (!debouncedContent) return
|
||||
|
||||
const saveFile = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await window.ipc.invoke('workspace:writeFile', {
|
||||
path: selectedPath,
|
||||
data: debouncedContent,
|
||||
opts: { encoding: 'utf8' }
|
||||
})
|
||||
initialContentRef.current = debouncedContent
|
||||
setLastSaved(new Date())
|
||||
} catch (err) {
|
||||
console.error('Failed to save file:', err)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
saveFile()
|
||||
}, [debouncedContent, selectedPath])
|
||||
|
||||
// Listen to run events
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
||||
|
|
@ -685,24 +729,48 @@ function App() {
|
|||
{selectedPath ? selectedPath : 'Chat'}
|
||||
</span>
|
||||
{selectedPath && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedPath(null)}
|
||||
className="ml-auto text-foreground"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Back to Chat
|
||||
</Button>
|
||||
<>
|
||||
{/* Save status indicator */}
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
{isSaving ? (
|
||||
<>
|
||||
<LoaderIcon className="h-3 w-3 animate-spin" />
|
||||
<span>Saving...</span>
|
||||
</>
|
||||
) : lastSaved ? (
|
||||
<>
|
||||
<CheckIcon className="h-3 w-3 text-green-500" />
|
||||
<span>Saved</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedPath(null)}
|
||||
className="ml-auto text-foreground"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Back to Chat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{selectedPath ? (
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap">
|
||||
{fileContent || 'Loading...'}
|
||||
</pre>
|
||||
</div>
|
||||
selectedPath.endsWith('.md') ? (
|
||||
<MarkdownEditor
|
||||
content={editorContent}
|
||||
onChange={setEditorContent}
|
||||
placeholder="Start writing..."
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap">
|
||||
{fileContent || 'Loading...'}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<Conversation className="relative flex-1 overflow-y-auto">
|
||||
|
|
|
|||
181
apps/x/apps/renderer/src/components/editor-toolbar.tsx
Normal file
181
apps/x/apps/renderer/src/components/editor-toolbar.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import type { Editor } from '@tiptap/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
StrikethroughIcon,
|
||||
CodeIcon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
QuoteIcon,
|
||||
MinusIcon,
|
||||
LinkIcon,
|
||||
CodeSquareIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface EditorToolbarProps {
|
||||
editor: Editor | null
|
||||
}
|
||||
|
||||
export function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||
if (!editor) return null
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
if (url === null) return
|
||||
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
return
|
||||
}
|
||||
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-toolbar">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
data-active={editor.isActive('bold') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Bold"
|
||||
>
|
||||
<BoldIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
data-active={editor.isActive('italic') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Italic"
|
||||
>
|
||||
<ItalicIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
data-active={editor.isActive('strike') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Strikethrough"
|
||||
>
|
||||
<StrikethroughIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
data-active={editor.isActive('code') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Inline Code"
|
||||
>
|
||||
<CodeIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
data-active={editor.isActive('heading', { level: 1 }) || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Heading 1"
|
||||
>
|
||||
<Heading1Icon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
data-active={editor.isActive('heading', { level: 2 }) || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Heading 2"
|
||||
>
|
||||
<Heading2Icon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
data-active={editor.isActive('heading', { level: 3 }) || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Heading 3"
|
||||
>
|
||||
<Heading3Icon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
data-active={editor.isActive('bulletList') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Bullet List"
|
||||
>
|
||||
<ListIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
data-active={editor.isActive('orderedList') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Ordered List"
|
||||
>
|
||||
<ListOrderedIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
data-active={editor.isActive('blockquote') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Blockquote"
|
||||
>
|
||||
<QuoteIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
data-active={editor.isActive('codeBlock') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Code Block"
|
||||
>
|
||||
<CodeSquareIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
title="Horizontal Rule"
|
||||
>
|
||||
<MinusIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={setLink}
|
||||
data-active={editor.isActive('link') || undefined}
|
||||
className="data-[active]:bg-accent"
|
||||
title="Link"
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
apps/x/apps/renderer/src/components/markdown-editor.tsx
Normal file
81
apps/x/apps/renderer/src/components/markdown-editor.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
import { useEffect, useCallback, useRef } from 'react'
|
||||
import { EditorToolbar } from './editor-toolbar'
|
||||
import '@/styles/editor.css'
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
content: string
|
||||
onChange: (markdown: string) => void
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function MarkdownEditor({ content, onChange, placeholder = 'Start writing...' }: MarkdownEditorProps) {
|
||||
const isInternalUpdate = useRef(false)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
levels: [1, 2, 3],
|
||||
},
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
rel: 'noopener noreferrer',
|
||||
target: '_blank',
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: false,
|
||||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
],
|
||||
content: '',
|
||||
onUpdate: ({ editor }) => {
|
||||
if (isInternalUpdate.current) return
|
||||
const markdown = editor.storage.markdown.getMarkdown()
|
||||
onChange(markdown)
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-sm max-w-none focus:outline-none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Update editor content when prop changes (e.g., file selection changes)
|
||||
useEffect(() => {
|
||||
if (editor && content !== undefined) {
|
||||
const currentContent = editor.storage.markdown?.getMarkdown() || ''
|
||||
if (currentContent !== content) {
|
||||
isInternalUpdate.current = true
|
||||
editor.commands.setContent(content)
|
||||
isInternalUpdate.current = false
|
||||
}
|
||||
}
|
||||
}, [editor, content])
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||
if (event.key === 's' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
// The parent component handles saving via onChange
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
||||
<EditorToolbar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
apps/x/apps/renderer/src/hooks/use-debounce.ts
Normal file
23
apps/x/apps/renderer/src/hooks/use-debounce.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Debounce a value by a specified delay
|
||||
* @param value The value to debounce
|
||||
* @param delay The delay in milliseconds
|
||||
* @returns The debounced value
|
||||
*/
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
214
apps/x/apps/renderer/src/styles/editor.css
Normal file
214
apps/x/apps/renderer/src/styles/editor.css
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/* Tiptap Editor Styles */
|
||||
|
||||
.tiptap-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.tiptap-editor .ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: var(--muted-foreground);
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.tiptap-editor .ProseMirror {
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror > * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
.tiptap-editor .ProseMirror h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror h1:first-child,
|
||||
.tiptap-editor .ProseMirror h2:first-child,
|
||||
.tiptap-editor .ProseMirror h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
.tiptap-editor .ProseMirror p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.tiptap-editor .ProseMirror ul,
|
||||
.tiptap-editor .ProseMirror ol {
|
||||
padding-left: 1.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror li p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Blockquote */
|
||||
.tiptap-editor .ProseMirror blockquote {
|
||||
border-left: 3px solid var(--border);
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
.tiptap-editor .ProseMirror code {
|
||||
background-color: var(--muted);
|
||||
border-radius: 0.25em;
|
||||
padding: 0.15em 0.3em;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Code Block */
|
||||
.tiptap-editor .ProseMirror pre {
|
||||
background-color: var(--muted);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.875em;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Horizontal Rule */
|
||||
.tiptap-editor .ProseMirror hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.tiptap-editor .ProseMirror a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Strong and Emphasis */
|
||||
.tiptap-editor .ProseMirror strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Task Lists */
|
||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror ul[data-type="taskList"] li > label {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
.tiptap-editor .ProseMirror ::selection {
|
||||
background-color: var(--ring);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.editor-toolbar .separator {
|
||||
width: 1px;
|
||||
height: 1.5rem;
|
||||
background-color: var(--border);
|
||||
margin: 0 0.25rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Content area centering */
|
||||
.tiptap-editor .ProseMirror {
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue