diff --git a/artifacts/sample-rich.html b/artifacts/sample-rich.html new file mode 100644 index 0000000..357ea11 --- /dev/null +++ b/artifacts/sample-rich.html @@ -0,0 +1 @@ +

Rich Test

This is a rich document.

diff --git a/components/FolderOverview.tsx b/components/FolderOverview.tsx index 720f273..0da51f9 100644 --- a/components/FolderOverview.tsx +++ b/components/FolderOverview.tsx @@ -92,6 +92,7 @@ const DOC_TYPE_LABELS: Record = { source_code: 'Source code', pdf: 'PDFs', image: 'Images', + rich_text: 'Rich documents', }; const formatDocTypeLabel = (docType: DocType) => DOC_TYPE_LABELS[docType] ?? docType.replace(/_/g, ' '); diff --git a/components/PromptEditor.tsx b/components/PromptEditor.tsx index 0748381..484afb8 100644 --- a/components/PromptEditor.tsx +++ b/components/PromptEditor.tsx @@ -10,6 +10,7 @@ import IconButton from './IconButton'; import Button from './Button'; import MonacoEditor, { CodeEditorHandle } from './CodeEditor'; import MonacoDiffEditor from './MonacoDiffEditor'; +import RichTextEditor, { type RichTextEditorHandle } from './RichTextEditor'; import PreviewPane from './PreviewPane'; import LanguageDropdown from './LanguageDropdown'; import PythonExecutionPanel from './PythonExecutionPanel'; @@ -42,6 +43,12 @@ interface DocumentEditorProps { commandTriggers: DocumentCommandTriggers; } +type EditorBridgeHandle = { + format: () => void; + setScrollTop: (scrollTop: number) => void; + getScrollInfo: () => Promise<{ scrollTop: number; scrollHeight: number; clientHeight: number }>; +}; + const useCommandTrigger = (trigger: number, callback: () => void | Promise) => { const previousRef = useRef(null); const callbackRef = useRef(callback); @@ -89,6 +96,8 @@ const PREVIEWABLE_LANGUAGES = new Set([ 'image/svg+xml', ]); +type EditorEngine = 'monaco' | 'plate'; + const resolveDefaultViewMode = (mode: ViewMode | null | undefined, languageHint: string | null | undefined): ViewMode => { if (mode) return mode; const normalizedHint = languageHint?.toLowerCase(); @@ -151,6 +160,11 @@ const DocumentEditor: React.FC = ({ const [splitSize, setSplitSize] = useState(50); const isLocked = Boolean(documentNode.locked); const [isLocking, setIsLocking] = useState(false); + const editorEnginePreferencesRef = useRef>({}); + const defaultEngineForDocument: EditorEngine = documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco'; + const [editorEngine, setEditorEngine] = useState( + editorEnginePreferencesRef.current[documentNode.id] ?? defaultEngineForDocument, + ); const { addLog } = useLogger(); const { skipNextAutoSave } = useDocumentAutoSave({ documentId: documentNode.id, @@ -197,7 +211,7 @@ const DocumentEditor: React.FC = ({ const titleInputRef = useRef(null); const languageButtonRef = useRef(null); const isContentInitialized = useRef(false); - const editorRef = useRef(null); + const editorRef = useRef(null); const previewScrollRef = useRef(null); const isSyncing = useRef(false); const syncTimeout = useRef(null); @@ -253,6 +267,9 @@ const DocumentEditor: React.FC = ({ setIsDirty(false); setIsSaving(false); setIsDiffMode(false); + const storedEngine = editorEnginePreferencesRef.current[documentNode.id]; + const resolvedEngine: EditorEngine = storedEngine ?? (documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco'); + setEditorEngine(resolvedEngine); prevDocumentIdRef.current = documentNode.id; prevDocumentContentRef.current = documentNode.content; return; @@ -301,6 +318,15 @@ const DocumentEditor: React.FC = ({ prevLockedRef.current = isLocked; }, [isLocked, documentNode.content]); + useEffect(() => { + const stored = editorEnginePreferencesRef.current[documentNode.id]; + if (stored) { + setEditorEngine(stored); + return; + } + setEditorEngine(documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco'); + }, [documentNode.doc_type, documentNode.id]); + useEffect(() => { }, [content]); @@ -476,6 +502,17 @@ const DocumentEditor: React.FC = ({ setViewMode(newMode); onViewModeChange(newMode); }, [onViewModeChange]); + + const handleEditorEngineChange = useCallback((engine: EditorEngine) => { + if (editorEngine === engine) { + return; + } + editorEnginePreferencesRef.current[documentNode.id] = engine; + setEditorEngine(engine); + if (engine === 'plate') { + setIsDiffMode(false); + } + }, [documentNode.id, editorEngine]); const handleFormatDocument = () => { if (isLocked) { @@ -659,10 +696,11 @@ const DocumentEditor: React.FC = ({ const language = documentNode.language_hint || 'plaintext'; const normalizedLanguage = language.toLowerCase(); - const supportsAiTools = ['markdown', 'plaintext'].includes(normalizedLanguage); + const isRichTextDocument = documentNode.doc_type === 'rich_text'; + const supportsAiTools = ['markdown', 'plaintext', 'html'].includes(normalizedLanguage); const canAddEmojiToTitle = documentNode.type === 'document'; const supportsPreview = PREVIEWABLE_LANGUAGES.has(normalizedLanguage); - const supportsFormatting = ['javascript', 'typescript', 'json', 'html', 'css', 'xml', 'yaml'].includes(normalizedLanguage); + const supportsFormatting = editorEngine === 'monaco' && ['javascript', 'typescript', 'json', 'html', 'css', 'xml', 'yaml'].includes(normalizedLanguage); const scriptBridgeAvailable = typeof window !== 'undefined' && (!!window.electronAPI || !!window.__DOCFORGE_SCRIPT_PREVIEW__); const isPythonDocument = typeof window !== 'undefined' && !!window.electronAPI && (normalizedLanguage === 'python'); @@ -820,6 +858,7 @@ const DocumentEditor: React.FC = ({ }, [onZoomTargetChange]); const renderContent = () => { + const showRichEditor = isRichTextDocument && editorEngine === 'plate' && !isDiffMode; const editor = isDiffMode ? ( = ({ newText={content} language={language} renderMode="inline" - readOnly={isLocked} + readOnly={isLocked || (isRichTextDocument && editorEngine === 'plate')} onChange={isLocked ? undefined : setContent} onScroll={handleEditorScroll} fontFamily={settings.editorFontFamily} @@ -837,22 +876,35 @@ const DocumentEditor: React.FC = ({ onFocusChange={handleEditorFocusChange} /> ) - : ( - - ); + : showRichEditor + ? ( + } + content={content} + onChange={setContent} + onScroll={handleEditorScroll} + readOnly={isLocked} + onFocusChange={handleEditorFocusChange} + fontFamily={settings.editorFontFamily} + fontSize={scaledEditorFontSize} + /> + ) + : ( + } + content={content} + language={language} + onChange={setContent} + onScroll={handleEditorScroll} + customShortcuts={settings.customShortcuts} + fontFamily={settings.editorFontFamily} + fontSize={scaledEditorFontSize} + activeLineHighlightColorLight={settings.editorActiveLineHighlightColor} + activeLineHighlightColorDark={settings.editorActiveLineHighlightColorDark} + readOnly={isLocked} + onFocusChange={handleEditorFocusChange} + /> + ); const preview = (
= ({ handleViewModeButton('split-horizontal')} tooltip="Split Horizontal" size="xs" className={`rounded-md ${viewMode === 'split-horizontal' ? 'bg-secondary text-primary' : ''}`}>
)} + {isRichTextDocument && ( + <> +
+
+ handleEditorEngineChange('plate')} + tooltip="Visual Editor" + size="xs" + className={`rounded-md px-2 ${editorEngine === 'plate' ? 'bg-secondary text-primary' : ''}`} + > + Visual + + handleEditorEngineChange('monaco')} + tooltip="Source Editor" + size="xs" + className={`rounded-md px-2 ${editorEngine === 'monaco' ? 'bg-secondary text-primary' : ''}`} + > + Source + +
+ + )}
{supportsFormatting && ( diff --git a/components/RichTextEditor.tsx b/components/RichTextEditor.tsx new file mode 100644 index 0000000..7bf332a --- /dev/null +++ b/components/RichTextEditor.tsx @@ -0,0 +1,327 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; +import type { Descendant } from 'slate'; +import { Plate, PlateContent, createPlateEditor } from '@udecode/plate-core/react'; +import { deserializeHtml, serializeHtml } from '@udecode/plate-core'; +import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react'; +import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react'; + +interface ScrollInfo { + scrollTop: number; + scrollHeight: number; + clientHeight: number; +} + +export interface RichTextEditorHandle { + format: () => void; + setScrollTop: (scrollTop: number) => void; + getScrollInfo: () => Promise; +} + +interface RichTextEditorProps { + content: string; + onChange: (html: string) => void; + onScroll?: (info: ScrollInfo) => void; + readOnly?: boolean; + onFocusChange?: (hasFocus: boolean) => void; + fontFamily?: string; + fontSize?: number; +} + +type SlateValue = Descendant[]; + +type MarkKey = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code'; + +type MarkButton = { + key: MarkKey; + label: string; + tooltip: string; +}; + +const EMPTY_VALUE: SlateValue = [ + { + type: 'p', + children: [{ text: '' }], + }, +]; + +const MARK_BUTTONS: MarkButton[] = [ + { key: 'bold', label: 'B', tooltip: 'Bold' }, + { key: 'italic', label: 'I', tooltip: 'Italic' }, + { key: 'underline', label: 'U', tooltip: 'Underline' }, + { key: 'strikethrough', label: 'S', tooltip: 'Strikethrough' }, + { key: 'code', label: '', tooltip: 'Inline Code' }, +]; + +const sanitizeSerializedHtml = (rawHtml: string): string => { + if (typeof window === 'undefined') { + return rawHtml; + } + + const container = window.document.createElement('div'); + container.innerHTML = rawHtml; + const editorRoot = container.querySelector('[data-slate-editor]') as HTMLElement | null; + const target = editorRoot ?? container; + + const scrubElement = (element: Element) => { + const attributes = Array.from(element.attributes); + for (const attr of attributes) { + if (attr.name.startsWith('data-slate')) { + element.removeAttribute(attr.name); + continue; + } + if (attr.name === 'class') { + const filtered = attr.value + .split(/\s+/) + .filter((cls) => cls && !cls.startsWith('slate-') && cls !== 'slate-editor'); + if (filtered.length > 0) { + element.setAttribute('class', filtered.join(' ')); + } else { + element.removeAttribute('class'); + } + continue; + } + if (attr.name === 'style') { + element.removeAttribute('style'); + } + } + Array.from(element.children).forEach((child) => scrubElement(child)); + }; + + scrubElement(target); + + if (editorRoot) { + return editorRoot.innerHTML; + } + return target.innerHTML; +}; + +const convertNodesToValue = (nodes: unknown): SlateValue => { + if (!Array.isArray(nodes)) { + return EMPTY_VALUE; + } + + const slateNodes: Descendant[] = []; + for (const node of nodes) { + if (!node) { + continue; + } + if (typeof node === 'string') { + if (node.trim().length === 0) { + continue; + } + slateNodes.push({ type: 'p', children: [{ text: node }] }); + continue; + } + if (typeof node === 'object') { + const candidate = node as Record; + if ('text' in candidate) { + slateNodes.push({ type: 'p', children: [candidate as unknown as Descendant] }); + continue; + } + if ('type' in candidate && 'children' in candidate) { + slateNodes.push(node as Descendant); + continue; + } + } + } + return slateNodes.length > 0 ? slateNodes : EMPTY_VALUE; +}; + +const RichTextEditor = forwardRef( + ({ content, onChange, onScroll, readOnly = false, onFocusChange, fontFamily, fontSize }, ref) => { + const plugins = useMemo(() => [BasicElementsPlugin, BasicMarksPlugin], []); + const editor = useMemo(() => createPlateEditor({ plugins }), [plugins]); + const [value, setValue] = useState(EMPTY_VALUE); + const [activeMarks, setActiveMarks] = useState>({}); + const containerRef = useRef(null); + const skipNextContentSyncRef = useRef(false); + const latestSerializedRef = useRef(content ?? ''); + + useEffect(() => { + if (skipNextContentSyncRef.current) { + skipNextContentSyncRef.current = false; + return; + } + + let cancelled = false; + + const applyContent = async () => { + if (typeof window === 'undefined') { + setValue(EMPTY_VALUE); + return; + } + + const parser = new window.DOMParser(); + const doc = parser.parseFromString(`${content || ''}`, 'text/html'); + const parsed = await deserializeHtml(editor, { element: doc.body }); + if (!cancelled) { + const nextValue = convertNodesToValue(parsed); + setValue(nextValue); + editor.children = nextValue; + latestSerializedRef.current = content ?? ''; + setActiveMarks(editor.api.marks?.() ?? {}); + } + }; + + void applyContent(); + + return () => { + cancelled = true; + }; + }, [content, editor]); + + useEffect(() => { + editor.children = value; + }, [editor, value]); + + const handleScroll = useCallback( + (event: React.UIEvent) => { + if (!onScroll) return; + const target = event.currentTarget; + onScroll({ + scrollTop: target.scrollTop, + scrollHeight: target.scrollHeight, + clientHeight: target.clientHeight, + }); + }, + [onScroll], + ); + + const pushSerializedUpdate = useCallback(async () => { + const rawHtml = await serializeHtml(editor, { + stripClassNames: true, + stripDataAttributes: true, + preserveClassNames: [], + }); + const sanitized = sanitizeSerializedHtml(rawHtml); + if (sanitized !== latestSerializedRef.current) { + latestSerializedRef.current = sanitized; + onChange(sanitized); + } + }, [editor, onChange]); + + const handleValueChange = useCallback( + (nextValue: SlateValue) => { + setValue(nextValue); + editor.children = nextValue; + setActiveMarks(editor.api.marks?.() ?? {}); + skipNextContentSyncRef.current = true; + void pushSerializedUpdate(); + }, + [editor, pushSerializedUpdate], + ); + + const handleToggleMark = useCallback( + (mark: MarkKey) => { + if (readOnly) return; + const transforms = editor.getTransforms(BasicMarksPlugin); + transforms.focus?.(); + const api = transforms[mark as keyof typeof transforms]; + if (api && typeof api === 'object' && 'toggle' in api && typeof (api as { toggle: () => void }).toggle === 'function') { + (api as { toggle: () => void }).toggle(); + setActiveMarks(editor.api.marks?.() ?? {}); + skipNextContentSyncRef.current = true; + void pushSerializedUpdate(); + } + }, + [editor, pushSerializedUpdate, readOnly], + ); + + useImperativeHandle( + ref, + () => ({ + format: () => { + editor.normalize(); + }, + setScrollTop: (scrollTop: number) => { + if (containerRef.current) { + containerRef.current.scrollTop = scrollTop; + } + }, + getScrollInfo: async () => { + const el = containerRef.current; + return { + scrollTop: el?.scrollTop ?? 0, + scrollHeight: el?.scrollHeight ?? 0, + clientHeight: el?.clientHeight ?? 0, + }; + }, + }), + [editor], + ); + + const fontStyles: React.CSSProperties = useMemo(() => { + const styles: React.CSSProperties = { + fontFamily: fontFamily || 'var(--font-sans, ui-sans-serif)', + }; + if (fontSize && Number.isFinite(fontSize)) { + styles.fontSize = `${fontSize}px`; + } + return styles; + }, [fontFamily, fontSize]); + + return ( +
+
+ {MARK_BUTTONS.map(({ key, label, tooltip }) => { + const isActive = Boolean(activeMarks?.[key]); + return ( + + ); + })} +
+
+ { + setActiveMarks(editor.api.marks?.() ?? {}); + }} + > + onFocusChange?.(true), + onBlur: () => onFocusChange?.(false), + }} + /> + +
+
+ ); + }, +); + +RichTextEditor.displayName = 'RichTextEditor'; + +export default RichTextEditor; diff --git a/electron/database.ts b/electron/database.ts index cd80a48..871ec86 100644 --- a/electron/database.ts +++ b/electron/database.ts @@ -921,7 +921,7 @@ export const databaseService = { } if (nodeType === 'document') { - const allowedDocTypes: DocType[] = ['prompt', 'source_code', 'pdf', 'image']; + const allowedDocTypes: DocType[] = ['prompt', 'source_code', 'pdf', 'image', 'rich_text']; const allowedViewModes: ViewMode[] = ['edit', 'preview', 'split-vertical', 'split-horizontal']; let docType = allowedDocTypes.includes(node.doc_type as DocType) diff --git a/hooks/usePrompts.ts b/hooks/usePrompts.ts index 1391c54..eeba052 100644 --- a/hooks/usePrompts.ts +++ b/hooks/usePrompts.ts @@ -61,9 +61,12 @@ export const useDocuments = () => { const allNodesFlat = useMemo(() => flattenNodes(nodes), [nodes]); const items: DocumentOrFolder[] = useMemo(() => allNodesFlat.map(nodeToDocumentOrFolder), [allNodesFlat]); - const addDocument = useCallback(async ({ parentId, title = 'New Document', content = '', doc_type = 'prompt', language_hint = 'markdown' }: { parentId: string | null, title?: string, content?: string, doc_type?: DocType, language_hint?: string | null }) => { - const resolvedLanguage = mapExtensionToLanguageId(language_hint); - const shouldPreviewByDefault = doc_type === 'pdf' || doc_type === 'image' || resolvedLanguage === 'pdf' || resolvedLanguage === 'image'; + const addDocument = useCallback(async ({ parentId, title = 'New Document', content = '', doc_type = 'prompt', language_hint }: { parentId: string | null, title?: string, content?: string, doc_type?: DocType, language_hint?: string | null }) => { + const resolvedDocType = doc_type ?? 'prompt'; + const defaultLanguageHint = resolvedDocType === 'rich_text' ? 'html' : 'markdown'; + const languageHintToUse = language_hint ?? defaultLanguageHint; + const resolvedLanguage = mapExtensionToLanguageId(languageHintToUse); + const shouldPreviewByDefault = resolvedDocType === 'pdf' || resolvedDocType === 'image' || resolvedLanguage === 'pdf' || resolvedLanguage === 'image'; const defaultViewMode = shouldPreviewByDefault ? 'preview' : undefined; const now = new Date().toISOString(); const newNode = await addNode({ @@ -73,7 +76,7 @@ export const useDocuments = () => { locked: false, document: { content, - doc_type, + doc_type: resolvedDocType, language_hint: resolvedLanguage, language_source: 'user', doc_type_source: 'user', diff --git a/package-lock.json b/package-lock.json index 7f4445d..0938675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@udecode/plate": "^49.0.0", + "@udecode/plate-basic-elements": "^49.0.0", + "@udecode/plate-basic-marks": "^49.0.0", "@uiw/react-color-compact": "^2.9.2", "better-sqlite3": "^11.1.2", "electron-log": "^5.1.5", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.2.1", "emoji-picker-react": "^4.15.0", + "slate": "^0.118.1", + "slate-history": "^0.113.1", + "slate-react": "^0.119.0", "uuid": "^10.0.0" }, "devDependencies": { @@ -1857,6 +1863,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -1984,6 +1996,39 @@ "node": ">=14" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", @@ -2708,14 +2753,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2775,6 +2820,212 @@ "@types/node": "*" } }, + "node_modules/@udecode/plate": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate/-/plate-49.0.0.tgz", + "integrity": "sha512-jhvQ47+lxiEzUEmXMWBK5YpAT1rIFxf78zoiH98r2CQ9NsZrBqce4onCQ684o5EhU0/7MpHBOqRkBuPnZWtFZA==", + "deprecated": "Package no longer supported, use platejs instead.", + "license": "MIT", + "dependencies": { + "@udecode/plate-core": "49.0.0", + "@udecode/plate-utils": "49.0.0", + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "47.3.1", + "@udecode/slate": "49.0.0", + "@udecode/utils": "47.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/plate-basic-elements": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-elements/-/plate-basic-elements-49.0.0.tgz", + "integrity": "sha512-6Bv7eovWrGfM5PFEHQbSAHsYxkskjC5vkxn7vQuZlBUl8O8s5Z3OTGNQrEr0U6JJDinn+ylMGPG8rs3v7CHIVQ==", + "deprecated": "Package no longer supported, use @platejs/basic-nodes instead.", + "license": "MIT", + "peerDependencies": { + "@udecode/plate": ">=49.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/plate-basic-marks": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-49.0.0.tgz", + "integrity": "sha512-qSVr6VKwafDiTvOjRicJh9bdM/WNgCzvEPV2kz1jIAhG2n++fG5LoWJ7qdsFLxtWleUAt8yQKduoGn+0dydk4g==", + "deprecated": "Package no longer supported, use @platejs/basic-nodes instead.", + "license": "MIT", + "peerDependencies": { + "@udecode/plate": ">=49.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/plate-core": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-49.0.0.tgz", + "integrity": "sha512-KaLv2coeHWy/Q3L9ldMXqfVK/d17EnyiMNH39M2iW3NSG6PZ1Kr9i43/z2u59pwFIUlJd4yt/g9idz8jgaWXfw==", + "deprecated": "Package no longer supported, use @platejs/core instead.", + "license": "MIT", + "dependencies": { + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "47.3.1", + "@udecode/slate": "49.0.0", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1", + "html-entities": "^2.6.0", + "is-hotkey": "^0.2.0", + "jotai": "~2.8.4", + "jotai-optics": "0.4.0", + "jotai-x": "2.3.2", + "lodash": "^4.17.21", + "nanoid": "^5.1.5", + "optics-ts": "2.4.1", + "slate-hyperscript": "0.100.0", + "slate-react": "0.114.2", + "use-deep-compare": "^1.3.0", + "zustand": "^5.0.3", + "zustand-x": "6.1.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/plate-core/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@udecode/plate-core/node_modules/slate-react": { + "version": "0.114.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.114.2.tgz", + "integrity": "sha512-yqJnX1/7A30szl9BxW3qX99MZy6mM6VtUi1rXTy0JpRMTKv3rduo0WOxqcX90tpt0ke2pzHGbrLLr1buIN4vrw==", + "license": "MIT", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.114.0", + "slate-dom": ">=0.110.2" + } + }, + "node_modules/@udecode/plate-utils": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-49.0.0.tgz", + "integrity": "sha512-WkU+PQIhJ4vrrbPMb2SqfjURV3HxmHky7O4TvaKAVhGdo9L0Rql9TKfO6OKt+kF7dD7VaYf0dZcdWLgM6dDONA==", + "deprecated": "Package no longer supported, use @platejs/utils instead.", + "license": "MIT", + "dependencies": { + "@udecode/plate-core": "49.0.0", + "@udecode/react-utils": "47.3.1", + "@udecode/slate": "49.0.0", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/react-hotkeys": { + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-hotkeys/-/react-hotkeys-37.0.0.tgz", + "integrity": "sha512-3ZV5LiaTnKyhXwN6U0NE2cofNsNN2IPMkNCDntbSIIRLYmI+o6LRkDwAucSNh/BIdNXfvxscsR04RYyIwjGbJw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@udecode/react-utils": { + "version": "47.3.1", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-47.3.1.tgz", + "integrity": "sha512-fHnY0RGOeKKPnFW8xx8VWlLI0yscHd/kIU5t0bZ5bJ7Vanlhk1CUnPGDNa5HCIMVQrLARngGut/lIyGtoF65EQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "^1.2.0", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@udecode/slate": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-49.0.0.tgz", + "integrity": "sha512-WttifcApSM+KNNivTC64XtgsRL4+OJ03qEaQmuW04Uoh8tKK/z5DePCRwWL19d+jkrPiPtzSB2/edtNJUhtgRQ==", + "deprecated": "Package no longer supported, use @platejs/slate instead.", + "license": "MIT", + "dependencies": { + "@udecode/utils": "47.2.7", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "slate": "0.114.0", + "slate-dom": "0.114.0" + } + }, + "node_modules/@udecode/slate/node_modules/slate": { + "version": "0.114.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.114.0.tgz", + "integrity": "sha512-r3KHl22433DlN5BpLAlL4b3D8ItoGKAkj91YT6GhP39XuLoBT+YFd9ObKuL/okgiPb5lbwnW+71fM45hHceN9w==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/@udecode/slate/node_modules/slate-dom": { + "version": "0.114.0", + "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.114.0.tgz", + "integrity": "sha512-3LWIfiDPNQSY+SCPsvMTErCkx2gXTViLoWISisw6uM+unwiOkEF9ZmpHp88/SSmcq6k3P4aIquehUNeNUlkdiA==", + "license": "MIT", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "slate": ">=0.99.0" + } + }, + "node_modules/@udecode/utils": { + "version": "47.2.7", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-47.2.7.tgz", + "integrity": "sha512-tQ8tIcdW+ZqWWrDgyf/moTLWtcErcHxaOfuCD/6qIL5hCq+jZm67nGHQToOT4Czti5Jr7CDPMgr8lYpdTEZcew==", + "license": "MIT" + }, "node_modules/@uiw/color-convert": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.2.tgz", @@ -4048,6 +4299,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -4154,6 +4414,12 @@ "node": ">= 10" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4361,7 +4627,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -5062,7 +5328,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5151,6 +5416,19 @@ "node": "*" } }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -6842,6 +7120,22 @@ "node": ">=12" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -7049,6 +7343,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -7230,6 +7534,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7253,6 +7563,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -7332,6 +7651,56 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jotai": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.8.4.tgz", + "integrity": "sha512-f6jwjhBJcDtpeauT2xH01gnqadKEySwwt1qNBLvAXcnojkmb76EdqRt05Ym8IamfHGAQz2qMKAwftnyjeSoHAA==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/jotai-optics": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.4.0.tgz", + "integrity": "sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==", + "license": "MIT", + "peerDependencies": { + "jotai": ">=2.0.0", + "optics-ts": ">=2.0.0" + } + }, + "node_modules/jotai-x": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-2.3.2.tgz", + "integrity": "sha512-WjOfSO4lZBtuy7igTcJ8ZMq9zf/MfjnJnMgsdLNLZrFeiaAWif7Kh/kXwIG4dFMLTOFZ9Bf96bztv21VBcTB0g==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=17.0.0", + "jotai": ">=2.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7657,7 +8026,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash-es": { @@ -7712,6 +8080,12 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -15013,6 +15387,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mutative": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mutative/-/mutative-1.1.0.tgz", + "integrity": "sha512-2PJADREjOusk3iJkD3rXV2YjAxTuaLxdfqtqTEt6vcY07LtEBR1seHuBHXWEIuscqRDGvbauYPs+A4Rj/KTczQ==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15204,6 +15587,12 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optics-ts": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/optics-ts/-/optics-ts-2.4.1.tgz", + "integrity": "sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==", + "license": "MIT" + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -15677,6 +16066,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-compare": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", + "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -16924,6 +17319,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", @@ -17236,6 +17640,79 @@ "node": ">=10" } }, + "node_modules/slate": { + "version": "0.118.1", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.118.1.tgz", + "integrity": "sha512-6H1DNgnSwAFhq/pIgf+tLvjNzH912M5XrKKhP9Frmbds2zFXdSJ6L/uFNyVKxQIkPzGWPD0m+wdDfmEuGFH5Tg==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-dom": { + "version": "0.119.0", + "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.119.0.tgz", + "integrity": "sha512-foc8a2NkE+1SldDIYaoqjhVKupt8RSuvHI868rfYOcypD4we5TT7qunjRKJ852EIRh/Ql8sSTepXgXKOUJnt1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "slate": ">=0.99.0" + } + }, + "node_modules/slate-history": { + "version": "0.113.1", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz", + "integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-hyperscript": { + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", + "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-react": { + "version": "0.119.0", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.119.0.tgz", + "integrity": "sha512-snHqhQ1NkZXyuqG4JTxywRg1accho/hnioM2JIYqziaQQcgfqLi2Pe1AHL82WIC1pLWdzPjy2O7drnSbO0DBsQ==", + "license": "MIT", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.114.0", + "slate-dom": ">=0.116.0" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -17810,12 +18287,24 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "license": "MIT" + }, "node_modules/tiny-typed-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -18333,6 +18822,27 @@ "dev": true, "license": "MIT" }, + "node_modules/use-deep-compare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.3.0.tgz", + "integrity": "sha512-94iG+dEdEP/Sl3WWde+w9StIunlV8Dgj+vkt5wTwMoFQLaijiEZSXXy8KtcStpmEDtIptRJiNeD4ACTtVvnIKA==", + "license": "MIT", + "dependencies": { + "dequal": "2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -19451,6 +19961,95 @@ "node": ">= 10" } }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zustand-x": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-6.1.0.tgz", + "integrity": "sha512-lW1Fs29bLCrerWDa3lZLPuEn+ZkbSGzXdwdImKLJUtI2OqlDjpcFac5WTzCPs2ul/igwXFnGiKH1mdn+1Pl2mw==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "lodash.mapvalues": "^4.6.0", + "mutative": "1.1.0", + "react-tracked": "^1.7.11", + "use-sync-external-store": "1.4.0" + }, + "peerDependencies": { + "zustand": ">=5.0.2" + } + }, + "node_modules/zustand-x/node_modules/react-tracked": { + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.14.tgz", + "integrity": "sha512-6UMlgQeRAGA+uyYzuQGm7kZB6ZQYFhc7sntgP7Oxwwd6M0Ud/POyb4K3QWT1eXvoifSa80nrAWnXWFGpOvbwkw==", + "license": "MIT", + "dependencies": { + "proxy-compare": "2.6.0", + "use-context-selector": "1.4.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/zustand-x/node_modules/use-context-selector": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.4.tgz", + "integrity": "sha512-pS790zwGxxe59GoBha3QYOwk8AFGp4DN6DOtH+eoqVmgBBRXVx4IlPDhJmmMiNQAgUaLlP+58aqRC3A4rdaSjg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 616a38f..5410721 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,17 @@ }, "dependencies": { "@uiw/react-color-compact": "^2.9.2", + "@udecode/plate": "^49.0.0", + "@udecode/plate-basic-elements": "^49.0.0", + "@udecode/plate-basic-marks": "^49.0.0", "better-sqlite3": "^11.1.2", "electron-log": "^5.1.5", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.2.1", "emoji-picker-react": "^4.15.0", + "slate": "^0.118.1", + "slate-history": "^0.113.1", + "slate-react": "^0.119.0", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/services/classificationService.ts b/services/classificationService.ts index 9b5c081..b470a5a 100644 --- a/services/classificationService.ts +++ b/services/classificationService.ts @@ -211,6 +211,9 @@ export const classifyDocumentContent = (options: ClassificationOptions): Classif if (langFromExtension === 'plantuml') { return classifyWith('plantuml', 'source_code', 'split-vertical', 0.8, 'Extension indicates PlantUML'); } + if (langFromExtension === 'html') { + return classifyWith('html', 'rich_text', null, 0.75, 'Extension indicates HTML'); + } if (langFromExtension !== 'plaintext') { return classifyWith(langFromExtension, 'source_code', null, 0.7, `Extension indicates ${langFromExtension}`); } @@ -233,7 +236,7 @@ export const classifyDocumentContent = (options: ClassificationOptions): Classif } if (looksLikeHtml(trimmed)) { - return classifyWith('html', 'source_code', null, 0.75, 'HTML tag heuristics matched'); + return classifyWith('html', 'rich_text', null, 0.75, 'HTML tag heuristics matched'); } if (looksLikeYaml(trimmed)) { diff --git a/services/documentExportService.ts b/services/documentExportService.ts index 117af02..d14ff65 100644 --- a/services/documentExportService.ts +++ b/services/documentExportService.ts @@ -21,6 +21,7 @@ const DEFAULT_DOC_TYPE_EXTENSION: Record = { source_code: 'txt', pdf: 'pdf', image: 'png', + rich_text: 'html', }; const DOC_TYPE_FILTER_LABELS: Partial> = { @@ -28,6 +29,7 @@ const DOC_TYPE_FILTER_LABELS: Partial> = { source_code: 'Code Files', pdf: 'PDF Documents', image: 'Image Files', + rich_text: 'Rich Documents', }; const LANGUAGE_EXTENSION_MAP: Record = { @@ -223,6 +225,7 @@ const inferMimeFromExtension = (extension: string | null, docType?: DocType): st if (docType) { if (docType === 'pdf') return 'application/pdf'; if (docType === 'image') return 'image/png'; + if (docType === 'rich_text') return 'text/html'; } return TEXT_MIME_DEFAULT; }; diff --git a/services/repository.ts b/services/repository.ts index ba6dc91..378f063 100644 --- a/services/repository.ts +++ b/services/repository.ts @@ -67,12 +67,15 @@ const createSampleBrowserState = (): BrowserState => { const documentId = 1; const shellDocumentId = 2; const powershellDocumentId = 3; + const richDocumentId = 4; const versionId = 1; const shellVersionId = 2; const powershellVersionId = 3; + const richVersionId = 4; const sampleContent = '# Welcome to DocForge\n\nThis is a static dataset provided for browser preview mode.'; const shellContent = '#!/bin/bash\n\necho "DocForge shell quickstart"\nls -la'; const powershellContent = 'Write-Host "DocForge PowerShell quickstart"\nGet-ChildItem'; + const richContent = '

Rich Text Demo

This document opens with the new visual editor. Use the toolbar to apply bold, italic, and other formatting.

'; const document: Document = { document_id: documentId, @@ -149,6 +152,31 @@ const createSampleBrowserState = (): BrowserState => { document: powershellDocument, }; + const richDocument: Document = { + document_id: richDocumentId, + node_id: 'sample-rich', + doc_type: 'rich_text', + language_hint: 'html', + default_view_mode: 'edit', + language_source: 'user', + doc_type_source: 'user', + classification_updated_at: now, + current_version_id: richVersionId, + content: richContent, + }; + + const richNode: Node = { + node_id: 'sample-rich', + parent_id: rootId, + node_type: 'document', + title: 'Rich Document Demo', + sort_order: 3, + created_at: now, + updated_at: now, + locked: false, + document: richDocument, + }; + const rootNode: Node = { node_id: rootId, parent_id: null, @@ -158,7 +186,7 @@ const createSampleBrowserState = (): BrowserState => { created_at: now, updated_at: now, locked: false, - children: [documentNode, shellNode, powershellNode], + children: [documentNode, shellNode, powershellNode, richNode], }; return { @@ -198,9 +226,18 @@ const createSampleBrowserState = (): BrowserState => { content: powershellContent, }, ], + [richDocumentId]: [ + { + version_id: richVersionId, + document_id: richDocumentId, + created_at: now, + content_id: richVersionId, + content: richContent, + }, + ], }, - nextDocumentId: powershellDocumentId + 1, - nextVersionId: powershellVersionId + 1, + nextDocumentId: richDocumentId + 1, + nextVersionId: richVersionId + 1, }; }; @@ -1317,7 +1354,7 @@ export const repository = { const { collection, parentId, insertIndex } = resolveTarget(); - const allowedDocTypes: DocType[] = ['prompt', 'source_code', 'pdf', 'image']; + const allowedDocTypes: DocType[] = ['prompt', 'source_code', 'pdf', 'image', 'rich_text']; const allowedViewModes: ViewMode[] = ['edit', 'preview', 'split-vertical', 'split-horizontal']; const createdIds: string[] = []; diff --git a/types.ts b/types.ts index 7d787c0..ab734c1 100644 --- a/types.ts +++ b/types.ts @@ -144,7 +144,7 @@ export interface UpdateErrorPayload { } export type NodeType = 'folder' | 'document'; -export type DocType = 'prompt' | 'source_code' | 'pdf' | 'image'; +export type DocType = 'prompt' | 'source_code' | 'pdf' | 'image' | 'rich_text'; export type ClassificationSource = 'auto' | 'user' | 'imported' | 'unknown'; export type SaveFilePayload =