mirror of
https://github.com/supabase/supabase.git
synced 2026-06-04 20:02:42 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature, docs update ## What is the new behavior? This PR introduces a new `RealtimeFlow` component and hook to the UI library for building collaborative React Flow with Supabase Realtime: - keeps nodes and edges in sync across multiple connected clients in real time - uses Yjs with `@supabase-labs/y-supabase` to propagate flow updates - supports optional persistence, so a flow can be restored from previously saved shared state ## Additional context https://github.com/user-attachments/assets/90d3a381-6f9c-427f-a493-5d91c2141462 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Collaborative "Realtime Flow" diagram editor with syncing overlays and a dual-view demo component * Interactive demo page and registry example for live editing (add/remove/rename nodes) * Framework-ready registry packages for Next.js, React, React Router, and TanStack * **Documentation** * Comprehensive docs added for Next.js, React, React Router, and TanStack (usage, persistence, hook API) * **Chores** * Added runtime dependency for the flow component package [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/44273) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
42 lines
7.2 KiB
JSON
42 lines
7.2 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "realtime-monaco-react-router",
|
|
"type": "registry:component",
|
|
"title": "Realtime Monaco",
|
|
"description": "Real-time Monaco editor for collaborative applications.",
|
|
"dependencies": [
|
|
"@monaco-editor/react@latest",
|
|
"y-monaco@latest",
|
|
"yjs@latest",
|
|
"@supabase/ssr@latest",
|
|
"@supabase/supabase-js@latest"
|
|
],
|
|
"registryDependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "registry/default/blocks/realtime-monaco/hooks/use-connect-on-mount.ts",
|
|
"content": "'use client'\n\nimport { SupabasePersistenceOptions, SupabaseProvider } from '@supabase-labs/y-supabase'\nimport type { editor as MonacoEditor } from 'monaco-editor'\nimport { useCallback, useEffect, useRef } from 'react'\nimport { MonacoBinding } from 'y-monaco'\nimport { Awareness } from 'y-protocols/awareness'\nimport * as Y from 'yjs'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\ntype UseConnectOnMountOptions = {\n channel: string\n persistence?: boolean | SupabasePersistenceOptions\n awareness?: boolean | Awareness\n}\n\nconst generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 80%, 60%)`\n\nexport function useConnectOnMount({\n channel,\n persistence,\n awareness = true,\n}: UseConnectOnMountOptions) {\n const docRef = useRef<Y.Doc | null>(null)\n const providerRef = useRef<SupabaseProvider | null>(null)\n const bindingRef = useRef<MonacoBinding | null>(null)\n const userRef = useRef<{ color: string } | null>(null)\n const styleRef = useRef<HTMLStyleElement | null>(null)\n const awarenessHandlerRef = useRef<(() => void) | null>(null)\n\n const connectOnMount = useCallback(\n (editor: MonacoEditor.IStandaloneCodeEditor) => {\n if (bindingRef.current) return\n\n const model = editor.getModel()\n if (!model) return\n\n const doc = new Y.Doc()\n const yText = doc.getText('monaco')\n const supabase = createClient()\n const provider = new SupabaseProvider(channel, doc, supabase as any, {\n awareness,\n persistence,\n })\n const providerAwareness = provider.getAwareness()\n\n if (providerAwareness) {\n if (!userRef.current) {\n userRef.current = {\n color: generateRandomColor(),\n }\n }\n providerAwareness.setLocalStateField('user', userRef.current)\n\n const applyAwarenessStyles = () => {\n if (!styleRef.current) {\n styleRef.current = document.createElement('style')\n styleRef.current.setAttribute('data-monaco-y-cursors', 'true')\n document.head.appendChild(styleRef.current)\n }\n\n let css = `\n .yRemoteSelection {\n background-color: var(--y-remote-selection-color, rgba(0, 0, 0, 0.2));\n opacity: 0.2;\n }\n .yRemoteSelectionHead {\n border-left: 2px solid var(--y-remote-selection-color, rgba(0, 0, 0, 0.7));\n margin-left: -1px;\n box-sizing: border-box;\n }\n `\n\n providerAwareness.getStates().forEach((state, clientId) => {\n const color = state?.user?.color\n const isValidHsl = /^hsl\\(\\d{1,3},\\s*\\d{1,3}%,\\s*\\d{1,3}%\\)$/.test(color)\n\n if (!isValidHsl) return\n if (!color) return\n\n css += `\n .yRemoteSelection-${clientId}, .yRemoteSelectionHead-${clientId} {\n --y-remote-selection-color: ${color};\n }\n `\n })\n\n styleRef.current.textContent = css\n }\n\n awarenessHandlerRef.current = applyAwarenessStyles\n applyAwarenessStyles()\n providerAwareness.on('update', applyAwarenessStyles)\n }\n\n docRef.current = doc\n providerRef.current = provider\n bindingRef.current = new MonacoBinding(yText, model, new Set([editor]), providerAwareness)\n },\n [channel]\n )\n\n useEffect(() => {\n return () => {\n if (awarenessHandlerRef.current && providerRef.current) {\n const awareness = providerRef.current.getAwareness()\n awareness?.off('update', awarenessHandlerRef.current)\n }\n styleRef.current?.remove()\n bindingRef.current?.destroy()\n bindingRef.current = null\n providerRef.current?.destroy()\n providerRef.current = null\n docRef.current?.destroy()\n docRef.current = null\n }\n }, [])\n\n return { connectOnMount }\n}\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/realtime-monaco/components/realtime-monaco.tsx",
|
|
"content": "'use client'\n\nimport { Editor } from '@monaco-editor/react'\nimport { SupabasePersistenceOptions } from '@supabase-labs/y-supabase'\nimport { Awareness } from 'y-protocols/awareness.js'\n\nimport { useConnectOnMount } from '../hooks/use-connect-on-mount'\n\ntype RealtimeMonacoProps = {\n channel: string\n language?: string\n height?: string | number\n className?: string\n awareness?: boolean | Awareness\n persistence?: boolean | SupabasePersistenceOptions\n theme?: 'light' | 'dark'\n}\n\nconst DEFAULT_HEIGHT = 550\n\nconst RealtimeMonaco = ({\n channel,\n language = 'javascript',\n height = DEFAULT_HEIGHT,\n awareness = true,\n persistence,\n theme,\n ...rest\n}: RealtimeMonacoProps) => {\n const { connectOnMount } = useConnectOnMount({ channel, persistence, awareness })\n\n return (\n <Editor\n height={height}\n language={language}\n theme={theme === 'dark' ? 'vs-dark' : 'light'}\n onMount={connectOnMount}\n {...rest}\n />\n )\n}\n\nexport { RealtimeMonaco }\n",
|
|
"type": "registry:component"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react-router/lib/supabase/client.ts",
|
|
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n",
|
|
"type": "registry:lib"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react-router/lib/supabase/server.ts",
|
|
"content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n",
|
|
"type": "registry:lib"
|
|
}
|
|
],
|
|
"envVars": {
|
|
"VITE_SUPABASE_URL": "",
|
|
"VITE_SUPABASE_PUBLISHABLE_KEY": ""
|
|
},
|
|
"docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`."
|
|
} |