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}),