Skip to content

Commit

Permalink
feat: split out UI to component registry and tweak docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
dpnova committed Dec 6, 2024
1 parent a06771e commit 74abc4c
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 15 deletions.
25 changes: 23 additions & 2 deletions apps/www/content/docs/collaboration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ title: Collaboration
## Features

- The yjs plugin enables support for collaboration using [slate-yjs](https://docs.slate-yjs.dev/) and [Hocuspocus](https://docs.slate-yjs.dev/walkthroughs/collaboration-hocuspocus).
- By default remote cursors are rendered slightly faded and become solid on hover. Use the `data` field in `cursorOptions` to customise the display name and color.
- To customize, copy `YjsAboveEditable.tsx` and `Overlay.tsx` into your project and override the `options.render.aboveEditable` option to this plugin.
- By default remote cursors are not rendered unless you install the remote cursor overlay and include it in the config.
- Cursors are rendered slightly faded and become solid on hover. Use the `data` field in `cursorOptions` to customise the display name and color.

</PackageInfo>

Expand All @@ -30,12 +30,16 @@ npm install @udecode/plate-yjs

```tsx
import { YjsPlugin } from '@udecode/plate-yjs/react';
import { RemoteCursorOverlay } from '@/components/plate-ui/remote-cursor-overlay';

const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin.configure({
options: {
render: {
afterEditable: RemoteCursorOverlay,
},
cursorOptions: {
autoSend: true,
data: { name: 'A plate user', color: '#5AC990' },
Expand All @@ -51,6 +55,23 @@ const editor = createPlateEditor({
});
```

### Editor Container

The editor requires a container component above `PlateContent` to ensure correct cursor overlay positioning:

```tsx
export const EditorContainer = (
props: React.HTMLAttributes<HTMLDivElement>
) => {
const editor = useEditorRef();
const containerRef = useEditorContainerRef();

return <div id={editor.uid} ref={containerRef} {...props} />;
};
```

This component is available in [Editor](/docs/components/editor).

## Backend

Follow the backend instructions in [Hocuspocus docs](https://tiptap.dev/hocuspocus/getting-started).
Expand Down
24 changes: 24 additions & 0 deletions apps/www/public/r/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,30 @@
"registryDependencies": [],
"type": "registry:ui"
},
{
"dependencies": [
"@slate-yjs/react",
"@udecode/plate-core"
],
"doc": {
"description": "A cursor overlay to display multiplayer cursors in the yjs plugin.",
"docs": [
{
"route": "/docs/collaboration"
}
],
"examples": []
},
"files": [
{
"path": "plate-ui/remote-cursor-overlay.tsx",
"type": "registry:ui"
}
],
"name": "remote-cursor-overlay",
"registryDependencies": [],
"type": "registry:ui"
},
{
"dependencies": [
"@udecode/plate-ai",
Expand Down
26 changes: 26 additions & 0 deletions apps/www/public/r/styles/default/remote-cursor-overlay.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"dependencies": [
"@slate-yjs/react",
"@udecode/plate-core"
],
"doc": {
"description": "A cursor overlay to display multiplayer cursors in the yjs plugin.",
"docs": [
{
"route": "/docs/collaboration"
}
],
"examples": []
},
"files": [
{
"content": "import React, {\n type CSSProperties,\n type PropsWithChildren,\n useState,\n} from 'react';\n\nimport {\n type CursorOverlayData,\n useRemoteCursorOverlayPositions,\n} from '@slate-yjs/react';\nimport { useEditorContainerRef } from '@udecode/plate-core/react';\n\nexport function addAlpha(hexColor: string, opacity: number): string {\n const normalized = Math.round(Math.min(Math.max(opacity, 0), 1) * 255);\n\n return hexColor + normalized.toString(16).toUpperCase();\n}\n\nexport type CursorData = {\n color: string;\n name: string;\n};\n\ntype CaretProps = Pick<CursorOverlayData<CursorData>, 'caretPosition' | 'data'>;\nconst cursorOpacity = 0.7;\nconst hoverOpacity = 1;\n\nfunction Caret({ caretPosition, data }: CaretProps) {\n const [isHover, setIsHover] = useState(false);\n\n const handleMouseEnter = () => {\n setIsHover(true);\n };\n const handleMouseLeave = () => {\n setIsHover(false);\n };\n const caretStyle: CSSProperties = {\n ...caretPosition,\n background: data?.color,\n opacity: cursorOpacity,\n transition: 'opacity 0.2s',\n };\n const caretStyleHover = { ...caretStyle, opacity: hoverOpacity };\n\n const labelStyle: CSSProperties = {\n background: data?.color,\n opacity: cursorOpacity,\n transform: 'translateY(-100%)',\n transition: 'opacity 0.2s',\n };\n const labelStyleHover = { ...labelStyle, opacity: hoverOpacity };\n\n return (\n <div\n className=\"absolute w-0.5\"\n style={isHover ? caretStyleHover : caretStyle}\n >\n <div\n className=\"absolute top-0 whitespace-nowrap rounded rounded-bl-none px-1.5 py-0.5 text-xs text-white\"\n style={isHover ? labelStyleHover : labelStyle}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {data?.name}\n </div>\n </div>\n );\n}\n\nfunction RemoteSelection({\n caretPosition,\n data,\n selectionRects,\n}: CursorOverlayData<CursorData>) {\n if (!data) {\n return null;\n }\n\n const selectionStyle: CSSProperties = {\n // Add a opacity to the background color\n backgroundColor: addAlpha(data.color, 0.5),\n };\n\n return (\n <React.Fragment>\n {selectionRects.map((position, i) => (\n <div\n key={i}\n className=\"pointer-events-none absolute\"\n style={{ ...selectionStyle, ...position }}\n ></div>\n ))}\n {caretPosition && <Caret caretPosition={caretPosition} data={data} />}\n </React.Fragment>\n );\n}\n\ntype RemoteCursorsProps = PropsWithChildren<{\n className?: string;\n}>;\n\nexport function RemoteCursorOverlay({ children }: RemoteCursorsProps) {\n const containerRef = useEditorContainerRef();\n const [cursors] = useRemoteCursorOverlayPositions<CursorData>({\n containerRef,\n });\n\n return (\n <>\n {children}\n {cursors.map((cursor) => (\n <RemoteSelection key={cursor.clientId} {...cursor} />\n ))}\n </>\n );\n}\n",
"path": "plate-ui/remote-cursor-overlay.tsx",
"target": "components/plate-ui/remote-cursor-overlay.tsx",
"type": "registry:ui"
}
],
"name": "remote-cursor-overlay",
"registryDependencies": [],
"type": "registry:ui"
}
16 changes: 16 additions & 0 deletions apps/www/src/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,22 @@ export const Index: Record<string, any> = {
subcategory: "",
chunks: []
},
"remote-cursor-overlay": {
name: "remote-cursor-overlay",
description: "",
type: "registry:ui",
registryDependencies: [],
files: [{
path: "src/registry/default/plate-ui/remote-cursor-overlay.tsx",
type: "registry:ui",
target: ""
}],
component: React.lazy(() => import("@/registry/default/plate-ui/remote-cursor-overlay.tsx")),
source: "",
category: "",
subcategory: "",
chunks: []
},
"ai-menu": {
name: "ai-menu",
description: "",
Expand Down
15 changes: 5 additions & 10 deletions apps/www/src/registry/default/plate-ui/remote-cursor-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
import React, {
type CSSProperties,
type PropsWithChildren,
useRef,
useState,
} from 'react';

import {
type CursorOverlayData,
useRemoteCursorOverlayPositions,
} from '@slate-yjs/react';

import { cn } from '@udecode/cn';
import { useEditorContainerRef } from '@udecode/plate-core/react';

export function addAlpha(hexColor: string, opacity: number): string {
const normalized = Math.round(Math.min(Math.max(opacity, 0), 1) * 255);
Expand Down Expand Up @@ -103,21 +101,18 @@ type RemoteCursorsProps = PropsWithChildren<{
className?: string;
}>;

export function RemoteCursorOverlay({
children,
className,
}: RemoteCursorsProps) {
const containerRef = useRef<HTMLDivElement>(null);
export function RemoteCursorOverlay({ children }: RemoteCursorsProps) {
const containerRef = useEditorContainerRef();
const [cursors] = useRemoteCursorOverlayPositions<CursorData>({
containerRef,
});

return (
<div ref={containerRef} className={cn('relative', className)}>
<>
{children}
{cursors.map((cursor) => (
<RemoteSelection key={cursor.clientId} {...cursor} />
))}
</div>
</>
);
}
15 changes: 15 additions & 0 deletions apps/www/src/registry/registry-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,21 @@ export const uiPrimitives: Registry = [
registryDependencies: [],
type: 'registry:ui',
},
{
dependencies: ['@slate-yjs/react', '@udecode/plate-core'],
doc: {
description:
'A cursor overlay to display multiplayer cursors in the yjs plugin.',
docs: [{ route: '/docs/collaboration' }],
examples: [],
},
files: [
{ path: 'plate-ui/remote-cursor-overlay.tsx', type: 'registry:ui' },
],
name: 'remote-cursor-overlay',
registryDependencies: [],
type: 'registry:ui',
},
];

export const ui: Registry = [...uiNodes, ...uiPrimitives, ...uiComponents];
3 changes: 1 addition & 2 deletions packages/yjs/src/react/YjsAboveEditable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { YjsEditor } from '@slate-yjs/core';
import { useEditorPlugin } from '@udecode/plate-common/react';

import { type YjsConfig, BaseYjsPlugin } from '../lib/BaseYjsPlugin';
import { RemoteCursorOverlay } from './Overlay';

export const YjsAboveEditable: React.FC<{
children: React.ReactNode;
Expand All @@ -29,5 +28,5 @@ export const YjsAboveEditable: React.FC<{

if (!isSynced) return null;

return <RemoteCursorOverlay>{children}</RemoteCursorOverlay>;
return <>{children}</>;
};
4 changes: 3 additions & 1 deletion packages/yjs/src/react/YjsPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ import { YjsAboveEditable } from './YjsAboveEditable';

/** Enables support for real-time collaboration using Yjs. */
export const YjsPlugin = toPlatePlugin(BaseYjsPlugin, {
render: { aboveEditable: YjsAboveEditable },
render: {
aboveEditable: YjsAboveEditable,
},
});

0 comments on commit 74abc4c

Please sign in to comment.