import { useEffect, useMemo, useState } from "react"; import { X } from "lucide-react"; import type { DocumentRefreshOptions } from "../../../document-model"; import type { InlineDocumentSourceTarget } from "../hooks/useInlineDocumentEditor"; import { localMutationHeaders } from "../../localMutationRequest"; import { useEditStatus } from "../../WorkbenchEditStatusContext"; import { matchesHotkey } from "../../../hotkeys"; import { Button } from "@/openpress/ui/button"; import { Textarea } from "@/openpress/ui/textarea"; const EDITOR_LAYER_CLASS = "pointer-events-none fixed inset-0 z-[980]"; const EDITOR_PANEL_CLASS = [ "pointer-events-auto fixed grid max-w-[calc(100vw-28px)] gap-[10px]", "rounded-[var(--op-workspace-radius-md)] border border-[var(--op-workspace-border)]", "bg-[var(--op-workspace-surface)] p-3 text-[var(--op-workspace-text)]", "shadow-[var(--op-workspace-shadow-popover)]", ].join(" "); const EDITOR_ROW_CLASS = "flex min-w-0 items-center justify-between gap-[10px]"; const EDITOR_CLOSE_BUTTON_CLASS = [ "op-ui-icon-button h-6 w-6 !rounded-[var(--op-workspace-radius-sm)] border-0 bg-transparent p-0", "text-[var(--op-workspace-text-muted)] hover:bg-[var(--op-workspace-surface-hover)] hover:text-[var(--op-workspace-text)]", "disabled:cursor-progress", ].join(" "); const EDITOR_CANCEL_BUTTON_CLASS = [ "op-ui-button !h-7 !rounded-[var(--op-workspace-radius-sm)] border border-transparent bg-transparent px-2", "text-[11px] font-medium text-[var(--op-workspace-text-muted)]", "hover:bg-[var(--op-workspace-surface-hover)] hover:text-[var(--op-workspace-text)] disabled:cursor-progress", ].join(" "); const EDITOR_STATUS_CLASS = { idle: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-[var(--op-workspace-text-soft)]", loading: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-[var(--op-workspace-text-soft)]", saving: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-[var(--op-workspace-accent)]", failed: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-[var(--op-workspace-danger)]", } satisfies Record<"idle" | "loading" | "saving" | "failed", string>; export function InlineSourceEditorLayer({ target, fetchImpl, onClose, onDocumentEdited, geometryVersion, }: { target: InlineDocumentSourceTarget | null; fetchImpl?: typeof fetch; onClose: () => void; onDocumentEdited?: (options?: DocumentRefreshOptions) => void | Promise; geometryVersion?: unknown; }) { const { startSave, completeSave, failSave } = useEditStatus(); const [text, setText] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "saving" | "failed">("idle"); const [error, setError] = useState(""); const sourceRequestUrl = useMemo(() => (target ? sourceReadUrl(target) : ""), [target]); const targetRect = useMemo( () => (target ? resolveSourceEditorTargetRect(target) : null), // eslint-disable-next-line react-hooks/exhaustive-deps [geometryVersion, target], ); useEffect(() => { if (!target) { setText(""); setStatus("idle"); setError(""); return undefined; } const request = fetchImpl ?? globalThis.fetch?.bind(globalThis); if (!request) { setStatus("failed"); setError("Source edit endpoint is unavailable."); return undefined; } let canceled = false; setStatus("loading"); setError(""); void request(sourceRequestUrl, { method: "GET" }) .then(async (response) => { if (!response.ok) { const message = await response.text().catch(() => ""); throw new Error(message || `Source read failed with status ${response.status}`); } return response.json() as Promise<{ source?: { text?: string } }>; }) .then((result) => { if (canceled) return; setText(result.source?.text ?? ""); setStatus("idle"); }) .catch((readError) => { if (canceled) return; const message = readError instanceof Error ? readError.message : String(readError); setStatus("failed"); setError(message); failSave(message); }); return () => { canceled = true; }; }, [failSave, fetchImpl, sourceRequestUrl, target]); if (!target) return null; const block = target.block; const position = sourceEditorPosition(targetRect ?? target.rect); const canSave = status !== "loading" && status !== "saving" && text.trim().length > 0; const handleSave = async () => { const request = fetchImpl ?? globalThis.fetch?.bind(globalThis); if (!request) { setStatus("failed"); setError("Source edit endpoint is unavailable."); return; } setStatus("saving"); setError(""); startSave(); try { const response = await request("/__openpress/source-edit", { method: "POST", headers: localMutationHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ blockId: block.id, path: block.path, kind: block.kind, name: block.name, source: block.source, sourceMode: true, text, }), }); if (!response.ok) { const message = await response.text().catch(() => ""); throw new Error(message || `Source edit failed with status ${response.status}`); } const result = await response.json().catch(() => undefined) as { document?: { renderId?: string } } | undefined; await onDocumentEdited?.({ expectedRenderId: result?.document?.renderId }); setStatus("idle"); completeSave(); onClose(); } catch (saveError) { const message = saveError instanceof Error ? saveError.message : String(saveError); setStatus("failed"); setError(message); failSave(message); } }; const statusText = sourceEditorStatusText(status, error); return (
event.stopPropagation()}>
SOURCE {block.name ?? block.kind ?? "Block"}