mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
inline task agent v1
This commit is contained in:
parent
5aba6025dc
commit
bd4cc1145d
13 changed files with 1221 additions and 6 deletions
|
|
@ -32,6 +32,7 @@ import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.
|
|||
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
|
||||
import { search } from '@x/core/dist/search/search.js';
|
||||
import { versionHistory } from '@x/core';
|
||||
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
|
||||
|
||||
type InvokeChannels = ipc.InvokeChannels;
|
||||
type IPCChannels = ipc.IPCChannels;
|
||||
|
|
@ -531,5 +532,10 @@ export function setupIpcHandlers() {
|
|||
'search:query': async (_event, args) => {
|
||||
return search(args.query, args.limit, args.types);
|
||||
},
|
||||
// Inline task schedule classification
|
||||
'inline-task:classifySchedule': async (_event, args) => {
|
||||
const schedule = await classifySchedule(args.instruction);
|
||||
return { schedule };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js"
|
|||
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
|
||||
import { init as initEmailLabeling } from "@x/core/dist/knowledge/label_emails.js";
|
||||
import { init as initNoteTagging } from "@x/core/dist/knowledge/tag_notes.js";
|
||||
import { init as initInlineTasks } from "@x/core/dist/knowledge/inline_tasks.js";
|
||||
import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js";
|
||||
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
|
||||
import started from "electron-squirrel-startup";
|
||||
|
|
@ -178,6 +179,9 @@ app.whenReady().then(async () => {
|
|||
// start note tagging service
|
||||
initNoteTagging();
|
||||
|
||||
// start inline task service (@rowboat: mentions)
|
||||
initInlineTasks();
|
||||
|
||||
// start background agent runner (scheduled agents)
|
||||
initAgentRunner();
|
||||
|
||||
|
|
|
|||
|
|
@ -1017,10 +1017,13 @@ function App() {
|
|||
frontmatterByPathRef.current.set(pathToLoad, fm)
|
||||
const normalizeForCompare = (s: string) => s.split('\n').map(line => line.trimEnd()).join('\n').trim()
|
||||
const isSameEditorFile = editorPathRef.current === pathToLoad
|
||||
const wouldClobberActiveEdits =
|
||||
isSameEditorFile
|
||||
&& normalizeForCompare(editorContentRef.current) !== normalizeForCompare(body)
|
||||
if (!wouldClobberActiveEdits) {
|
||||
const knownBaseline = initialContentByPathRef.current.get(pathToLoad)
|
||||
const hasKnownBaseline = knownBaseline !== undefined
|
||||
const hasUnsavedEdits =
|
||||
hasKnownBaseline
|
||||
&& normalizeForCompare(editorContentRef.current) !== normalizeForCompare(knownBaseline)
|
||||
const shouldPreserveActiveDraft = isSameEditorFile && hasUnsavedEdits
|
||||
if (!shouldPreserveActiveDraft) {
|
||||
setEditorContent(body)
|
||||
if (pathToLoad.endsWith('.md')) {
|
||||
setEditorCacheForPath(pathToLoad, body)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Placeholder from '@tiptap/extension-placeholder'
|
|||
import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import { ImageUploadPlaceholderExtension, createImageUploadHandler } from '@/extensions/image-upload'
|
||||
import { TaskBlockExtension } from '@/extensions/task-block'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
|
||||
|
||||
|
|
@ -133,6 +134,8 @@ function getMarkdownWithBlankLines(editor: Editor): string {
|
|||
})
|
||||
})
|
||||
blocks.push(listLines.join('\n'))
|
||||
} else if (node.type === 'taskBlock') {
|
||||
blocks.push('```task\n' + (node.attrs?.data as string || '{}') + '\n```')
|
||||
} else if (node.type === 'codeBlock') {
|
||||
const lang = (node.attrs?.language as string) || ''
|
||||
blocks.push('```' + lang + '\n' + nodeToText(node) + '\n```')
|
||||
|
|
@ -181,8 +184,21 @@ import { WikiLink } from '@/extensions/wiki-link'
|
|||
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
||||
import { Command, CommandEmpty, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { ensureMarkdownExtension, normalizeWikiPath, wikiLabel } from '@/lib/wiki-links'
|
||||
import { extractAllFrontmatterValues, buildFrontmatter } from '@/lib/frontmatter'
|
||||
import { RowboatMentionPopover } from './rowboat-mention-popover'
|
||||
import '@/styles/editor.css'
|
||||
|
||||
type RowboatMentionMatch = {
|
||||
range: { from: number; to: number }
|
||||
}
|
||||
|
||||
type RowboatBlockEdit = {
|
||||
/** ProseMirror position of the taskBlock node */
|
||||
nodePos: number
|
||||
/** Existing instruction text */
|
||||
existingText: string
|
||||
}
|
||||
|
||||
type WikiLinkConfig = {
|
||||
files: string[]
|
||||
recent: string[]
|
||||
|
|
@ -304,6 +320,10 @@ export function MarkdownEditor({
|
|||
const onPrimaryHeadingCommitRef = useRef(onPrimaryHeadingCommit)
|
||||
const wikiKeyStateRef = useRef<{ open: boolean; options: string[]; value: string }>({ open: false, options: [], value: '' })
|
||||
const handleSelectWikiLinkRef = useRef<(path: string) => void>(() => {})
|
||||
const [activeRowboatMention, setActiveRowboatMention] = useState<RowboatMentionMatch | null>(null)
|
||||
const [rowboatBlockEdit, setRowboatBlockEdit] = useState<RowboatBlockEdit | null>(null)
|
||||
const [rowboatAnchorTop, setRowboatAnchorTop] = useState<{ top: number; left: number; width: number } | null>(null)
|
||||
const rowboatBlockEditRef = useRef<RowboatBlockEdit | null>(null)
|
||||
|
||||
// Keep ref in sync with state for the plugin to access
|
||||
selectionHighlightRef.current = selectionHighlight
|
||||
|
|
@ -399,6 +419,7 @@ export function MarkdownEditor({
|
|||
},
|
||||
}),
|
||||
ImageUploadPlaceholderExtension,
|
||||
TaskBlockExtension,
|
||||
WikiLink.configure({
|
||||
onCreate: wikiLinks?.onCreate
|
||||
? (path) => {
|
||||
|
|
@ -492,7 +513,7 @@ export function MarkdownEditor({
|
|||
|
||||
return false
|
||||
},
|
||||
handleClickOn: (_view, _pos, node, _nodePos, event) => {
|
||||
handleClickOn: (_view, _pos, node, nodePos, event) => {
|
||||
if (node.type.name === 'wikiLink') {
|
||||
event.preventDefault()
|
||||
wikiLinks?.onOpen?.(node.attrs.path)
|
||||
|
|
@ -575,6 +596,55 @@ export function MarkdownEditor({
|
|||
})
|
||||
}, [editor, wikiLinks])
|
||||
|
||||
const updateRowboatMentionState = useCallback(() => {
|
||||
if (!editor) return
|
||||
const { selection } = editor.state
|
||||
if (!selection.empty) {
|
||||
setActiveRowboatMention(null)
|
||||
setRowboatAnchorTop(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { $from } = selection
|
||||
if ($from.parent.type.spec.code) {
|
||||
setActiveRowboatMention(null)
|
||||
setRowboatAnchorTop(null)
|
||||
return
|
||||
}
|
||||
|
||||
const text = $from.parent.textBetween(0, $from.parent.content.size, '\n', '\n')
|
||||
const textBefore = text.slice(0, $from.parentOffset)
|
||||
|
||||
// Match @rowboat at a word boundary (preceded by nothing or whitespace)
|
||||
const match = textBefore.match(/(^|\s)@rowboat$/)
|
||||
if (!match) {
|
||||
setActiveRowboatMention(null)
|
||||
setRowboatAnchorTop(null)
|
||||
return
|
||||
}
|
||||
|
||||
const triggerStart = textBefore.length - '@rowboat'.length
|
||||
const from = selection.from - (textBefore.length - triggerStart)
|
||||
const to = selection.from
|
||||
setActiveRowboatMention({ range: { from, to } })
|
||||
|
||||
const wrapper = wrapperRef.current
|
||||
if (!wrapper) {
|
||||
setRowboatAnchorTop(null)
|
||||
return
|
||||
}
|
||||
|
||||
const coords = editor.view.coordsAtPos(selection.from)
|
||||
const wrapperRect = wrapper.getBoundingClientRect()
|
||||
const proseMirrorEl = wrapper.querySelector('.ProseMirror') as HTMLElement | null
|
||||
const pmRect = proseMirrorEl?.getBoundingClientRect()
|
||||
setRowboatAnchorTop({
|
||||
top: coords.top - wrapperRect.top + wrapper.scrollTop,
|
||||
left: pmRect ? pmRect.left - wrapperRect.left : 0,
|
||||
width: pmRect ? pmRect.width : wrapperRect.width,
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !wikiLinks) return
|
||||
editor.on('update', updateWikiLinkState)
|
||||
|
|
@ -585,6 +655,32 @@ export function MarkdownEditor({
|
|||
}
|
||||
}, [editor, wikiLinks, updateWikiLinkState])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return
|
||||
editor.on('update', updateRowboatMentionState)
|
||||
editor.on('selectionUpdate', updateRowboatMentionState)
|
||||
return () => {
|
||||
editor.off('update', updateRowboatMentionState)
|
||||
editor.off('selectionUpdate', updateRowboatMentionState)
|
||||
}
|
||||
}, [editor, updateRowboatMentionState])
|
||||
|
||||
// When a tell-rowboat block is clicked, compute anchor and open popover
|
||||
useEffect(() => {
|
||||
if (!rowboatBlockEdit || !editor) return
|
||||
const wrapper = wrapperRef.current
|
||||
if (!wrapper) return
|
||||
const coords = editor.view.coordsAtPos(rowboatBlockEdit.nodePos)
|
||||
const wrapperRect = wrapper.getBoundingClientRect()
|
||||
const proseMirrorEl = wrapper.querySelector('.ProseMirror') as HTMLElement | null
|
||||
const pmRect = proseMirrorEl?.getBoundingClientRect()
|
||||
setRowboatAnchorTop({
|
||||
top: coords.top - wrapperRect.top + wrapper.scrollTop,
|
||||
left: pmRect ? pmRect.left - wrapperRect.left : 0,
|
||||
width: pmRect ? pmRect.width : wrapperRect.width,
|
||||
})
|
||||
}, [editor, rowboatBlockEdit])
|
||||
|
||||
// Update editor content when prop changes (e.g., file selection changes)
|
||||
useEffect(() => {
|
||||
if (editor && content !== undefined) {
|
||||
|
|
@ -675,6 +771,85 @@ export function MarkdownEditor({
|
|||
handleSelectWikiLinkRef.current = handleSelectWikiLink
|
||||
}, [handleSelectWikiLink])
|
||||
|
||||
const handleRowboatAdd = useCallback(async (instruction: string) => {
|
||||
if (!editor) return
|
||||
|
||||
if (rowboatBlockEdit) {
|
||||
// Editing existing taskBlock — update its data attribute
|
||||
const { nodePos } = rowboatBlockEdit
|
||||
const node = editor.state.doc.nodeAt(nodePos)
|
||||
if (node && node.type.name === 'taskBlock') {
|
||||
// Preserve existing schedule data
|
||||
let updated: Record<string, unknown> = { instruction }
|
||||
try {
|
||||
const existing = JSON.parse(node.attrs.data || '{}')
|
||||
updated = { ...existing, instruction }
|
||||
} catch {
|
||||
// Invalid JSON — just write new
|
||||
}
|
||||
const tr = editor.state.tr.setNodeMarkup(nodePos, undefined, { data: JSON.stringify(updated) })
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
setRowboatBlockEdit(null)
|
||||
rowboatBlockEditRef.current = null
|
||||
setRowboatAnchorTop(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (activeRowboatMention) {
|
||||
// Classify schedule intent for new blocks
|
||||
const blockData: Record<string, unknown> = { instruction }
|
||||
try {
|
||||
const result = await window.ipc.invoke('inline-task:classifySchedule', { instruction })
|
||||
if (result.schedule) {
|
||||
const { label, ...rest } = result.schedule
|
||||
blockData.schedule = rest
|
||||
blockData['schedule-label'] = label
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RowboatAdd] Schedule classification failed:', error)
|
||||
}
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(
|
||||
{ from: activeRowboatMention.range.from, to: activeRowboatMention.range.to },
|
||||
[
|
||||
{ type: 'taskBlock', attrs: { data: JSON.stringify(blockData) } },
|
||||
{ type: 'paragraph' },
|
||||
],
|
||||
)
|
||||
.run()
|
||||
|
||||
// Mark note as live
|
||||
if (onFrontmatterChange) {
|
||||
const fields = extractAllFrontmatterValues(frontmatter ?? null)
|
||||
fields['live_note'] = 'true'
|
||||
onFrontmatterChange(buildFrontmatter(fields))
|
||||
}
|
||||
|
||||
setActiveRowboatMention(null)
|
||||
setRowboatAnchorTop(null)
|
||||
}
|
||||
}, [editor, activeRowboatMention, rowboatBlockEdit, frontmatter, onFrontmatterChange])
|
||||
|
||||
const handleRowboatRemove = useCallback(() => {
|
||||
if (!editor || !rowboatBlockEdit) return
|
||||
const { nodePos } = rowboatBlockEdit
|
||||
const node = editor.state.doc.nodeAt(nodePos)
|
||||
if (node) {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange({ from: nodePos, to: nodePos + node.nodeSize })
|
||||
.run()
|
||||
}
|
||||
setRowboatBlockEdit(null)
|
||||
rowboatBlockEditRef.current = null
|
||||
setRowboatAnchorTop(null)
|
||||
}, [editor, rowboatBlockEdit])
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
updateWikiLinkState()
|
||||
}, [updateWikiLinkState])
|
||||
|
|
@ -789,6 +964,19 @@ export function MarkdownEditor({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null}
|
||||
<RowboatMentionPopover
|
||||
open={Boolean((activeRowboatMention || rowboatBlockEdit) && rowboatAnchorTop)}
|
||||
anchor={rowboatAnchorTop}
|
||||
initialText={rowboatBlockEdit?.existingText ?? ''}
|
||||
onAdd={handleRowboatAdd}
|
||||
onRemove={rowboatBlockEdit ? handleRowboatRemove : undefined}
|
||||
onClose={() => {
|
||||
setActiveRowboatMention(null)
|
||||
setRowboatBlockEdit(null)
|
||||
rowboatBlockEditRef.current = null
|
||||
setRowboatAnchorTop(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
109
apps/x/apps/renderer/src/components/rowboat-mention-popover.tsx
Normal file
109
apps/x/apps/renderer/src/components/rowboat-mention-popover.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
interface RowboatMentionPopoverProps {
|
||||
open: boolean
|
||||
anchor: { top: number; left: number; width: number } | null
|
||||
initialText?: string
|
||||
onAdd: (instruction: string) => void | Promise<void>
|
||||
onRemove?: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function RowboatMentionPopover({ open, anchor, initialText = '', onAdd, onRemove, onClose }: RowboatMentionPopoverProps) {
|
||||
const [text, setText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setText(initialText)
|
||||
setLoading(false)
|
||||
requestAnimationFrame(() => {
|
||||
textareaRef.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [open, initialText])
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
return () => document.removeEventListener('mousedown', handleMouseDown)
|
||||
}, [open, onClose])
|
||||
|
||||
if (!open || !anchor) return null
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed || loading) return
|
||||
setLoading(true)
|
||||
try {
|
||||
await onAdd(trimmed)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setText('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute z-50"
|
||||
style={{
|
||||
top: anchor.top,
|
||||
left: anchor.left,
|
||||
width: anchor.width,
|
||||
}}
|
||||
>
|
||||
<div className="relative border border-input rounded-md bg-popover shadow-sm">
|
||||
<div className="flex items-start gap-1.5 px-3 pt-2 pb-8">
|
||||
<span className="text-sm text-muted-foreground select-none shrink-0 leading-[1.5]">@rowboat</span>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="flex-1 bg-transparent text-sm placeholder:text-muted-foreground focus:outline-none resize-none leading-[1.5]"
|
||||
placeholder=""
|
||||
rows={2}
|
||||
value={text}
|
||||
disabled={loading}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey || e.shiftKey)) {
|
||||
e.preventDefault()
|
||||
void handleSubmit()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-1.5 right-1.5 flex items-center gap-1.5">
|
||||
{onRemove && (
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded px-2.5 py-1 text-xs font-medium text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={onRemove}
|
||||
disabled={loading}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={!text.trim() || loading}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{loading ? <Loader2 className="size-3 animate-spin" /> : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
98
apps/x/apps/renderer/src/extensions/task-block.tsx
Normal file
98
apps/x/apps/renderer/src/extensions/task-block.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/react'
|
||||
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
|
||||
import { CalendarClock, X } from 'lucide-react'
|
||||
import { inlineTask } from '@x/shared'
|
||||
|
||||
function TaskBlockView({ node, deleteNode }: { node: { attrs: { data: string } }; deleteNode: () => void }) {
|
||||
const raw = node.attrs.data
|
||||
let instruction = ''
|
||||
let scheduleLabel = ''
|
||||
|
||||
try {
|
||||
const parsed = inlineTask.InlineTaskBlockSchema.parse(JSON.parse(raw))
|
||||
instruction = parsed.instruction
|
||||
scheduleLabel = parsed['schedule-label'] ?? ''
|
||||
} catch {
|
||||
// Fallback: show raw data
|
||||
instruction = raw
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="task-block-wrapper" data-type="task-block">
|
||||
<div className="task-block-card">
|
||||
<button
|
||||
className="task-block-delete"
|
||||
onClick={deleteNode}
|
||||
aria-label="Delete task block"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
<div className="task-block-content">
|
||||
<span className="task-block-instruction"><span className="task-block-prefix">@rowboat</span> {instruction}</span>
|
||||
{scheduleLabel && (
|
||||
<span className="task-block-schedule">
|
||||
<CalendarClock size={12} />
|
||||
{scheduleLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const TaskBlockExtension = Node.create({
|
||||
name: 'taskBlock',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
selectable: true,
|
||||
draggable: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
data: {
|
||||
default: '{}',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'pre',
|
||||
priority: 60,
|
||||
getAttrs(element) {
|
||||
const code = element.querySelector('code')
|
||||
if (!code) return false
|
||||
const cls = code.className || ''
|
||||
if (cls.includes('language-task') || cls.includes('language-tell-rowboat')) {
|
||||
return { data: code.textContent || '{}' }
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, unknown> }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'task-block' })]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(TaskBlockView)
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) {
|
||||
state.write('```task\n' + node.attrs.data + '\n```')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
parse: {
|
||||
// handled by parseHTML
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -531,6 +531,83 @@
|
|||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Task block */
|
||||
.tiptap-editor .ProseMirror .task-block-wrapper {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-card {
|
||||
position: relative;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background-color: color-mix(in srgb, var(--muted) 40%, transparent);
|
||||
cursor: default;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-delete {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: color-mix(in srgb, var(--foreground) 40%, transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-card:hover .task-block-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-delete:hover {
|
||||
background-color: color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-card:hover {
|
||||
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-wrapper.ProseMirror-selectednode .task-block-card {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-prefix {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-instruction {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .task-block-schedule {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: color-mix(in srgb, var(--foreground) 55%, transparent);
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .tiptap-editor .ProseMirror {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
|
@ -552,6 +629,10 @@
|
|||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.dark .tiptap-editor .ProseMirror pre code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dark .tiptap-editor .ProseMirror code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ff7b72;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue