ubiquitous-invention/apps/web/components/whiteboard/canvas.tsx

218 lines
5.5 KiB
TypeScript
Raw Normal View History

"use client";
import * as React from "react";
import dynamic from "next/dynamic";
import type { Editor, TLStore } from "tldraw";
import { useCollaboration } from "@/lib/yjs-provider";
import { cn } from "@/lib/utils";
import { WhiteboardToolbar } from "./toolbar";
import { syncTldrawStoreWithYjs } from "./yjs-store";
import { customShapeUtils } from "./shapes";
import "tldraw/tldraw.css";
export type WhiteboardCanvasProps = {
documentId?: string;
className?: string;
readOnly?: boolean;
};
function useHtmlDarkClass(): boolean {
const [dark, setDark] = React.useState(false);
React.useEffect(() => {
const el = document.documentElement;
const read = () => setDark(el.classList.contains("dark"));
read();
const observer = new MutationObserver(read);
observer.observe(el, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
return dark;
}
type TldrawModule = typeof import("tldraw");
function TldrawCollabBody({
documentId,
readOnly,
className,
tldraw,
}: {
documentId: string;
readOnly: boolean;
className?: string;
tldraw: TldrawModule;
}) {
const { doc, isConnected, isSynced } = useCollaboration(`object:${documentId}`);
const dark = useHtmlDarkClass();
const store = React.useMemo<TLStore>(
() => tldraw.createTLStore({ shapeUtils: customShapeUtils }),
[documentId, tldraw],
);
React.useEffect(() => {
if (!doc || !isSynced) return;
return syncTldrawStoreWithYjs(store, doc);
}, [doc, isSynced, store]);
const editorRef = React.useRef<Editor | null>(null);
React.useEffect(() => {
editorRef.current?.updateInstanceState({ isReadonly: readOnly });
}, [readOnly]);
React.useEffect(() => {
editorRef.current?.user.updateUserPreferences({
colorScheme: dark ? "dark" : "light",
});
}, [dark]);
const { Tldraw } = tldraw;
const collab = React.useMemo(
() => ({ isConnected, isSynced }),
[isConnected, isSynced],
);
return (
<Tldraw
store={store}
shapeUtils={customShapeUtils}
className={cn("h-full min-h-0 w-full min-w-0", className)}
inferDarkMode={false}
onMount={(editor) => {
editorRef.current = editor;
editor.updateInstanceState({ isReadonly: readOnly });
editor.user.updateUserPreferences({
colorScheme: dark ? "dark" : "light",
});
}}
components={{
InFrontOfTheCanvas: () => (
<WhiteboardToolbar collab={collab} readOnly={readOnly} />
),
}}
/>
);
}
function TldrawLocalBody({
readOnly,
className,
tldraw,
}: {
readOnly: boolean;
className?: string;
tldraw: TldrawModule;
}) {
const dark = useHtmlDarkClass();
const editorRef = React.useRef<Editor | null>(null);
const store = React.useMemo<TLStore>(
() => tldraw.createTLStore({ shapeUtils: customShapeUtils }),
[tldraw],
);
React.useEffect(() => {
editorRef.current?.updateInstanceState({ isReadonly: readOnly });
}, [readOnly]);
React.useEffect(() => {
editorRef.current?.user.updateUserPreferences({
colorScheme: dark ? "dark" : "light",
});
}, [dark]);
const { Tldraw } = tldraw;
return (
<Tldraw
store={store}
shapeUtils={customShapeUtils}
className={cn("h-full min-h-0 w-full min-w-0", className)}
inferDarkMode={false}
onMount={(editor) => {
editorRef.current = editor;
editor.updateInstanceState({ isReadonly: readOnly });
editor.user.updateUserPreferences({
colorScheme: dark ? "dark" : "light",
});
}}
components={{
InFrontOfTheCanvas: () => <WhiteboardToolbar readOnly={readOnly} />,
}}
/>
);
}
const TldynamicCollab = dynamic(
() =>
import("tldraw").then((tldraw) => {
function CollabLoaded(
props: Omit<WhiteboardCanvasProps, "documentId" | "readOnly"> & {
documentId: string;
readOnly: boolean;
},
) {
return <TldrawCollabBody {...props} tldraw={tldraw} />;
}
return CollabLoaded;
}),
{ ssr: false, loading: () => <WhiteboardLoading /> },
);
const TldynamicLocal = dynamic(
() =>
import("tldraw").then((tldraw) => {
function LocalLoaded(props: { readOnly: boolean; className?: string }) {
return <TldrawLocalBody {...props} tldraw={tldraw} />;
}
return LocalLoaded;
}),
{ ssr: false, loading: () => <WhiteboardLoading /> },
);
function WhiteboardLoading() {
return (
<div
className="flex h-full min-h-[320px] w-full animate-pulse items-center justify-center rounded-lg border border-dashed border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.35)] text-xs text-muted-foreground"
aria-hidden
>
Loading whiteboard
</div>
);
}
export function WhiteboardCanvas({ documentId, className, readOnly = false }: WhiteboardCanvasProps) {
const [client, setClient] = React.useState(false);
React.useEffect(() => setClient(true), []);
if (!client) {
return (
<div className={cn("relative flex min-h-0 flex-1 flex-col", className)}>
<WhiteboardLoading />
</div>
);
}
return (
<div
className={cn(
"relative flex min-h-0 flex-1 flex-col [&_.tl-container]:h-full [&_.tl-container]:min-h-0",
className,
)}
>
{documentId ? (
<TldynamicCollab documentId={documentId} readOnly={!!readOnly} className={className} />
) : (
<TldynamicLocal readOnly={!!readOnly} className={className} />
)}
</div>
);
}