Skip to content

Commit da37142

Browse files
authored
Merge pull request #808 from streamich/peritext-floating-menus
Peritext floating menus setup
2 parents 364d22b + 9c29f06 commit da37142

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1127
-168
lines changed

.github/workflows/gh-pages.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
jobs:
88
gh-pages:
99
runs-on: ubuntu-latest
10+
permissions:
11+
contents: write
1012
strategy:
1113
matrix:
1214
node-version: [20.x]

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"@jsonjoy.com/util": "^1.4.0",
135135
"arg": "^5.0.2",
136136
"hyperdyperid": "^1.2.0",
137+
"nano-css": "^5.6.2",
137138
"sonic-forest": "^1.0.3",
138139
"thingies": "^2.1.1",
139140
"tree-dump": "^1.0.2",
@@ -155,7 +156,7 @@
155156
"json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d",
156157
"json-logic-js": "^2.0.2",
157158
"nano-theme": "^1.4.3",
158-
"nice-ui": "^1.18.0",
159+
"nice-ui": "^1.25.0",
159160
"quill-delta": "^5.1.0",
160161
"react": "^18.3.1",
161162
"react-dom": "^18.3.1",

src/json-crdt-extensions/peritext/slice/constants.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@ export const enum SliceTypeCon {
6262
bg = -18, // <span style="background: ...">
6363
kbd = -19, // <kbd>
6464
hidden = -20, // <span style="color: transparent; background: black">
65-
footnote = -21, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
66-
ref = -22, // <a> with href="#ref-..." and title="Reference ..." (Reference to some element in the document)
67-
iaside = -23, // Inline <aside>
68-
iembed = -24, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
69-
bookmark = -25, // UI for creating a link to this slice
65+
q = -21, // <q> (inline quote)
66+
cite = -22, // <cite> (inline citation)
67+
footnote = -23, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
68+
ref = -24, // <a> with href="#ref-..." and title="Reference ..." (Reference to some element in the document)
69+
iaside = -25, // Inline <aside>
70+
iembed = -26, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
71+
bookmark = -27, // UI for creating a link to this slice
7072
}
7173

7274
/**

src/json-crdt-peritext-ui/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,16 @@
22

33
CRDT-native UI for JSON CRDT `peritext` extension. Supports block-level and
44
inline-level collaborative editing, with the ability to nest blocks.
5+
6+
7+
## Software architecture layers
8+
9+
Below is the software architecture of the Peritext rich text editor. At the top
10+
is the most user-facing layer, and at the bottom is the most foundational layer.
11+
12+
- Rendering surface plugins
13+
- React rendering surface `<PeritextFragment controller={DomController} />`
14+
- DOM event handlers, `new DomController(defaults: PeritextEventDefaults)`
15+
- Peritext events, `create(txt: Peritext): PeritextEventDefaults`
16+
- Peritext JSON CRDT extension, `Peritext`
17+
- JSON CRDT, `Model`

src/json-crdt-peritext-ui/__demos__/components/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import {Provider, GlobalCss} from 'nano-theme';
3+
import {NiceUiProvider} from 'nice-ui/lib/context';
34
import {ModelWithExt, ext} from '../../../json-crdt-extensions';
45
import {PeritextView} from '../../react';
56
import {CursorPlugin} from '../../plugins/cursor';
@@ -26,11 +27,11 @@ export const App: React.FC = () => {
2627
}, []);
2728

2829
return (
29-
<Provider theme={'light'}>
30+
<NiceUiProvider>
3031
<GlobalCss />
3132
<div style={{maxWidth: '690px', fontSize: '21px', lineHeight: '1.7em', margin: '32px auto'}}>
3233
<PeritextView peritext={peritext} plugins={plugins} />
3334
</div>
34-
</Provider>
35+
</NiceUiProvider>
3536
);
3637
};

src/json-crdt-peritext-ui/__demos__/main.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ document.body.appendChild(div);
77

88
const root = createRoot(div);
99
root.render(
10-
<React.StrictMode>
11-
<App />
12-
</React.StrictMode>,
10+
// <React.StrictMode>
11+
<App />,
12+
// </React.StrictMode>,
1313
);

src/json-crdt-peritext-ui/dom/DomController.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import {printTree, type Printable} from 'tree-dump';
22
import {InputController} from '../dom/InputController';
33
import {CursorController} from '../dom/CursorController';
44
import {RichTextController} from '../dom/RichTextController';
5-
import {PeritextEventDefaults} from '../events/PeritextEventDefaults';
6-
import {PeritextEventTarget} from '../events/PeritextEventTarget';
75
import {KeyController} from '../dom/KeyController';
86
import {CompositionController} from '../dom/CompositionController';
7+
import type {PeritextEventDefaults} from '../events/PeritextEventDefaults';
8+
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
99
import type {UiLifeCycles} from '../dom/types';
10-
import type {Peritext} from '../../json-crdt-extensions';
1110

1211
export interface DomControllerOpts {
1312
source: HTMLElement;
14-
txt: Peritext;
13+
events: PeritextEventDefaults;
1514
}
1615

1716
export class DomController implements UiLifeCycles, Printable {
@@ -23,10 +22,9 @@ export class DomController implements UiLifeCycles, Printable {
2322
public readonly richText: RichTextController;
2423

2524
constructor(public readonly opts: DomControllerOpts) {
26-
const {source, txt} = opts;
27-
const et = (this.et = new PeritextEventTarget());
28-
const defaults = new PeritextEventDefaults(txt, et);
29-
et.defaults = defaults;
25+
const {source, events} = opts;
26+
const {txt} = events;
27+
const et = (this.et = opts.events.et);
3028
const keys = (this.keys = new KeyController({source}));
3129
const comp = (this.comp = new CompositionController({et, source, txt}));
3230
this.input = new InputController({et, source, txt, comp});

src/json-crdt-peritext-ui/dom/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,12 @@ export interface UiLifeCycles {
55
stop(): void;
66
}
77

8+
export interface UiLifeCyclesRender {
9+
/**
10+
* Called when UI component is mounted. Returns a function to be called when
11+
* the component is removed from the screen.
12+
*/
13+
start(): () => void;
14+
}
15+
816
export type Rect = Pick<DOMRect, 'x' | 'y' | 'width' | 'height'>;

