"use client"; import { type ReactNode, useEffect, useMemo, useState } from "react"; import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react"; import { Markdown } from "../markdown/markdown"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs"; import { cn } from "../lib/utils"; import { ArtifactPane, type ArtifactPaneProps } from "../primitives/artifact-pane"; import { CollaboratorsList, TiptapEditor, type TiptapEditorProps, } from "./tiptap-editor"; import { EditorProvider, type ConnectionState, type EditorProviderProps, } from "./editor-provider"; import { MarkdownDocumentEditor } from "./markdown-document-editor"; import { htmlToMarkdown, markdownToHtml, normalizeMarkdown, } from "./markdown-conversion"; import { useCollaborators, useEditorConnection } from "./use-editor"; export type DocumentEditorMode = "preview" | "edit"; export type DocumentEditorBackend = "local" | "collaborative"; export interface DocumentEditorPaneCollaborationConfig extends Omit {} export interface DocumentEditorPaneProps extends Omit { tabs?: ArtifactPaneProps["tabs"]; toolbar?: ReactNode; markdown?: string; mode?: DocumentEditorMode; defaultMode?: DocumentEditorMode; onModeChange?: (mode: DocumentEditorMode) => void; backend?: DocumentEditorBackend; placeholder?: string; autoFocus?: boolean; readOnly?: boolean; onChange?: (markdown: string) => void; onSave?: (markdown: string) => Promise | void; saving?: boolean; saveLabel?: string; previewClassName?: string; editorClassName?: string; collaboration?: DocumentEditorPaneCollaborationConfig; } function connectionTone(state: ConnectionState) { switch (state) { case "synced": return "text-[var(--surface-success-text)] border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]"; case "connected": case "connecting": return "text-[var(--surface-info-text)] border-[var(--surface-info-border)] bg-[var(--surface-info-bg)]"; case "disconnected": default: return "text-[var(--surface-warning-text)] border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)]"; } } function connectionLabel(state: ConnectionState) { switch (state) { case "synced": return "Live synced"; case "connected": return "Connected"; case "connecting": return "Connecting"; case "disconnected": default: return "Offline"; } } function connectionDescription( state: ConnectionState, collaborators: number, readOnly?: boolean, ) { if (readOnly) { return state === "disconnected" ? "Live access is paused. You can keep reading while the editor reconnects." : "You are viewing the live document in read-only mode."; } switch (state) { case "synced": return collaborators > 0 ? `You and ${collaborators} collaborator${ collaborators === 1 ? "" : "s" } are editing the same document.` : "You are editing the live document. Changes sync automatically."; case "connected": case "connecting": return "Connecting the live document. Local edits stay in place while sync catches up."; case "disconnected": default: return "Live updates are paused. You can keep editing and reconnect when the transport is healthy again."; } } function CollaborativeDocumentSurface({ markdown, placeholder, autoFocus, readOnly, className, onChange, }: { markdown: string; placeholder?: string; autoFocus?: boolean; readOnly?: boolean; className?: string; onChange?: (markdown: string) => void; }) { const { state } = useEditorConnection(); const { collaborators } = useCollaborators(); const initialContent = useMemo(() => markdownToHtml(markdown), [markdown]); const collaboratorCount = collaborators.length + 1; return (
{state === "disconnected" ? ( ) : ( )} {connectionLabel(state)} {collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`}

{connectionDescription(state, collaborators.length, readOnly)}

{ onChange?.(normalizeMarkdown(htmlToMarkdown(editor.getHTML()))); }} />
); } /** * DocumentEditorPane — reusable markdown document surface with preview/edit * modes and optional collaborative editing backed by Yjs/Hocuspocus. */ export function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown = "", mode, defaultMode = "preview", onModeChange, backend = "local", placeholder = "Start writing...", autoFocus = false, readOnly = false, onChange, onSave, saving = false, saveLabel = "Save changes", previewClassName, editorClassName, collaboration, }: DocumentEditorPaneProps) { const [draft, setDraft] = useState(markdown); const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode); const activeMode = mode ?? uncontrolledMode; const isCollaborative = backend === "collaborative" && Boolean(collaboration); const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(markdown); const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : isDirty ? "Unsaved changes" : "Saved"; useEffect(() => { setDraft(markdown); }, [markdown]); useEffect(() => { if (mode === undefined) { setUncontrolledMode(defaultMode); } }, [defaultMode, mode]); const setMode = (nextMode: DocumentEditorMode) => { if (mode === undefined) { setUncontrolledMode(nextMode); } onModeChange?.(nextMode); }; const handleChange = (nextMarkdown: string) => { setDraft(nextMarkdown); onChange?.(nextMarkdown); }; const editorToolbar = (
Preview {isCollaborative ? "Live edit" : "Edit"}
{toolbar} {isCollaborative ? "Live document" : "Local draft"} {saveStateLabel} {onSave && !readOnly && ( )}
); const preview = (
{draft}
); const localEditor = ( { handleChange(nextMarkdown); }} className={editorClassName} /> ); const collaborativeEditor = collaboration ? ( ) : localEditor; return ( setMode(nextValue as DocumentEditorMode)} className="h-full" > {preview} {isCollaborative ? collaborativeEditor : localEditor} ); }