start liveblocks integration

This commit is contained in:
Ishaan Dey 2024-05-03 13:53:21 -07:00
parent 6e28d283cd
commit 0f1654e3dd
8 changed files with 467 additions and 14 deletions

View File

@ -1,4 +1,5 @@
import Navbar from "@/components/editor/navbar"
import { Room } from "@/components/editor/room"
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import dynamic from "next/dynamic"
@ -48,10 +49,12 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return (
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
{/* <Room> */}
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxId={sandboxId} />
</div>
{/* </Room> */}
</div>
)
}

View File

@ -1,3 +1,4 @@
import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
@ -13,9 +14,9 @@ export default async function AppAuthLayout({
}
const dbUser = await fetch(`http://localhost:8787/api/user?id=${user.id}`)
const dbUserJSON = await dbUser.json()
const dbUserJSON = (await dbUser.json()) as User
if (!dbUserJSON?.id) {
if (!dbUserJSON.id) {
const res = await fetch("http://localhost:8787/api/user", {
method: "POST",
headers: {

View File

@ -0,0 +1,43 @@
import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { Liveblocks } from "@liveblocks/node"
import { NextRequest } from "next/server"
const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY!
const liveblocks = new Liveblocks({
secret: API_KEY!,
})
export async function POST(request: NextRequest) {
const clerkUser = await currentUser()
if (!clerkUser) {
return new Response("Unauthorized", { status: 401 })
}
const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`)
const user = (await res.json()) as User
// Create a session for the current user
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers
const session = liveblocks.prepareSession(user.id, {
userInfo: {
id: user.id,
name: user.name,
email: user.email,
},
})
// Give the user access to the room
user.sandbox.forEach((sandbox) => {
session.allow(`${sandbox.id}`, session.FULL_ACCESS)
})
user.usersToSandboxes.forEach((userToSandbox) => {
session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS)
})
// Authorize the user and return the result
const { body, status } = await session.authorize()
return new Response(body, { status })
}

View File

@ -1,9 +1,17 @@
"use client"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import monaco from "monaco-editor"
import { useEffect, useRef, useState } from "react"
// import theme from "./theme.json"
import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { io } from "socket.io-client"
import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs"
import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"
import { useRoom } from "@/liveblocks.config"
import {
ResizableHandle,
@ -22,17 +30,12 @@ import {
} from "lucide-react"
import Tab from "../ui/tab"
import Sidebar from "./sidebar"
import { useClerk } from "@clerk/nextjs"
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
import { io } from "socket.io-client"
import { processFileType, validateName } from "@/lib/utils"
import { toast } from "sonner"
import EditorTerminal from "./terminal"
import { Button } from "../ui/button"
import { User } from "@/lib/types"
import { Input } from "../ui/input"
import GenerateInput from "./generate"
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
import { User } from "@/lib/types"
import { processFileType, validateName } from "@/lib/utils"
export default function CodeEditor({
userData,
@ -62,6 +65,7 @@ export default function CodeEditor({
const [terminals, setTerminals] = useState<string[]>([])
const clerk = useClerk()
const room = useRoom()
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const editorContainerRef = useRef<HTMLDivElement>(null)
@ -230,7 +234,7 @@ export default function CodeEditor({
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
const activeTab = tabs.find((t) => t.id === activeId)
// const activeTab = tabs.find((t) => t.id === activeId)
// console.log("saving:", activeTab?.name, editorRef.current?.getValue())
setTabs((prev) =>
@ -258,6 +262,27 @@ export default function CodeEditor({
}
})
useEffect(() => {
if (!editorRef.current) return
const yDoc = new Y.Doc()
const yText = yDoc.getText("monaco")
const yProvider: any = new LiveblocksProvider(room, yDoc)
const binding = new MonacoBinding(
yText,
editorRef.current.getModel() as monaco.editor.ITextModel,
new Set([editorRef.current]),
yProvider.awareness as Awareness
)
return () => {
yDoc.destroy()
yProvider.destroy()
binding.destroy()
}
}, [editorRef, room])
// connection/disconnection effect + resizeobserver
useEffect(() => {
socket.connect()

View File

@ -0,0 +1,22 @@
"use client"
import { RoomProvider } from "@/liveblocks.config"
import { useSearchParams } from "next/navigation"
import { ClientSideSuspense } from "@liveblocks/react"
export function Room({ children }: { children: React.ReactNode }) {
// const roomId = useExampleRoomId("liveblocks:examples:nextjs-yjs-monaco");
return (
<RoomProvider
id={"roomId"}
initialPresence={{
cursor: null,
}}
>
<ClientSideSuspense fallback={<div>Loading!!!!</div>}>
{() => children}
</ClientSideSuspense>
</RoomProvider>
)
}

View File

@ -0,0 +1,161 @@
import { createClient } from "@liveblocks/client"
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react"
import YLiveblocksProvider from "@liveblocks/yjs"
const client = createClient({
// publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
authEndpoint: "/api/lb-auth",
// throttle: 100,
async resolveUsers({ userIds }) {
// Used only for Comments and Notifications. Return a list of user information
// retrieved from `userIds`. This info is used in comments, mentions etc.
// const usersData = await __fetchUsersFromDB__(userIds);
//
// return usersData.map((userData) => ({
// name: userData.name,
// avatar: userData.avatar.src,
// }));
return []
},
async resolveMentionSuggestions({ text }) {
// Used only for Comments. Return a list of userIds that match `text`.
// These userIds are used to create a mention list when typing in the
// composer.
//
// For example when you type "@jo", `text` will be `"jo"`, and
// you should to return an array with John and Joanna's userIds:
// ["john@example.com", "joanna@example.com"]
// const users = await getUsers({ search: text });
// return users.map((user) => user.id);
return []
},
async resolveRoomsInfo({ roomIds }) {
// Used only for Comments and Notifications. Return a list of room information
// retrieved from `roomIds`.
// const roomsData = await __fetchRoomsFromDB__(roomIds);
//
// return roomsData.map((roomData) => ({
// name: roomData.name,
// url: roomData.url,
// }));
return []
},
})
// Presence represents the properties that exist on every user in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
// cursor: { x: number, y: number } | null,
// ...
}
// Optionally, Storage represents the shared document that persists in the
// Room, even after all users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
// author: LiveObject<{ firstName: string, lastName: string }>,
// ...
}
// Optionally, UserMeta represents static/readonly metadata on each user, as
// provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user's name or avatar.
type UserMeta = {
// id?: string, // Accessible through `user.id`
// info?: Json, // Accessible through `user.info`
}
// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
type RoomEvent = {
// type: "NOTIFICATION",
// ...
}
// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
export type ThreadMetadata = {
// resolved: boolean;
// quote: string;
// time: number;
}
// Room-level hooks, use inside `RoomProvider`
export const {
suspense: {
RoomProvider,
useRoom,
useMyPresence,
useUpdateMyPresence,
useSelf,
useOthers,
useOthersMapped,
useOthersListener,
useOthersConnectionIds,
useOther,
useBroadcastEvent,
useEventListener,
useErrorListener,
useStorage,
useObject,
useMap,
useList,
useBatch,
useHistory,
useUndo,
useRedo,
useCanUndo,
useCanRedo,
useMutation,
useStatus,
useLostConnectionListener,
useThreads,
useCreateThread,
useEditThreadMetadata,
useCreateComment,
useEditComment,
useDeleteComment,
useAddReaction,
useRemoveReaction,
useThreadSubscription,
useMarkThreadAsRead,
useRoomNotificationSettings,
useUpdateRoomNotificationSettings,
// These hooks can be exported from either context
// useUser,
// useRoomInfo
},
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(
client
)
// Project-level hooks, use inside `LiveblocksProvider`
export const {
suspense: {
LiveblocksProvider,
useMarkInboxNotificationAsRead,
useMarkAllInboxNotificationsAsRead,
useInboxNotifications,
useUnreadInboxNotificationsCount,
// These hooks can be exported from either context
useUser,
useRoomInfo,
},
} = createLiveblocksContext<UserMeta, ThreadMetadata>(client)
export type TypedLiveblocksProvider = YLiveblocksProvider<
Presence,
Storage,
UserMeta,
RoomEvent
>

View File

@ -11,6 +11,10 @@
"@clerk/nextjs": "^4.29.12",
"@clerk/themes": "^1.7.12",
"@hookform/resolvers": "^3.3.4",
"@liveblocks/client": "^1.12.0",
"@liveblocks/node": "^1.12.0",
"@liveblocks/react": "^1.12.0",
"@liveblocks/yjs": "^1.12.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.1.5",
@ -38,6 +42,9 @@
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"vscode-icons-js": "^11.6.1",
"y-monaco": "^0.1.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15",
"zod": "^3.23.3"
},
"devDependencies": {
@ -340,6 +347,57 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@liveblocks/client": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.12.0.tgz",
"integrity": "sha512-TL4sPbWBlrGF7UXLNx2RYuZi/Z51jXALMFAdaWkYE0Qz7mMwxTVSyjPVR7ZXVuqdSo0CTQ1rpPyZeqcoUDtEoQ==",
"dependencies": {
"@liveblocks/core": "1.12.0"
}
},
"node_modules/@liveblocks/core": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.12.0.tgz",
"integrity": "sha512-cPuVZwSh+EBnJL8DA999h4QDNSlOMFyxHPPHm9qVBf9Cl+NY7/Bg3cyKFiHOki6g7y8dQj8No2tpnAHWdqqalA=="
},
"node_modules/@liveblocks/node": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@liveblocks/node/-/node-1.12.0.tgz",
"integrity": "sha512-xq9EusU2ntZ2vkIQT6TrqPWv4N+txMkzn2FgNllJxt+VdJhhvbt1UkAIokywIZSElFC6gt1VUNMRFM+a7nSJUw==",
"dependencies": {
"@liveblocks/core": "1.12.0",
"@stablelib/base64": "^1.0.1",
"fast-sha256": "^1.3.0",
"node-fetch": "^2.6.1"
}
},
"node_modules/@liveblocks/react": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.12.0.tgz",
"integrity": "sha512-bxI420FRqUqEAHIyC6jTT40JiMDaum6+7Ag6osme2N0YsshOKZx4/z3uCD3LPdOQAzhaHiv9ZmVYwRFp3z9RfA==",
"dependencies": {
"@liveblocks/client": "1.12.0",
"@liveblocks/core": "1.12.0",
"nanoid": "^3",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.14.0 || ^17 || ^18"
}
},
"node_modules/@liveblocks/yjs": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@liveblocks/yjs/-/yjs-1.12.0.tgz",
"integrity": "sha512-jCGUlfh8hW2Cr5jRrx/c/dBaY3u6psIIVPETDO91AvKgeVld50I7tcYGjFARWwaOYMhSD3c57iAsOjEkGIvpHA==",
"dependencies": {
"@liveblocks/client": "1.12.0",
"@liveblocks/core": "1.12.0",
"js-base64": "^3.7.5"
},
"peerDependencies": {
"yjs": "^13.6.1"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
@ -1329,6 +1387,11 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="
},
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@ -2008,6 +2071,11 @@
"resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.3.tgz",
"integrity": "sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A=="
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@ -2228,6 +2296,15 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@ -2253,6 +2330,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
},
"node_modules/js-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
@ -2266,6 +2348,26 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/lib0": {
"version": "0.2.93",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.93.tgz",
"integrity": "sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==",
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -2522,6 +2624,25 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch-native": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz",
@ -3376,6 +3497,11 @@
"to-no-case": "^1.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -3519,6 +3645,20 @@
"tslib": "^2.6.2"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3645,6 +3785,41 @@
"node": ">=0.4.0"
}
},
"node_modules/y-monaco": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.5.tgz",
"integrity": "sha512-zyTCMXtewIG+jP9JikJ5NN3/jLCqpkDY3titPIGBrmVeZsJr6qVeFVP3hPfmWOPHGlGZ39KCIzXrC7mFZRNFIw==",
"dependencies": {
"lib0": "^0.2.43"
},
"engines": {
"node": ">=12.0.0",
"npm": ">=6.0.0"
},
"peerDependencies": {
"monaco-editor": ">=0.20.0",
"yjs": "^13.3.1"
}
},
"node_modules/y-protocols": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
"integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==",
"dependencies": {
"lib0": "^0.2.85"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"peerDependencies": {
"yjs": "^13.0.0"
}
},
"node_modules/yaml": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
@ -3656,6 +3831,22 @@
"node": ">= 14"
}
},
"node_modules/yjs": {
"version": "13.6.15",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.15.tgz",
"integrity": "sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==",
"dependencies": {
"lib0": "^0.2.86"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/zod": {
"version": "3.23.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.3.tgz",

View File

@ -12,6 +12,10 @@
"@clerk/nextjs": "^4.29.12",
"@clerk/themes": "^1.7.12",
"@hookform/resolvers": "^3.3.4",
"@liveblocks/client": "^1.12.0",
"@liveblocks/node": "^1.12.0",
"@liveblocks/react": "^1.12.0",
"@liveblocks/yjs": "^1.12.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.1.5",
@ -39,6 +43,9 @@
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"vscode-icons-js": "^11.6.1",
"y-monaco": "^0.1.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15",
"zod": "^3.23.3"
},
"devDependencies": {