diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index b80ea0c..9be037f 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,23 +1,30 @@ "use client"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; -import { Pencil, Eye, Maximize2, Columns2 } from "lucide-react"; +import { Pencil, Eye, PanelLeft, Columns2, PanelRight } from "lucide-react"; import { useEditorStore } from "@/lib/store"; -import { SHORTCUTS, applyEdit } from "@/lib/md-edit"; +import { SHORTCUTS, applyEdit, insertAtCursor } from "@/lib/md-edit"; import { Toolbar } from "./Toolbar"; import { Preview } from "./Preview"; const noopSubscribe = () => () => {}; type View = "write" | "preview"; +const MIN = 0.15; +const MAX = 0.85; export function Editor() { const markdown = useEditorStore((s) => s.markdown); const setMarkdown = useEditorStore((s) => s.setMarkdown); const pageSize = useEditorStore((s) => s.options.pageSize); + const ratio = useEditorStore((s) => s.splitRatio); + const setSplitRatio = useEditorStore((s) => s.setSplitRatio); const textareaRef = useRef(null); const [view, setView] = useState("write"); // mobile/tablet only - const [focus, setFocus] = useState(false); // desktop: hide preview + const containerRef = useRef(null); + const [dragging, setDragging] = useState(false); + const rectRef = useRef<{ left: number; width: number } | null>(null); + const rafRef = useRef(null); const mounted = useSyncExternalStore(noopSubscribe, () => true, () => false); @@ -27,6 +34,56 @@ export function Editor() { return () => clearTimeout(id); }, [markdown]); + // Divider drag (desktop). Width is cached at drag start; updates are + // rAF-throttled. The preview only reflows — it never re-parses on resize. + useEffect(() => { + if (!dragging) return; + function onMove(e: MouseEvent) { + const rect = rectRef.current; + if (!rect || rafRef.current) return; + rafRef.current = requestAnimationFrame(() => { + rafRef.current = null; + const x = (e.clientX - rect.left) / rect.width; + setSplitRatio(Math.min(MAX, Math.max(MIN, x))); + }); + } + function onUp() { + setDragging(false); + } + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + const prevSelect = document.body.style.userSelect; + const prevCursor = document.body.style.cursor; + document.body.style.userSelect = "none"; + document.body.style.cursor = "col-resize"; + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + document.body.style.userSelect = prevSelect; + document.body.style.cursor = prevCursor; + }; + }, [dragging, setSplitRatio]); + + function startDrag(e: React.MouseEvent) { + const el = containerRef.current; + if (!el) return; + const r = el.getBoundingClientRect(); + rectRef.current = { left: r.left, width: r.width }; + setDragging(true); + e.preventDefault(); + } + + function onDividerKey(e: React.KeyboardEvent) { + if (e.key === "ArrowLeft") setSplitRatio(Math.max(MIN, ratio - 0.02)); + else if (e.key === "ArrowRight") setSplitRatio(Math.min(MAX, ratio + 0.02)); + else if (e.key === "Home") setSplitRatio(MIN); + else if (e.key === "End") setSplitRatio(MAX); + else return; + e.preventDefault(); + } + function handleKeyDown(e: React.KeyboardEvent) { const el = e.currentTarget; const mod = e.metaKey || e.ctrlKey; @@ -34,16 +91,9 @@ export function Editor() { if (transform) { e.preventDefault(); applyEdit(el, markdown, setMarkdown, transform); - return; - } - if (e.key === "Tab") { + } else if (e.key === "Tab") { e.preventDefault(); - const start = el.selectionStart; - const end = el.selectionEnd; - setMarkdown(markdown.slice(0, start) + " " + markdown.slice(end)); - requestAnimationFrame(() => { - el.selectionStart = el.selectionEnd = start + 2; - }); + insertAtCursor(el, " ", setMarkdown); } } @@ -57,6 +107,10 @@ export function Editor() { const words = markdown.trim() ? markdown.trim().split(/\s+/).length : 0; const minutes = Math.max(1, Math.round(words / 200)); + const editorOnly = ratio >= 0.98; + const previewOnly = ratio <= 0.02; + const isSplit = !editorOnly && !previewOnly; + const gridTemplate = `minmax(0, ${ratio}fr) ${isSplit ? 7 : 0}px minmax(0, ${1 - ratio}fr)`; return (
@@ -66,12 +120,16 @@ export function Editor() { setView("preview")} icon={} label="Preview" />
-
+
{/* Shell — where you type */}