@dxos/react-ui-editor 0.3.3-main.052ad73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +21 -0
- package/dist/lib/browser/index.mjs +883 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/types/src/components/Composer/Composer.d.ts +15 -0
- package/dist/types/src/components/Composer/Composer.d.ts.map +1 -0
- package/dist/types/src/components/Composer/Composer.stories.d.ts +28 -0
- package/dist/types/src/components/Composer/Composer.stories.d.ts.map +1 -0
- package/dist/types/src/components/Composer/index.d.ts +2 -0
- package/dist/types/src/components/Composer/index.d.ts.map +1 -0
- package/dist/types/src/components/Markdown/Markdown.d.ts +19 -0
- package/dist/types/src/components/Markdown/Markdown.d.ts.map +1 -0
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts +19 -0
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -0
- package/dist/types/src/components/Markdown/index.d.ts +2 -0
- package/dist/types/src/components/Markdown/index.d.ts.map +1 -0
- package/dist/types/src/components/Markdown/markdownTags.d.ts +17 -0
- package/dist/types/src/components/Markdown/markdownTags.d.ts.map +1 -0
- package/dist/types/src/components/Markdown/markdownTheme.d.ts +93 -0
- package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -0
- package/dist/types/src/components/RichText/RichText.d.ts +20 -0
- package/dist/types/src/components/RichText/RichText.d.ts.map +1 -0
- package/dist/types/src/components/RichText/RichText.stories.d.ts +26 -0
- package/dist/types/src/components/RichText/RichText.stories.d.ts.map +1 -0
- package/dist/types/src/components/RichText/index.d.ts +2 -0
- package/dist/types/src/components/RichText/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/model.d.ts +37 -0
- package/dist/types/src/model.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +3 -0
- package/dist/types/src/styles/index.d.ts.map +1 -0
- package/dist/types/src/styles/markdown.d.ts +17 -0
- package/dist/types/src/styles/markdown.d.ts.map +1 -0
- package/dist/types/src/styles/tokens.d.ts +3 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +3 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/proto/gen/schema.d.ts +15 -0
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +1 -0
- package/dist/types/src/testing/proto/index.d.ts +2 -0
- package/dist/types/src/testing/proto/index.d.ts.map +1 -0
- package/dist/types/src/testing/replicator.d.ts +45 -0
- package/dist/types/src/testing/replicator.d.ts.map +1 -0
- package/dist/types/src/yjs/cursors.d.ts +5 -0
- package/dist/types/src/yjs/cursors.d.ts.map +1 -0
- package/dist/types/src/yjs/index.d.ts +3 -0
- package/dist/types/src/yjs/index.d.ts.map +1 -0
- package/dist/types/src/yjs/space-provider.d.ts +27 -0
- package/dist/types/src/yjs/space-provider.d.ts.map +1 -0
- package/dist/types/stories/html/code-mirror.stories.d.ts +12 -0
- package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -0
- package/package.json +82 -0
- package/src/components/Composer/Composer.stories.tsx +80 -0
- package/src/components/Composer/Composer.tsx +33 -0
- package/src/components/Composer/index.ts +5 -0
- package/src/components/Markdown/Markdown.stories.tsx +56 -0
- package/src/components/Markdown/Markdown.tsx +240 -0
- package/src/components/Markdown/index.ts +5 -0
- package/src/components/Markdown/markdownTags.ts +38 -0
- package/src/components/Markdown/markdownTheme.ts +236 -0
- package/src/components/RichText/RichText.stories.tsx +68 -0
- package/src/components/RichText/RichText.tsx +181 -0
- package/src/components/RichText/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/index.ts +9 -0
- package/src/model.ts +72 -0
- package/src/styles/index.ts +6 -0
- package/src/styles/markdown.ts +46 -0
- package/src/styles/tokens.ts +8 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/proto/gen/schema.ts +47 -0
- package/src/testing/proto/index.ts +5 -0
- package/src/testing/proto/schema.proto +15 -0
- package/src/testing/replicator.ts +191 -0
- package/src/yjs/cursors.ts +21 -0
- package/src/yjs/index.ts +6 -0
- package/src/yjs/space-provider.ts +126 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { styleTags, Tag } from '@lezer/highlight';
|
|
6
|
+
import { type MarkdownConfig } from '@lezer/markdown';
|
|
7
|
+
|
|
8
|
+
export const markdownTags = {
|
|
9
|
+
headingMark: Tag.define(),
|
|
10
|
+
quoteMark: Tag.define(),
|
|
11
|
+
listMark: Tag.define(),
|
|
12
|
+
linkMark: Tag.define(),
|
|
13
|
+
emphasisMark: Tag.define(),
|
|
14
|
+
codeMark: Tag.define(),
|
|
15
|
+
codeText: Tag.define(),
|
|
16
|
+
inlineCode: Tag.define(),
|
|
17
|
+
url: Tag.define(),
|
|
18
|
+
linkReference: Tag.define(),
|
|
19
|
+
linkLabel: Tag.define(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const markdownTagsExtension: MarkdownConfig = {
|
|
23
|
+
props: [
|
|
24
|
+
styleTags({
|
|
25
|
+
HeaderMark: markdownTags.headingMark,
|
|
26
|
+
QuoteMark: markdownTags.quoteMark,
|
|
27
|
+
ListMark: markdownTags.listMark,
|
|
28
|
+
LinkMark: markdownTags.linkMark,
|
|
29
|
+
EmphasisMark: markdownTags.emphasisMark,
|
|
30
|
+
CodeMark: markdownTags.codeMark,
|
|
31
|
+
CodeText: markdownTags.codeText,
|
|
32
|
+
InlineCode: markdownTags.inlineCode,
|
|
33
|
+
URL: markdownTags.url,
|
|
34
|
+
LinkReference: markdownTags.linkReference,
|
|
35
|
+
LinkLabel: markdownTags.linkLabel,
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { HighlightStyle } from '@codemirror/language';
|
|
7
|
+
import { tags } from '@lezer/highlight';
|
|
8
|
+
import get from 'lodash.get';
|
|
9
|
+
|
|
10
|
+
import { markdownTags } from './markdownTags';
|
|
11
|
+
import { bold, heading, italic, mark, strikethrough, tokens } from '../../styles';
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Use theme colors.
|
|
14
|
+
// TODO(burdon): Light mode.
|
|
15
|
+
|
|
16
|
+
export const chalky = '#e5c07b';
|
|
17
|
+
export const coral = '#e06c75';
|
|
18
|
+
export const cyan = '#56b6c2';
|
|
19
|
+
export const invalid = '#ffffff';
|
|
20
|
+
export const ivory = '#abb2bf';
|
|
21
|
+
export const stone = '#7d8799';
|
|
22
|
+
export const malibu = '#61afef';
|
|
23
|
+
export const sage = '#98c379';
|
|
24
|
+
export const whiskey = '#d19a66';
|
|
25
|
+
export const violet = '#c678dd';
|
|
26
|
+
const _darkBackground = '#21252b';
|
|
27
|
+
export const highlightBackground = '#2c313a';
|
|
28
|
+
const _background = '#282c34';
|
|
29
|
+
export const tooltipBackground = '#353a42';
|
|
30
|
+
const _selection = '#3E4451';
|
|
31
|
+
export const cursor = '#ffffff';
|
|
32
|
+
|
|
33
|
+
const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
|
|
34
|
+
|
|
35
|
+
export const markdownTheme = {
|
|
36
|
+
// TODO(thure): consider whether these commented-out rules from one-dark-theme should be integrated.
|
|
37
|
+
// '&': {
|
|
38
|
+
// color: ivory,
|
|
39
|
+
// backgroundColor: background
|
|
40
|
+
// },
|
|
41
|
+
// '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
|
42
|
+
// '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
|
43
|
+
// backgroundColor: selection
|
|
44
|
+
// },
|
|
45
|
+
// '.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
|
46
|
+
// '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
|
47
|
+
// '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
|
48
|
+
// '.cm-searchMatch': {
|
|
49
|
+
// backgroundColor: '#72a1ff59',
|
|
50
|
+
// outline: '1px solid #457dff'
|
|
51
|
+
// },
|
|
52
|
+
// '.cm-searchMatch.cm-searchMatch-selected': {
|
|
53
|
+
// backgroundColor: '#6199ff2f'
|
|
54
|
+
// },
|
|
55
|
+
// '.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
|
56
|
+
// '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
|
57
|
+
// '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
|
58
|
+
// backgroundColor: '#bad0f847'
|
|
59
|
+
// },
|
|
60
|
+
// '.cm-gutters': {
|
|
61
|
+
// backgroundColor: background,
|
|
62
|
+
// color: stone,
|
|
63
|
+
// border: 'none'
|
|
64
|
+
// },
|
|
65
|
+
// '.cm-activeLineGutter': {
|
|
66
|
+
// backgroundColor: highlightBackground
|
|
67
|
+
// },
|
|
68
|
+
// '.cm-foldPlaceholder': {
|
|
69
|
+
// backgroundColor: 'transparent',
|
|
70
|
+
// border: 'none',
|
|
71
|
+
// color: '#ddd'
|
|
72
|
+
// },
|
|
73
|
+
|
|
74
|
+
'.dark & .cm-tooltip': {
|
|
75
|
+
border: 'none',
|
|
76
|
+
backgroundColor: tooltipBackground,
|
|
77
|
+
},
|
|
78
|
+
'.dark & .cm-tooltip .cm-tooltip-arrow:before': {
|
|
79
|
+
borderTopColor: 'transparent',
|
|
80
|
+
borderBottomColor: 'transparent',
|
|
81
|
+
},
|
|
82
|
+
'.dark & .cm-tooltip .cm-tooltip-arrow:after': {
|
|
83
|
+
borderTopColor: tooltipBackground,
|
|
84
|
+
borderBottomColor: tooltipBackground,
|
|
85
|
+
},
|
|
86
|
+
'.dark & .cm-tooltip-autocomplete': {
|
|
87
|
+
'& > ul > li[aria-selected]': {
|
|
88
|
+
backgroundColor: highlightBackground,
|
|
89
|
+
color: ivory,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
'&.cm-focused': {
|
|
93
|
+
outline: 'none',
|
|
94
|
+
},
|
|
95
|
+
'& .cm-line': {
|
|
96
|
+
paddingInline: 0,
|
|
97
|
+
},
|
|
98
|
+
'& .cm-line *': {
|
|
99
|
+
lineHeight: 1.6,
|
|
100
|
+
},
|
|
101
|
+
'& .cm-selectionBackground': {
|
|
102
|
+
background: get(tokens, 'extend.colors.primary.150', '#00ffff') + 'aa',
|
|
103
|
+
},
|
|
104
|
+
'.dark & .cm-selectionBackground': {
|
|
105
|
+
background: get(tokens, 'extend.colors.primary.500', '#00ffff') + 'aa',
|
|
106
|
+
},
|
|
107
|
+
'& .cm-selectionMatch': {
|
|
108
|
+
background: get(tokens, 'extend.colors.primary.100', '#00ffff') + '44',
|
|
109
|
+
},
|
|
110
|
+
'.dark & .cm-selectionMatch': {
|
|
111
|
+
background: get(tokens, 'extend.colors.primary.400', '#00ffff') + '44',
|
|
112
|
+
},
|
|
113
|
+
'& .cm-content': {
|
|
114
|
+
caretColor: 'black',
|
|
115
|
+
},
|
|
116
|
+
'.dark & .cm-content': {
|
|
117
|
+
caretColor: cursor,
|
|
118
|
+
},
|
|
119
|
+
'& .cm-cursor': {
|
|
120
|
+
borderLeftColor: 'black',
|
|
121
|
+
},
|
|
122
|
+
'.dark & .cm-cursor': {
|
|
123
|
+
borderLeftColor: cursor,
|
|
124
|
+
},
|
|
125
|
+
'.cm-placeholder': {
|
|
126
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
127
|
+
},
|
|
128
|
+
'& .cm-scroller': {
|
|
129
|
+
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
130
|
+
},
|
|
131
|
+
'& .cm-activeLine': {
|
|
132
|
+
backgroundColor: 'transparent',
|
|
133
|
+
},
|
|
134
|
+
'.dark & .cm-activeLine': {
|
|
135
|
+
backgroundColor: 'transparent',
|
|
136
|
+
},
|
|
137
|
+
'& .cm-ySelectionInfo': {
|
|
138
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
139
|
+
padding: '2px 4px',
|
|
140
|
+
marginBlockStart: '-4px',
|
|
141
|
+
},
|
|
142
|
+
'& .cm-ySelection': {
|
|
143
|
+
display: 'inline-block',
|
|
144
|
+
},
|
|
145
|
+
'& .cm-ySelectionCaret': {
|
|
146
|
+
display: 'inline-block',
|
|
147
|
+
verticalAlign: 'top',
|
|
148
|
+
},
|
|
149
|
+
...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
150
|
+
const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
|
|
151
|
+
// TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
|
|
152
|
+
acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
|
|
153
|
+
acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
|
|
154
|
+
acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
|
|
155
|
+
return acc;
|
|
156
|
+
}, {}),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const markdownDarkHighlighting = HighlightStyle.define(
|
|
160
|
+
[
|
|
161
|
+
{
|
|
162
|
+
tag: [
|
|
163
|
+
tags.keyword,
|
|
164
|
+
tags.name,
|
|
165
|
+
tags.deleted,
|
|
166
|
+
tags.character,
|
|
167
|
+
tags.propertyName,
|
|
168
|
+
tags.macroName,
|
|
169
|
+
tags.color,
|
|
170
|
+
tags.constant(tags.name),
|
|
171
|
+
tags.standard(tags.name),
|
|
172
|
+
tags.definition(tags.name),
|
|
173
|
+
tags.separator,
|
|
174
|
+
tags.typeName,
|
|
175
|
+
tags.className,
|
|
176
|
+
tags.number,
|
|
177
|
+
tags.changed,
|
|
178
|
+
tags.annotation,
|
|
179
|
+
tags.modifier,
|
|
180
|
+
tags.self,
|
|
181
|
+
tags.namespace,
|
|
182
|
+
tags.operator,
|
|
183
|
+
tags.operatorKeyword,
|
|
184
|
+
tags.escape,
|
|
185
|
+
tags.regexp,
|
|
186
|
+
tags.special(tags.string),
|
|
187
|
+
tags.meta,
|
|
188
|
+
tags.comment,
|
|
189
|
+
tags.atom,
|
|
190
|
+
tags.bool,
|
|
191
|
+
tags.special(tags.variableName),
|
|
192
|
+
tags.processingInstruction,
|
|
193
|
+
tags.string,
|
|
194
|
+
tags.inserted,
|
|
195
|
+
tags.invalid,
|
|
196
|
+
],
|
|
197
|
+
color: 'inherit !important',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
tag: [tags.link, tags.url],
|
|
201
|
+
color: 'inherit !important',
|
|
202
|
+
textDecoration: 'none !important',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
tag: [tags.function(tags.variableName), tags.labelName],
|
|
206
|
+
color: malibu,
|
|
207
|
+
fontFamily: monospace,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
tag: [
|
|
211
|
+
markdownTags.codeMark,
|
|
212
|
+
markdownTags.emphasisMark,
|
|
213
|
+
markdownTags.headingMark,
|
|
214
|
+
markdownTags.linkLabel,
|
|
215
|
+
markdownTags.linkReference,
|
|
216
|
+
markdownTags.listMark,
|
|
217
|
+
markdownTags.quoteMark,
|
|
218
|
+
markdownTags.url,
|
|
219
|
+
tags.meta,
|
|
220
|
+
tags.processingInstruction,
|
|
221
|
+
],
|
|
222
|
+
class: mark,
|
|
223
|
+
},
|
|
224
|
+
{ tag: [markdownTags.codeText, markdownTags.inlineCode], class: 'font-mono' },
|
|
225
|
+
{ tag: tags.emphasis, class: italic },
|
|
226
|
+
{ tag: tags.heading1, class: heading[1] },
|
|
227
|
+
{ tag: tags.heading2, class: heading[2] },
|
|
228
|
+
{ tag: tags.heading3, class: heading[3] },
|
|
229
|
+
{ tag: tags.heading4, class: heading[4] },
|
|
230
|
+
{ tag: tags.heading5, class: heading[5] },
|
|
231
|
+
{ tag: tags.heading6, class: heading[6] },
|
|
232
|
+
{ tag: tags.strikethrough, class: strikethrough },
|
|
233
|
+
{ tag: tags.strong, class: bold },
|
|
234
|
+
],
|
|
235
|
+
{ scope: markdownLanguage, all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') } },
|
|
236
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { PublicKey } from '@dxos/react-client';
|
|
10
|
+
import { TextKind } from '@dxos/react-client/echo';
|
|
11
|
+
import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
|
|
12
|
+
import { useId } from '@dxos/react-ui';
|
|
13
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
14
|
+
|
|
15
|
+
import { RichTextComposer, type RichTextComposerProps } from './RichText';
|
|
16
|
+
import { Replicator, useYjsModel } from '../../testing';
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
component: RichTextComposer,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Default = {
|
|
23
|
+
args: {
|
|
24
|
+
model: {
|
|
25
|
+
id: 'editor',
|
|
26
|
+
content: 'Hello, Storybook!',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const replicator = new Replicator(TextKind.RICH);
|
|
32
|
+
export const WithYjs = {
|
|
33
|
+
render: (args: Omit<RichTextComposerProps, 'model'>) => {
|
|
34
|
+
const [generate, setGenerate] = useState(false);
|
|
35
|
+
const generateId = useId('generate');
|
|
36
|
+
|
|
37
|
+
const [id] = useState(PublicKey.random().toHex());
|
|
38
|
+
const model = useYjsModel({ id, replicator });
|
|
39
|
+
|
|
40
|
+
useDataGenerator({
|
|
41
|
+
generator: generate ? textGenerator : undefined,
|
|
42
|
+
options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<main className='flex-1 min-w-0 p-4'>
|
|
47
|
+
<div id={generateId} className='flex'>
|
|
48
|
+
<input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
|
|
49
|
+
Generate Data
|
|
50
|
+
</div>
|
|
51
|
+
<RichTextComposer
|
|
52
|
+
{...args}
|
|
53
|
+
model={model}
|
|
54
|
+
slots={{
|
|
55
|
+
editor: {
|
|
56
|
+
className: mx(
|
|
57
|
+
'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
|
|
58
|
+
args.slots?.editor?.className,
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</main>
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
// TODO(wittjosiah): Decorator for doing this without clients being initialized?
|
|
67
|
+
decorators: [ClientDecorator({ count: 2 })],
|
|
68
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { mergeAttributes } from '@tiptap/core';
|
|
5
|
+
import Collaboration from '@tiptap/extension-collaboration';
|
|
6
|
+
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
|
|
7
|
+
import Heading from '@tiptap/extension-heading';
|
|
8
|
+
import ListItem from '@tiptap/extension-list-item';
|
|
9
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
10
|
+
import { type Editor, EditorContent, useEditor as useNaturalEditor } from '@tiptap/react';
|
|
11
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
12
|
+
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
import { generateName } from '@dxos/display-name';
|
|
15
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
16
|
+
|
|
17
|
+
import { type ComposerModel, type ComposerSlots } from '../../model';
|
|
18
|
+
import {
|
|
19
|
+
blockquote,
|
|
20
|
+
bold,
|
|
21
|
+
codeBlock,
|
|
22
|
+
heading,
|
|
23
|
+
type HeadingLevel,
|
|
24
|
+
horizontalRule,
|
|
25
|
+
italic,
|
|
26
|
+
listItem,
|
|
27
|
+
orderedList,
|
|
28
|
+
paragraph,
|
|
29
|
+
strikethrough,
|
|
30
|
+
unorderedList,
|
|
31
|
+
codeWithoutMarks,
|
|
32
|
+
} from '../../styles';
|
|
33
|
+
import { cursorColor } from '../../yjs';
|
|
34
|
+
|
|
35
|
+
export type TipTapEditor = Editor;
|
|
36
|
+
|
|
37
|
+
type UseEditorOptions = {
|
|
38
|
+
model?: ComposerModel;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
slots?: Pick<ComposerSlots, 'editor'>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const useEditor = ({ model, placeholder = 'Enter text…', slots = {} }: UseEditorOptions) => {
|
|
44
|
+
const extensions = useMemo(
|
|
45
|
+
() => [
|
|
46
|
+
StarterKit.configure({
|
|
47
|
+
// Extensions
|
|
48
|
+
history: false,
|
|
49
|
+
// Nodes
|
|
50
|
+
blockquote: {
|
|
51
|
+
HTMLAttributes: {
|
|
52
|
+
class: blockquote,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
bulletList: {
|
|
56
|
+
HTMLAttributes: {
|
|
57
|
+
class: unorderedList,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
codeBlock: {
|
|
61
|
+
HTMLAttributes: {
|
|
62
|
+
class: codeBlock,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
heading: false, // (thure): `StarterKit` doesn’t let you configure how headings are rendered, see `Heading` below.
|
|
66
|
+
horizontalRule: {
|
|
67
|
+
HTMLAttributes: {
|
|
68
|
+
class: horizontalRule,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
listItem: false, // (thure): `StarterKit` doesn’t let you configure how list items are rendered, see `ListItem` below.
|
|
72
|
+
orderedList: {
|
|
73
|
+
HTMLAttributes: {
|
|
74
|
+
class: orderedList,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
paragraph: {
|
|
78
|
+
HTMLAttributes: {
|
|
79
|
+
class: paragraph,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
// Marks
|
|
83
|
+
bold: {
|
|
84
|
+
HTMLAttributes: {
|
|
85
|
+
class: bold,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
code: {
|
|
89
|
+
HTMLAttributes: {
|
|
90
|
+
class: codeWithoutMarks,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
italic: {
|
|
94
|
+
HTMLAttributes: {
|
|
95
|
+
class: italic,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
strike: {
|
|
99
|
+
HTMLAttributes: {
|
|
100
|
+
class: strikethrough,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
Heading.extend({
|
|
105
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
106
|
+
const hasLevel = this.options.levels.includes(node.attrs.level);
|
|
107
|
+
const level: HeadingLevel = hasLevel ? node.attrs.level : this.options.levels[0];
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
`h${level}`,
|
|
111
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
112
|
+
class: heading[level],
|
|
113
|
+
}),
|
|
114
|
+
0,
|
|
115
|
+
];
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
ListItem.extend({
|
|
119
|
+
renderHTML: ({ HTMLAttributes }) => [
|
|
120
|
+
'li',
|
|
121
|
+
mergeAttributes(HTMLAttributes, {
|
|
122
|
+
marker: '• ',
|
|
123
|
+
class: listItem,
|
|
124
|
+
}),
|
|
125
|
+
['div', { role: 'none' }, 0],
|
|
126
|
+
],
|
|
127
|
+
}),
|
|
128
|
+
// https://github.com/ueberdosis/tiptap/tree/main/packages/extension-collaboration
|
|
129
|
+
...(model && typeof model.content !== 'string' ? [Collaboration.configure({ fragment: model.content })] : []),
|
|
130
|
+
...(model?.provider
|
|
131
|
+
? [
|
|
132
|
+
CollaborationCursor.configure({
|
|
133
|
+
provider: model.provider,
|
|
134
|
+
user: model.peer && {
|
|
135
|
+
name: model.peer.name ?? generateName(model.peer.id),
|
|
136
|
+
color: cursorColor.color,
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
]
|
|
140
|
+
: []),
|
|
141
|
+
Placeholder.configure({
|
|
142
|
+
placeholder,
|
|
143
|
+
emptyEditorClass: 'before:content-[attr(data-placeholder)] before:absolute opacity-50 cursor-text',
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
[model?.id],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return useNaturalEditor(
|
|
150
|
+
{
|
|
151
|
+
extensions,
|
|
152
|
+
editorProps: {
|
|
153
|
+
attributes: {
|
|
154
|
+
class: mx('focus:outline-none focus-visible:outline-none', slots.editor?.className),
|
|
155
|
+
spellcheck: slots.editor?.spellCheck === false ? 'false' : 'true',
|
|
156
|
+
tabindex: slots.editor?.tabIndex ? String(slots.editor?.tabIndex) : '0',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
[extensions],
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type RichTextComposerProps = UseEditorOptions & {
|
|
165
|
+
slots?: ComposerSlots;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @deprecated
|
|
170
|
+
*/
|
|
171
|
+
// TODO(burdon): Currently broken.
|
|
172
|
+
export const RichTextComposer = forwardRef<Editor | null, RichTextComposerProps>((props, ref) => {
|
|
173
|
+
const editor = useEditor(props);
|
|
174
|
+
useImperativeHandle<Editor | null, Editor | null>(ref, () => editor, [editor]);
|
|
175
|
+
|
|
176
|
+
// Reference:
|
|
177
|
+
// https://tiptap.dev/installation/react
|
|
178
|
+
// https://github.com/ueberdosis/tiptap
|
|
179
|
+
// https://tiptap.dev/guide/output/#option-3-yjs
|
|
180
|
+
return <EditorContent {...props.slots?.root} editor={editor} />;
|
|
181
|
+
});
|
package/src/index.ts
ADDED
package/src/model.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
import { type ComponentProps, useMemo } from 'react';
|
|
7
|
+
import type * as awarenessProtocol from 'y-protocols/awareness';
|
|
8
|
+
|
|
9
|
+
import { type Space, type Text } from '@dxos/react-client/echo';
|
|
10
|
+
import { type Identity } from '@dxos/react-client/halo';
|
|
11
|
+
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
12
|
+
|
|
13
|
+
import { SpaceAwarenessProvider } from './yjs';
|
|
14
|
+
|
|
15
|
+
export type ComposerSlots = {
|
|
16
|
+
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
17
|
+
editor?: {
|
|
18
|
+
className?: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
spellCheck?: boolean;
|
|
21
|
+
tabIndex?: number;
|
|
22
|
+
markdownTheme?: Parameters<typeof EditorView.theme>[0];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Awareness = awarenessProtocol.Awareness;
|
|
27
|
+
type Provider = { awareness: Awareness };
|
|
28
|
+
|
|
29
|
+
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
30
|
+
export type ComposerModel = {
|
|
31
|
+
id: string;
|
|
32
|
+
content: string | YText | YXmlFragment;
|
|
33
|
+
provider?: Provider;
|
|
34
|
+
peer?: {
|
|
35
|
+
id: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UseTextModelOptions = {
|
|
41
|
+
identity?: Identity | null;
|
|
42
|
+
space?: Space;
|
|
43
|
+
text?: Text;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
47
|
+
// TODO(burdon): Decouple space (make Composer less dependent on entire stack)?
|
|
48
|
+
export const useTextModel = ({ identity, space, text }: UseTextModelOptions): ComposerModel | undefined => {
|
|
49
|
+
const provider = useMemo(() => {
|
|
50
|
+
if (!space || !text?.doc) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` });
|
|
55
|
+
}, [identity, space, text?.doc]);
|
|
56
|
+
|
|
57
|
+
if (!text?.doc || !text?.content) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id: text.doc.guid,
|
|
63
|
+
content: text.content,
|
|
64
|
+
provider,
|
|
65
|
+
peer: identity
|
|
66
|
+
? {
|
|
67
|
+
id: identity.identityKey.toHex(),
|
|
68
|
+
name: identity.profile?.displayName,
|
|
69
|
+
}
|
|
70
|
+
: undefined,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
6
|
+
|
|
7
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
|
|
9
|
+
export const heading: Record<HeadingLevel, string> = {
|
|
10
|
+
1: 'mbs-4 mbe-2 text-4xl font-semibold text-inherit no-underline',
|
|
11
|
+
2: 'mbs-4 mbe-2 text-3xl font-bold text-inherit no-underline',
|
|
12
|
+
3: 'mbs-4 mbe-2 text-2xl font-bold text-inherit no-underline',
|
|
13
|
+
4: 'mbs-4 mbe-2 text-xl font-extrabold text-inherit no-underline',
|
|
14
|
+
5: 'mbs-4 mbe-2 text-lg font-extrabold text-inherit no-underline',
|
|
15
|
+
6: 'mbs-4 mbe-2 font-black text-inherit no-underline',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const blockquote = 'mlb-2 border-is-4 border-neutral-500/50 pis-5';
|
|
19
|
+
|
|
20
|
+
// TODO(thure): Tailwind was not seeing `[&>li:before]:content-["•"]` as a utility class, but it would work if instead of `"•"` it was `"X"`… why?
|
|
21
|
+
export const unorderedList =
|
|
22
|
+
'mlb-2 grid grid-cols-[min-content_1fr] [&>li:before]:content-[attr(marker)] [&>li:before]:mlb-1 [&>li:before]:mie-2';
|
|
23
|
+
export const orderedList =
|
|
24
|
+
'mlb-2 grid grid-cols-[min-content_1fr] [&>li:before]:content-[counters(section,_".")_"._"] [counter-reset:section] [&>li:before]:mlb-1';
|
|
25
|
+
|
|
26
|
+
export const listItem = 'contents before:[counter-increment:section]';
|
|
27
|
+
|
|
28
|
+
export const codeBlock = 'mlb-2 font-mono bg-neutral-500/10 p-3 rounded';
|
|
29
|
+
|
|
30
|
+
export const horizontalRule = 'mlb-4 border-neutral-500/50';
|
|
31
|
+
|
|
32
|
+
export const paragraph = 'mlb-1';
|
|
33
|
+
|
|
34
|
+
export const bold = 'font-bold';
|
|
35
|
+
|
|
36
|
+
export const code = 'font-mono bg-neutral-500/10 rounded pli-0.5 plb-0.5 -mlb-0.5';
|
|
37
|
+
|
|
38
|
+
export const placeholder = 'font-mono';
|
|
39
|
+
|
|
40
|
+
export const codeWithoutMarks = mx(code, 'pli-1.5 mli-0.5');
|
|
41
|
+
|
|
42
|
+
export const italic = 'italic';
|
|
43
|
+
|
|
44
|
+
export const strikethrough = 'line-through';
|
|
45
|
+
|
|
46
|
+
export const mark = '!font-normal !no-underline !text-inherit opacity-40';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { tailwindConfig, type TailwindConfig } from '@dxos/react-ui-theme';
|
|
6
|
+
|
|
7
|
+
// TODO(thure): Why export the whole theme? Can this be done differently?
|
|
8
|
+
export const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
|