src/json-crdt-peritext-ui/events/PeritextEventDefaults.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {CursorAnchor} from '../../json-crdt-extensions/peritext/slice/constants';
2+
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
23
import type {Peritext} from '../../json-crdt-extensions/peritext';
34
import type {EditorSlices} from '../../json-crdt-extensions/peritext/editor/EditorSlices';
4-
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
55
import type * as events from './types';
66

77
/**
@@ -13,8 +13,8 @@ import type * as events from './types';
1313
*/
1414
export class PeritextEventDefaults implements PeritextEventHandlerMap {
1515
public constructor(
16-
protected readonly txt: Peritext,
17-
protected readonly et: PeritextEventTarget,
16+
public readonly txt: Peritext,
17+
public readonly et: PeritextEventTarget,
1818
) {}
1919

2020
public readonly change = (event: CustomEvent<events.ChangeDetail>) => {};

src/json-crdt-peritext-ui/events/PeritextEventTarget.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {TypedEventTarget} from '../../util/events/TypedEventTarget';
1+
import {SubscriptionEventTarget} from '../../util/events/TypedEventTarget';
22
import type {PeritextEventDetailMap, CursorDetail, FormatDetail, DeleteDetail, MarkerDetail} from './types';
33

44
export type PeritextEventMap = {
@@ -11,7 +11,7 @@ export type PeritextEventHandlerMap = {
1111

1212
let __id = 0;
1313

14-
export class PeritextEventTarget extends TypedEventTarget<PeritextEventMap> {
14+
export class PeritextEventTarget extends SubscriptionEventTarget<PeritextEventMap> {
1515
public readonly id: number = __id++;
1616

1717
public defaults: Partial<PeritextEventHandlerMap> = {};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {PeritextEventDefaults} from './PeritextEventDefaults';
2+
import {PeritextEventTarget} from './PeritextEventTarget';
3+
import type {Peritext} from '../../json-crdt-extensions';
4+
5+
export const create = (txt: Peritext) => {
6+
const et = new PeritextEventTarget();
7+
const defaults = new PeritextEventDefaults(txt, et);
8+
et.defaults = defaults;
9+
return defaults;
10+
};

src/json-crdt-peritext-ui/plugins/cursor/RenderAnchor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33
import {rule, keyframes} from 'nano-theme';
44
import {DefaultRendererColors} from './constants';
55
import {usePeritext} from '../../react';
6-
import {useSyncStore} from '../../react/hooks';
6+
import {useSyncStoreOpt} from '../../react/hooks';
77
import type {AnchorViewProps} from '../../react/cursor/AnchorView';
88

99
export const fadeInAnimation = keyframes({
@@ -42,7 +42,7 @@ export interface RenderAnchorProps extends AnchorViewProps {
4242

4343
export const RenderAnchor: React.FC<RenderAnchorProps> = ({children}) => {
4444
const {dom} = usePeritext();
45-
const focus = useSyncStore(dom.cursor.focus);
45+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
4646

4747
const style = focus ? undefined : {background: DefaultRendererColors.InactiveCursor};
4848

src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import useHarmonicIntervalFn from 'react-use/lib/useHarmonicIntervalFn';
33
import {keyframes, rule} from 'nano-theme';
44
import {usePeritext} from '../../react/context';
5-
import {useSyncStore} from '../../react/hooks';
5+
import {useSyncStore, useSyncStoreOpt} from '../../react/hooks';
66
import {DefaultRendererColors} from './constants';
77
import {CommonSliceType} from '../../../json-crdt-extensions';
88
import {useCursorPlugin} from './context';
@@ -54,7 +54,7 @@ export const RenderCaret: React.FC<RenderCaretProps> = ({italic, children}) => {
5454
const [show, setShow] = React.useState(true);
5555
useHarmonicIntervalFn(() => setShow(Date.now() % (ms + ms) > ms), ms);
5656
const {dom} = usePeritext();
57-
const focus = useSyncStore(dom.cursor.focus);
57+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
5858
const plugin = useCursorPlugin();
5959

6060
const score = plugin.score.value;

src/json-crdt-peritext-ui/plugins/cursor/RenderFocus.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33
import {rule, drule, keyframes} from 'nano-theme';
44
import {DefaultRendererColors} from './constants';
55
import {usePeritext} from '../../react';
6-
import {useSyncStore} from '../../react/hooks';
6+
import {useSyncStoreOpt} from '../../react/hooks';
77
import type {FocusViewProps} from '../../react/cursor/FocusView';
88

99
const width = 0.14;
@@ -43,7 +43,7 @@ export interface RenderFocusProps extends FocusViewProps {
4343

4444
export const RenderFocus: React.FC<RenderFocusProps> = ({left, italic, children}) => {
4545
const {dom} = usePeritext();
46-
const focus = useSyncStore(dom.cursor.focus);
46+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
4747

4848
const style: React.CSSProperties = focus ? {} : {background: DefaultRendererColors.InactiveCursor, animation: 'none'};
4949

src/json-crdt-peritext-ui/plugins/cursor/RenderInline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// biome-ignore lint: React is used for JSX
22
import * as React from 'react';
33
import {usePeritext} from '../../react';
4-
import {useSyncStore} from '../../react/hooks';
4+
import {useSyncStoreOpt} from '../../react/hooks';
55
import {DefaultRendererColors} from './constants';
66
import type {InlineViewProps} from '../../react/InlineView';
77

@@ -12,7 +12,7 @@ interface RenderInlineSelectionProps extends RenderInlineProps {
1212
const RenderInlineSelection: React.FC<RenderInlineSelectionProps> = (props) => {
1313
const {children, selection} = props;
1414
const {dom} = usePeritext();
15-
const focus = useSyncStore(dom.cursor.focus);
15+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
1616

1717
const [left, right] = selection;
1818
const style: React.CSSProperties = {

src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import * as React from 'react';
22
import {context, type CursorPluginContextValue} from './context';
33
import {ValueSyncStore} from '../../../util/events/sync-store';
44
import type {ChangeDetail} from '../../events/types';
5-
import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
5+
import type {PeritextSurfaceState, PeritextViewProps} from '../../react';
66
import type {CursorPlugin} from './CursorPlugin';
77

88
export interface RenderPeritextProps extends PeritextViewProps {
9-
ctx?: PeritextSurfaceContextValue;
9+
ctx?: PeritextSurfaceState;
1010
plugin: CursorPlugin;
1111
children?: React.ReactNode;
1212
}

src/json-crdt-peritext-ui/plugins/cursor/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as React from 'react';
2-
import type {PeritextSurfaceContextValue} from '../../react';
2+
import type {PeritextSurfaceState} from '../../react';
33
import type {ValueSyncStore} from '../../../util/events/sync-store';
44
import type {CursorPlugin} from './CursorPlugin';
55

66
export interface CursorPluginContextValue {
7-
ctx?: PeritextSurfaceContextValue;
7+
ctx?: PeritextSurfaceState;
88

99
plugin: CursorPlugin;
1010

src/json-crdt-peritext-ui/plugins/debug/RenderPeritext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {drule, rule, useTheme} from 'nano-theme';
33
import {context} from './context';
44
import {Button} from '../../components/Button';
55
import {Console} from './Console';
6-
import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
76
import {ValueSyncStore} from '../../../util/events/sync-store';
7+
import type {PeritextSurfaceState, PeritextViewProps} from '../../react';
88

99
const blockClass = rule({
1010
pos: 'relative',
@@ -29,7 +29,7 @@ const childrenDebugClass = rule({
2929
export interface RenderPeritextProps extends PeritextViewProps {
3030
enabled?: boolean;
3131
children?: React.ReactNode;
32-
ctx?: PeritextSurfaceContextValue;
32+
ctx?: PeritextSurfaceState;
3333
}
3434

3535
export const RenderPeritext: React.FC<RenderPeritextProps> = ({enabled: enabledProp = true, ctx, children}) => {

src/json-crdt-peritext-ui/plugins/debug/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import type {ValueSyncStore} from '../../../util/events/sync-store';
3-
import type {PeritextSurfaceContextValue} from '../../react';
3+
import type {PeritextSurfaceState} from '../../react/state';
44

55
export interface DebugRenderersContextValue {
66
enabled: boolean;
@@ -10,7 +10,7 @@ export interface DebugRenderersContextValue {
1010
peritext: ValueSyncStore<boolean>;
1111
model: ValueSyncStore<boolean>;
1212
};
13-
ctx?: PeritextSurfaceContextValue;
13+
ctx?: PeritextSurfaceState;
1414
}
1515

1616
export const context = React.createContext<DebugRenderersContextValue>(null!);

src/json-crdt-peritext-ui/plugins/minimal/RenderAnchor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {rule, keyframes} from 'nano-theme';
44
import {Char} from '../../constants';
55
import {DefaultRendererColors} from './constants';
66
import {usePeritext} from '../../react';
7-
import {useSyncStore} from '../../react/hooks';
7+
import {useSyncStoreOpt} from '../../react/hooks';
88
import type {AnchorViewProps} from '../../react/cursor/AnchorView';
99

1010
export const fadeInAnimation = keyframes({
@@ -42,7 +42,7 @@ export interface RenderAnchorProps extends AnchorViewProps {}
4242

4343
export const RenderAnchor: React.FC<RenderAnchorProps> = () => {
4444
const {dom} = usePeritext();
45-
const focus = useSyncStore(dom.cursor.focus);
45+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
4646

4747
const style = focus ? undefined : {background: DefaultRendererColors.InactiveCursor};
4848

src/json-crdt-peritext-ui/plugins/minimal/RenderCaret.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import useHarmonicIntervalFn from 'react-use/lib/useHarmonicIntervalFn';
33
import {keyframes, rule} from 'nano-theme';
44
import {usePeritext} from '../../react/context';
5-
import {useSyncStore} from '../../react/hooks';
5+
import {useSyncStore, useSyncStoreOpt} from '../../react/hooks';
66
import type {CaretViewProps} from '../../react/cursor/CaretView';
77
import {DefaultRendererColors} from './constants';
88
import {CommonSliceType} from '../../../json-crdt-extensions';
@@ -54,7 +54,7 @@ export const RenderCaret: React.FC<RenderCaretProps> = ({italic, children}) => {
5454
const [show, setShow] = React.useState(true);
5555
useHarmonicIntervalFn(() => setShow(Date.now() % (ms + ms) > ms), ms);
5656
const {dom} = usePeritext();
57-
const focus = useSyncStore(dom.cursor.focus);
57+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
5858
const plugin = usePlugin();
5959

6060
const score = plugin.score.value;

src/json-crdt-peritext-ui/plugins/minimal/RenderFocus.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33
import {rule, drule, keyframes} from 'nano-theme';
44
import {DefaultRendererColors} from './constants';
55
import {usePeritext} from '../../react';
6-
import {useSyncStore} from '../../react/hooks';
6+
import {useSyncStoreOpt} from '../../react/hooks';
77
import type {FocusViewProps} from '../../react/cursor/FocusView';
88

99
const width = 0.14;
@@ -44,7 +44,7 @@ export interface RenderFocusProps extends FocusViewProps {
4444

4545
export const RenderFocus: React.FC<RenderFocusProps> = ({left, italic, children}) => {
4646
const {dom} = usePeritext();
47-
const focus = useSyncStore(dom.cursor.focus);
47+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
4848

4949
const style: React.CSSProperties = focus ? {} : {background: DefaultRendererColors.InactiveCursor, animation: 'none'};
5050

src/json-crdt-peritext-ui/plugins/minimal/RenderInline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// biome-ignore lint: React is used for JSX
22
import * as React from 'react';
33
import {usePeritext} from '../../react';
4-
import {useSyncStore} from '../../react/hooks';
4+
import {useSyncStoreOpt} from '../../react/hooks';
55
import {DefaultRendererColors} from './constants';
66
import type {InlineViewProps} from '../../react/InlineView';
77
import {CommonSliceType} from '../../../json-crdt-extensions';
@@ -13,7 +13,7 @@ interface RenderInlineSelectionProps extends RenderInlineProps {
1313
const RenderInlineSelection: React.FC<RenderInlineSelectionProps> = (props) => {
1414
const {children, selection} = props;
1515
const {dom} = usePeritext();
16-
const focus = useSyncStore(dom.cursor.focus);
16+
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
1717

1818
const [left, right] = selection;
1919
const style: React.CSSProperties = {

0 commit comments

Comments
 (0)