@dxos/react-ui-editor 0.3.3-main.1b7ca63

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.
Files changed (81) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +21 -0
  3. package/dist/lib/browser/index.mjs +883 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/types/src/components/Composer/Composer.d.ts +15 -0
  7. package/dist/types/src/components/Composer/Composer.d.ts.map +1 -0
  8. package/dist/types/src/components/Composer/Composer.stories.d.ts +28 -0
  9. package/dist/types/src/components/Composer/Composer.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Composer/index.d.ts +2 -0
  11. package/dist/types/src/components/Composer/index.d.ts.map +1 -0
  12. package/dist/types/src/components/Markdown/Markdown.d.ts +19 -0
  13. package/dist/types/src/components/Markdown/Markdown.d.ts.map +1 -0
  14. package/dist/types/src/components/Markdown/Markdown.stories.d.ts +19 -0
  15. package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/Markdown/index.d.ts +2 -0
  17. package/dist/types/src/components/Markdown/index.d.ts.map +1 -0
  18. package/dist/types/src/components/Markdown/markdownTags.d.ts +17 -0
  19. package/dist/types/src/components/Markdown/markdownTags.d.ts.map +1 -0
  20. package/dist/types/src/components/Markdown/markdownTheme.d.ts +93 -0
  21. package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -0
  22. package/dist/types/src/components/RichText/RichText.d.ts +20 -0
  23. package/dist/types/src/components/RichText/RichText.d.ts.map +1 -0
  24. package/dist/types/src/components/RichText/RichText.stories.d.ts +26 -0
  25. package/dist/types/src/components/RichText/RichText.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/RichText/index.d.ts +2 -0
  27. package/dist/types/src/components/RichText/index.d.ts.map +1 -0
  28. package/dist/types/src/components/index.d.ts +4 -0
  29. package/dist/types/src/components/index.d.ts.map +1 -0
  30. package/dist/types/src/index.d.ts +5 -0
  31. package/dist/types/src/index.d.ts.map +1 -0
  32. package/dist/types/src/model.d.ts +37 -0
  33. package/dist/types/src/model.d.ts.map +1 -0
  34. package/dist/types/src/styles/index.d.ts +3 -0
  35. package/dist/types/src/styles/index.d.ts.map +1 -0
  36. package/dist/types/src/styles/markdown.d.ts +17 -0
  37. package/dist/types/src/styles/markdown.d.ts.map +1 -0
  38. package/dist/types/src/styles/tokens.d.ts +3 -0
  39. package/dist/types/src/styles/tokens.d.ts.map +1 -0
  40. package/dist/types/src/testing/index.d.ts +3 -0
  41. package/dist/types/src/testing/index.d.ts.map +1 -0
  42. package/dist/types/src/testing/proto/gen/schema.d.ts +15 -0
  43. package/dist/types/src/testing/proto/gen/schema.d.ts.map +1 -0
  44. package/dist/types/src/testing/proto/index.d.ts +2 -0
  45. package/dist/types/src/testing/proto/index.d.ts.map +1 -0
  46. package/dist/types/src/testing/replicator.d.ts +45 -0
  47. package/dist/types/src/testing/replicator.d.ts.map +1 -0
  48. package/dist/types/src/yjs/cursors.d.ts +5 -0
  49. package/dist/types/src/yjs/cursors.d.ts.map +1 -0
  50. package/dist/types/src/yjs/index.d.ts +3 -0
  51. package/dist/types/src/yjs/index.d.ts.map +1 -0
  52. package/dist/types/src/yjs/space-provider.d.ts +27 -0
  53. package/dist/types/src/yjs/space-provider.d.ts.map +1 -0
  54. package/dist/types/stories/html/code-mirror.stories.d.ts +12 -0
  55. package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -0
  56. package/package.json +82 -0
  57. package/src/components/Composer/Composer.stories.tsx +80 -0
  58. package/src/components/Composer/Composer.tsx +33 -0
  59. package/src/components/Composer/index.ts +5 -0
  60. package/src/components/Markdown/Markdown.stories.tsx +56 -0
  61. package/src/components/Markdown/Markdown.tsx +240 -0
  62. package/src/components/Markdown/index.ts +5 -0
  63. package/src/components/Markdown/markdownTags.ts +38 -0
  64. package/src/components/Markdown/markdownTheme.ts +236 -0
  65. package/src/components/RichText/RichText.stories.tsx +68 -0
  66. package/src/components/RichText/RichText.tsx +181 -0
  67. package/src/components/RichText/index.ts +5 -0
  68. package/src/components/index.ts +7 -0
  69. package/src/index.ts +9 -0
  70. package/src/model.ts +72 -0
  71. package/src/styles/index.ts +6 -0
  72. package/src/styles/markdown.ts +46 -0
  73. package/src/styles/tokens.ts +8 -0
  74. package/src/testing/index.ts +6 -0
  75. package/src/testing/proto/gen/schema.ts +47 -0
  76. package/src/testing/proto/index.ts +5 -0
  77. package/src/testing/proto/schema.proto +15 -0
  78. package/src/testing/replicator.ts +191 -0
  79. package/src/yjs/cursors.ts +21 -0
  80. package/src/yjs/index.ts +6 -0
  81. 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
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './RichText';
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './Composer';
6
+ export * from './Markdown';
7
+ export * from './RichText';
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './components';
6
+ export * from './model';
7
+
8
+ export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
9
+ export { Doc, YText, YXmlFragment } from '@dxos/text-model';
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,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './markdown';
6
+ export * from './tokens';
@@ -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;
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './proto';
6
+ export * from './replicator';