diff --git a/src/json-crdt-peritext-ui/__demos__/components/App.tsx b/src/json-crdt-peritext-ui/__demos__/components/App.tsx
index 453f911938..9ae03602a7 100644
--- a/src/json-crdt-peritext-ui/__demos__/components/App.tsx
+++ b/src/json-crdt-peritext-ui/__demos__/components/App.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import {Provider, GlobalCss} from 'nano-theme';
import {ModelWithExt, ext} from '../../../json-crdt-extensions';
import {PeritextView} from '../../react';
+import {cursorPlugin} from '../../plugins/cursor';
import {renderers} from '../../plugins/default';
import {renderers as debugRenderers} from '../../plugins/debug';
@@ -21,7 +22,7 @@ export const App: React.FC = () => {
);
diff --git a/src/json-crdt-peritext-ui/components/CaretScore/index.tsx b/src/json-crdt-peritext-ui/components/CaretScore/index.tsx
index d9d03ff966..781a021f6b 100644
--- a/src/json-crdt-peritext-ui/components/CaretScore/index.tsx
+++ b/src/json-crdt-peritext-ui/components/CaretScore/index.tsx
@@ -3,7 +3,7 @@ import {keyframes, rule} from 'nano-theme';
const scoreAnimation = keyframes({
from: {
- op: 0.7,
+ op: 0.8,
tr: 'scale(1.2)',
},
to: {
@@ -38,12 +38,12 @@ const scoreClass = rule({
const scoreDeltaClass = rule({
pos: 'absolute',
- b: '1.27em',
+ b: '1.3em',
l: '1.2em',
fz: '.5em',
fw: 'bold',
op: 0.5,
- col: 'blue',
+ col: '#07f',
an: scoreAnimation + ' .3s ease-out forwards',
pe: 'none',
us: 'none',
diff --git a/src/json-crdt-peritext-ui/plugins/default/RenderAnchor.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderAnchor.tsx
similarity index 100%
rename from src/json-crdt-peritext-ui/plugins/default/RenderAnchor.tsx
rename to src/json-crdt-peritext-ui/plugins/cursor/RenderAnchor.tsx
diff --git a/src/json-crdt-peritext-ui/plugins/default/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
similarity index 96%
rename from src/json-crdt-peritext-ui/plugins/default/RenderCaret.tsx
rename to src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
index a447706ae2..6c5609e677 100644
--- a/src/json-crdt-peritext-ui/plugins/default/RenderCaret.tsx
+++ b/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
@@ -5,7 +5,7 @@ import {usePeritext} from '../../react/context';
import {useSyncStore} from '../../react/hooks';
import {DefaultRendererColors} from './constants';
import {CommonSliceType} from '../../../json-crdt-extensions';
-import {useDefaultPlugin} from './context';
+import {useCursorPlugin} from './context';
import {CaretScore} from '../../components/CaretScore';
import type {CaretViewProps} from '../../react/selection/CaretView';
@@ -54,7 +54,7 @@ export const RenderCaret: React.FC = ({italic, children}) => {
useHarmonicIntervalFn(() => setShow(Date.now() % (ms + ms) > ms), ms);
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
- const plugin = useDefaultPlugin();
+ const plugin = useCursorPlugin();
const score = plugin.score.value;
const delta = plugin.scoreDelta.value;
diff --git a/src/json-crdt-peritext-ui/plugins/default/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderFocus.tsx
similarity index 100%
rename from src/json-crdt-peritext-ui/plugins/default/RenderFocus.tsx
rename to src/json-crdt-peritext-ui/plugins/cursor/RenderFocus.tsx
diff --git a/src/json-crdt-peritext-ui/plugins/cursor/RenderInline.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderInline.tsx
new file mode 100644
index 0000000000..f44e679838
--- /dev/null
+++ b/src/json-crdt-peritext-ui/plugins/cursor/RenderInline.tsx
@@ -0,0 +1,45 @@
+// biome-ignore lint: React is used for JSX
+import * as React from 'react';
+import {usePeritext} from '../../react';
+import {useSyncStore} from '../../react/hooks';
+import {DefaultRendererColors} from './constants';
+import type {InlineViewProps} from '../../react/InlineView';
+
+interface RenderInlineSelectionProps extends RenderInlineProps {
+ selection: [left: 'anchor' | 'focus' | '', right: 'anchor' | 'focus' | ''];
+}
+
+const RenderInlineSelection: React.FC = (props) => {
+ const {children, selection} = props;
+ const {dom} = usePeritext();
+ const focus = useSyncStore(dom.cursor.focus);
+
+ const [left, right] = selection;
+ const style: React.CSSProperties = {
+ backgroundColor: focus ? DefaultRendererColors.ActiveSelection : DefaultRendererColors.InactiveSelection,
+ borderRadius: left === 'anchor' ? '.25em 1px 1px .25em' : right === 'anchor' ? '1px .25em .25em 1px' : '1px',
+ };
+
+ return {children};
+};
+
+export interface RenderInlineProps extends InlineViewProps {
+ children: React.ReactNode;
+}
+
+export const RenderInline: React.FC = (props) => {
+ const {inline, children} = props;
+ const selection = inline.selection();
+
+ let element = children;
+
+ if (selection) {
+ element = (
+
+ {element}
+
+ );
+ }
+
+ return element;
+};
diff --git a/src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx
new file mode 100644
index 0000000000..786f40e79b
--- /dev/null
+++ b/src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import {context, type CursorPluginContextValue} from './context';
+import {ValueSyncStore} from '../../../util/events/sync-store';
+import type {ChangeDetail} from '../../events/types';
+import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
+
+export interface RenderPeritextProps extends PeritextViewProps {
+ ctx?: PeritextSurfaceContextValue;
+ children?: React.ReactNode;
+}
+
+export const RenderPeritext: React.FC = ({ctx, children}) => {
+ const value: CursorPluginContextValue = React.useMemo(
+ () => ({
+ ctx,
+ score: new ValueSyncStore(0),
+ scoreDelta: new ValueSyncStore(0),
+ lastVisScore: new ValueSyncStore(0),
+ }),
+ [ctx],
+ );
+
+ React.useEffect(() => {
+ const dom = ctx?.dom;
+ if (!dom || !value) return;
+ let lastNow: number = 0;
+ const listener = (event: CustomEvent) => {
+ const now = Date.now();
+ const timeDiff = now - lastNow;
+ let delta = 0;
+ switch (event.detail.ev?.type) {
+ case 'delete':
+ case 'insert':
+ case 'format':
+ case 'marker': {
+ delta = timeDiff < 30 ? 10 : timeDiff < 70 ? 5 : timeDiff < 150 ? 2 : timeDiff <= 1000 ? 1 : -1;
+ break;
+ }
+ default: {
+ delta = timeDiff <= 1000 ? 0 : -1;
+ break;
+ }
+ }
+ if (delta) value.score.next(delta >= 0 ? value.score.value + delta : 0);
+ value.scoreDelta.next(delta);
+ lastNow = now;
+ };
+ dom.et.addEventListener('change', listener);
+ return () => {
+ dom.et.removeEventListener('change', listener);
+ };
+ }, [ctx?.dom, value]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/json-crdt-peritext-ui/plugins/default/constants.ts b/src/json-crdt-peritext-ui/plugins/cursor/constants.ts
similarity index 100%
rename from src/json-crdt-peritext-ui/plugins/default/constants.ts
rename to src/json-crdt-peritext-ui/plugins/cursor/constants.ts
diff --git a/src/json-crdt-peritext-ui/plugins/cursor/context.ts b/src/json-crdt-peritext-ui/plugins/cursor/context.ts
new file mode 100644
index 0000000000..fb290001e6
--- /dev/null
+++ b/src/json-crdt-peritext-ui/plugins/cursor/context.ts
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import type {PeritextSurfaceContextValue} from '../../react';
+import type {ValueSyncStore} from '../../../util/events/sync-store';
+
+export interface CursorPluginContextValue {
+ ctx?: PeritextSurfaceContextValue;
+
+ /** Current score. */
+ score: ValueSyncStore;
+
+ /** By how much the score changed. */
+ scoreDelta: ValueSyncStore;
+
+ /** The last score that was shown to the user. */
+ lastVisScore: ValueSyncStore;
+}
+
+export const context = React.createContext(null!);
+
+export const useCursorPlugin = () => React.useContext(context);
diff --git a/src/json-crdt-peritext-ui/plugins/cursor/index.ts b/src/json-crdt-peritext-ui/plugins/cursor/index.ts
new file mode 100644
index 0000000000..27c82153d3
--- /dev/null
+++ b/src/json-crdt-peritext-ui/plugins/cursor/index.ts
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import {RenderCaret} from './RenderCaret';
+import {RenderFocus} from './RenderFocus';
+import {RenderAnchor} from './RenderAnchor';
+import {RenderInline} from './RenderInline';
+import {RenderPeritext} from './RenderPeritext';
+import type {PeritextPlugin} from '../../react/types';
+
+const h = React.createElement;
+
+/**
+ * Plugin which renders the main cursor and all other current user local
+ * cursors.
+ */
+export const cursorPlugin: PeritextPlugin = {
+ caret: (props, children) => h(RenderCaret, props, children),
+ focus: (props, children) => h(RenderFocus, props, children),
+ anchor: (props, children) => h(RenderAnchor, props, children),
+ inline: (props, children) => h(RenderInline, props as any, children),
+ peritext: (props, children, ctx) => h(RenderPeritext, {...props, children, ctx}),
+};
diff --git a/src/json-crdt-peritext-ui/plugins/default/RenderInline.tsx b/src/json-crdt-peritext-ui/plugins/default/RenderInline.tsx
index 31383e0b37..68fb81813b 100644
--- a/src/json-crdt-peritext-ui/plugins/default/RenderInline.tsx
+++ b/src/json-crdt-peritext-ui/plugins/default/RenderInline.tsx
@@ -1,28 +1,7 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
-import {usePeritext} from '../../react';
-import {useSyncStore} from '../../react/hooks';
-import {DefaultRendererColors} from './constants';
-import type {InlineViewProps} from '../../react/InlineView';
import {CommonSliceType} from '../../../json-crdt-extensions';
-
-interface RenderInlineSelectionProps extends RenderInlineProps {
- selection: [left: 'anchor' | 'focus' | '', right: 'anchor' | 'focus' | ''];
-}
-
-const RenderInlineSelection: React.FC = (props) => {
- const {children, selection} = props;
- const {dom} = usePeritext();
- const focus = useSyncStore(dom.cursor.focus);
-
- const [left, right] = selection;
- const style: React.CSSProperties = {
- backgroundColor: focus ? DefaultRendererColors.ActiveSelection : DefaultRendererColors.InactiveSelection,
- borderRadius: left === 'anchor' ? '.25em 1px 1px .25em' : right === 'anchor' ? '1px .25em .25em 1px' : '1px',
- };
-
- return {children};
-};
+import type {InlineViewProps} from '../../react/InlineView';
export interface RenderInlineProps extends InlineViewProps {
children: React.ReactNode;
@@ -30,12 +9,8 @@ export interface RenderInlineProps extends InlineViewProps {
export const RenderInline: React.FC = (props) => {
const {inline, children} = props;
-
const attr = inline.attr();
- const selection = inline.selection();
-
let element = children;
-
if (attr[CommonSliceType.code]) element = {element}
;
if (attr[CommonSliceType.mark]) element = {element};
if (attr[CommonSliceType.del]) element = {element};
@@ -46,14 +21,5 @@ export const RenderInline: React.FC = (props) => {
if (attr[CommonSliceType.kbd]) element = {element};
if (attr[CommonSliceType.hidden])
element = {element};
-
- if (selection) {
- element = (
-
- {element}
-
- );
- }
-
return element;
};
diff --git a/src/json-crdt-peritext-ui/plugins/default/RenderPeritext.tsx b/src/json-crdt-peritext-ui/plugins/default/RenderPeritext.tsx
index 617755d522..0438130f8b 100644
--- a/src/json-crdt-peritext-ui/plugins/default/RenderPeritext.tsx
+++ b/src/json-crdt-peritext-ui/plugins/default/RenderPeritext.tsx
@@ -1,8 +1,6 @@
import * as React from 'react';
import {Chrome} from './Chrome';
import {context, type DefaultPluginContextValue} from './context';
-import {ValueSyncStore} from '../../../util/events/sync-store';
-import type {ChangeDetail} from '../../events/types';
import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
export interface RenderPeritextProps extends PeritextViewProps {
@@ -11,46 +9,7 @@ export interface RenderPeritextProps extends PeritextViewProps {
}
export const RenderPeritext: React.FC = ({ctx, children}) => {
- const value: DefaultPluginContextValue = React.useMemo(
- () => ({
- ctx,
- score: new ValueSyncStore(0),
- scoreDelta: new ValueSyncStore(0),
- lastVisScore: new ValueSyncStore(0),
- }),
- [ctx],
- );
-
- React.useEffect(() => {
- const dom = ctx?.dom;
- if (!dom || !value) return;
- let lastNow: number = 0;
- const listener = (event: CustomEvent) => {
- const now = Date.now();
- const timeDiff = now - lastNow;
- let delta = 0;
- switch (event.detail.ev?.type) {
- case 'delete':
- case 'insert':
- case 'format':
- case 'marker': {
- delta = timeDiff < 30 ? 10 : timeDiff < 70 ? 5 : timeDiff < 150 ? 2 : timeDiff <= 1000 ? 1 : -1;
- break;
- }
- default: {
- delta = timeDiff <= 1000 ? 0 : -1;
- break;
- }
- }
- if (delta) value.score.next(delta >= 0 ? value.score.value + delta : 0);
- value.scoreDelta.next(delta);
- lastNow = now;
- };
- dom.et.addEventListener('change', listener);
- return () => {
- dom.et.removeEventListener('change', listener);
- };
- }, [ctx?.dom, value]);
+ const value: DefaultPluginContextValue = React.useMemo(() => ({ctx}), [ctx]);
return (
diff --git a/src/json-crdt-peritext-ui/plugins/default/context.ts b/src/json-crdt-peritext-ui/plugins/default/context.ts
index 91547902b1..2c52e0b1a8 100644
--- a/src/json-crdt-peritext-ui/plugins/default/context.ts
+++ b/src/json-crdt-peritext-ui/plugins/default/context.ts
@@ -1,18 +1,8 @@
import * as React from 'react';
import type {PeritextSurfaceContextValue} from '../../react';
-import type {ValueSyncStore} from '../../../util/events/sync-store';
export interface DefaultPluginContextValue {
ctx?: PeritextSurfaceContextValue;
-
- /** Current score. */
- score: ValueSyncStore;
-
- /** By how much the score changed. */
- scoreDelta: ValueSyncStore;
-
- /** The last score that was shown to the user. */
- lastVisScore: ValueSyncStore;
}
export const context = React.createContext(null!);
diff --git a/src/json-crdt-peritext-ui/plugins/default/index.ts b/src/json-crdt-peritext-ui/plugins/default/index.ts
index a087dff02f..4a604c4977 100644
--- a/src/json-crdt-peritext-ui/plugins/default/index.ts
+++ b/src/json-crdt-peritext-ui/plugins/default/index.ts
@@ -1,7 +1,4 @@
import * as React from 'react';
-import {RenderCaret} from './RenderCaret';
-import {RenderFocus} from './RenderFocus';
-import {RenderAnchor} from './RenderAnchor';
import {RenderInline} from './RenderInline';
import {RenderPeritext} from './RenderPeritext';
import {text} from '../minimal/text';
@@ -12,9 +9,6 @@ const h = React.createElement;
export const renderers: PeritextPlugin = {
text,
- caret: (props, children) => h(RenderCaret, props, children),
- focus: (props, children) => h(RenderFocus, props, children),
- anchor: (props, children) => h(RenderAnchor, props, children),
inline: (props, children) => h(RenderInline, props as any, children),
block: (props, children) => h(RenderBlock, props as any, children),
peritext: (props, children, ctx) => h(RenderPeritext, {...props, children, ctx}),