"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import type { Editor } from "@tiptap/react" import { apiClient } from "@/lib/api/client" import { notifyParagraphStyleCatalog, setActiveParagraphStyleCatalog, } from "@/lib/drive/docs-paragraph-style-bridge" import { buildParagraphStylesCss } from "@/lib/drive/docs-paragraph-styles-css" import type { DocParagraphStyleDefinition, DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles" import { createCustomParagraphStyle, defaultDocumentParagraphStyles, emptyUserParagraphStyles, mergeParagraphStyleCatalogs, } from "@/lib/drive/docs-paragraph-styles" import type { DocParagraphStylesState } from "@/lib/drive/docs-styles-types" const STYLES_DEBOUNCE_MS = 600 function parseCatalog(raw: unknown): DocParagraphStylesCatalog | null { if (!raw || typeof raw !== "object") return null const definitions = (raw as DocParagraphStylesCatalog).definitions if (!definitions || typeof definitions !== "object") return null return { definitions } } export function useDocsParagraphStyles({ editor, initialDocumentStyles, editable, canonicalPath, saveUrl, }: { editor: Editor | null initialDocumentStyles?: DocParagraphStylesCatalog | null editable?: boolean canonicalPath?: string saveUrl?: string }) { const [documentStyles, setDocumentStyles] = useState( () => initialDocumentStyles ?? defaultDocumentParagraphStyles() ) const [userStyles, setUserStyles] = useState(emptyUserParagraphStyles) const documentStylesRef = useRef(documentStyles) const saveTimer = useRef | null>(null) useEffect(() => { documentStylesRef.current = documentStyles }, [documentStyles]) useEffect(() => { if (initialDocumentStyles) setDocumentStyles(initialDocumentStyles) }, [initialDocumentStyles]) useEffect(() => { let cancelled = false void apiClient .get("/richtext/user-paragraph-styles") .then((catalog) => { if (cancelled) return setUserStyles(parseCatalog(catalog) ?? emptyUserParagraphStyles()) }) .catch(() => { if (!cancelled) setUserStyles(emptyUserParagraphStyles()) }) return () => { cancelled = true } }, []) const mergedCatalog = useMemo( () => mergeParagraphStyleCatalogs(documentStyles, userStyles), [documentStyles, userStyles] ) useEffect(() => { setActiveParagraphStyleCatalog(mergedCatalog) notifyParagraphStyleCatalog(mergedCatalog) }, [mergedCatalog]) useEffect(() => { const css = buildParagraphStylesCss(mergedCatalog) const id = "docs-paragraph-styles-dynamic" let el = document.getElementById(id) as HTMLStyleElement | null if (!el) { el = document.createElement("style") el.id = id document.head.appendChild(el) } el.textContent = css }, [mergedCatalog]) const flushDocumentStylesSave = useCallback(async () => { if (!editable || !canonicalPath) return const payload = { path: canonicalPath, paragraphStyles: documentStylesRef.current } if (saveUrl) { const res = await fetch(saveUrl, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) if (!res.ok) throw new Error("paragraph styles save failed") return } await apiClient.put("/richtext/save", payload) }, [canonicalPath, editable, saveUrl]) const scheduleDocumentStylesSave = useCallback(() => { if (!editable) return if (saveTimer.current) clearTimeout(saveTimer.current) saveTimer.current = setTimeout(() => { void flushDocumentStylesSave().catch(() => undefined) }, STYLES_DEBOUNCE_MS) }, [editable, flushDocumentStylesSave]) const saveUserStyles = useCallback(async (catalog: DocParagraphStylesCatalog) => { setUserStyles(catalog) await apiClient.put("/richtext/user-paragraph-styles", catalog) }, []) const updateDocumentStyle = useCallback( (styleId: string, definition: DocParagraphStyleDefinition) => { setDocumentStyles((prev) => ({ definitions: { ...prev.definitions, [styleId]: { ...definition, id: styleId, scope: "document" }, }, })) scheduleDocumentStylesSave() }, [scheduleDocumentStylesSave] ) const updateUserStyle = useCallback( (styleId: string, definition: DocParagraphStyleDefinition) => { setUserStyles((prev) => { const next = { definitions: { ...prev.definitions, [styleId]: { ...definition, id: styleId, scope: "user" as const }, }, } void saveUserStyles(next) return next }) }, [saveUserStyles] ) const createUserStyle = useCallback( (input: { name: string; basedOn?: string }) => { const template = mergedCatalog.definitions[input.basedOn ?? "normal"] const created = createCustomParagraphStyle({ ...input, template }) setUserStyles((prev) => { const next = { definitions: { ...prev.definitions, [created.id]: created }, } void saveUserStyles(next) return next }) return created }, [mergedCatalog.definitions, saveUserStyles] ) useEffect(() => { if (!editor) return const onUpdate = ({ styleId, definition, }: { styleId: string definition: DocParagraphStyleDefinition }) => { if (definition.scope === "user") updateUserStyle(styleId, definition) else updateDocumentStyle(styleId, definition) } editor.on("docsParagraphStyleUpdate", onUpdate) return () => { editor.off("docsParagraphStyleUpdate", onUpdate) } }, [editor, updateDocumentStyle, updateUserStyle]) useEffect( () => () => { if (saveTimer.current) clearTimeout(saveTimer.current) }, [] ) const state: DocParagraphStylesState = useMemo( () => ({ documentStyles, userStyles, mergedCatalog }), [documentStyles, mergedCatalog, userStyles] ) return { state, updateDocumentStyle, updateUserStyle, createUserStyle, applyStyle: (styleId: string) => { editor?.commands.applyDocsParagraphStyle(styleId) }, updateStyleFromSelection: (styleId: string) => { editor?.commands.updateDocsParagraphStyleFromSelection(styleId) }, } }