import { lazy, Suspense, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { MODES } from '../lib/editorConstants.js'; import { parseOrgDocument } from '../lib/org/parseDocument.js'; import { buildPreviewHtml, ensureMarkedLoaded, ensureOrgLoaded } from '../lib/previewHtml.js'; import OrgOutline from './OrgOutline.jsx'; const OrgEditor = lazy(() => import('./OrgEditor.jsx')); function useDividerOrientation() { const [orientation, setOrientation] = useState('vertical'); useEffect(() => { const media = window.matchMedia('(max-width: 768px)'); const update = () => setOrientation(media.matches ? 'horizontal' : 'vertical'); update(); media.addEventListener('change', update); return () => media.removeEventListener('change', update); }, []); return orientation; } function useWideLayout() { const [wide, setWide] = useState(() => typeof window !== 'undefined' ? window.matchMedia('(min-width: 900px)').matches : true, ); useEffect(() => { const media = window.matchMedia('(min-width: 900px)'); const update = () => setWide(media.matches); update(); media.addEventListener('change', update); return () => media.removeEventListener('change', update); }, []); return wide; } export function countWords(text) { const trimmed = text.trim(); if (!trimmed) return 0; return trimmed.split(/\s+/).filter(Boolean).length; } export default function EditorLayout({ mode, content, onContentChange, readOnly = false, showEditor = true, editorRef, previewOnly = false, }) { const [markedReady, setMarkedReady] = useState(false); const [orgReady, setOrgReady] = useState(false); const scrollToLineRef = useRef(null); const dividerOrientation = useDividerOrientation(); const wideLayout = useWideLayout(); const deferredContent = useDeferredValue(content); const previewIsStale = content !== deferredContent; useEffect(() => { if (mode !== MODES.MARKDOWN) return undefined; let cancelled = false; ensureMarkedLoaded().then(() => { if (!cancelled) setMarkedReady(true); }); return () => { cancelled = true; }; }, [mode]); useEffect(() => { if (mode !== MODES.ORG) return undefined; let cancelled = false; ensureOrgLoaded().then(() => { if (!cancelled) setOrgReady(true); }); return () => { cancelled = true; }; }, [mode]); const orgMeta = useMemo(() => { if (mode !== MODES.ORG) return null; return parseOrgDocument(deferredContent); }, [mode, deferredContent]); const html = useMemo(() => { return buildPreviewHtml(mode, deferredContent); }, [mode, deferredContent, markedReady, orgReady]); const handleRegisterScroll = useCallback((scrollFn) => { scrollToLineRef.current = scrollFn; }, []); const handleOutlineSelect = useCallback((line) => { scrollToLineRef.current?.(line); }, []); const wordCount = useMemo(() => countWords(content), [content]); const charCount = content.length; const editorAriaLabel = mode === MODES.ORG ? 'Org-mode editing area' : 'Markdown editing area'; const showOutline = mode === MODES.ORG && showEditor && !previewOnly && wideLayout && orgMeta?.headings?.length > 0; return ( <>
{showOutline && ( )} {showEditor && !previewOnly && ( <>
Write
{mode === MODES.ORG ? ( Loading Org editor…}> ) : (