@dxos/react-ui-editor 0.6.11-staging.e6894a4 → 0.6.11
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/dist/lib/browser/index.mjs +126 -73
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/InputMode.stories.d.ts.map +1 -1
- package/dist/types/src/TextEditor.stories.d.ts +10 -2
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +0 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +5 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/util/react.d.ts +4 -0
- package/dist/types/src/extensions/util/react.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +2 -2
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/InputMode.stories.tsx +8 -10
- package/src/TextEditor.stories.tsx +58 -32
- package/src/defaults.ts +5 -3
- package/src/extensions/automerge/automerge.stories.tsx +5 -6
- package/src/extensions/factories.ts +15 -4
- package/src/extensions/folding.tsx +17 -4
- package/src/extensions/markdown/bundle.ts +1 -5
- package/src/extensions/markdown/decorate.ts +40 -23
- package/src/extensions/util/react.tsx +15 -0
- package/src/hooks/useTextEditor.ts +3 -5
- package/src/styles/markdown.ts +0 -2
- package/src/styles/theme.ts +12 -8
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@dxos/react-ui-editor",
|
3
|
-
"version": "0.6.11
|
3
|
+
"version": "0.6.11",
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
5
5
|
"homepage": "https://dxos.org",
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
@@ -47,19 +47,19 @@
|
|
47
47
|
"lodash.sortby": "^4.7.0",
|
48
48
|
"react-dropzone": "^14.2.3",
|
49
49
|
"style-mod": "^4.1.0",
|
50
|
-
"@dxos/async": "0.6.11
|
51
|
-
"@dxos/automerge": "0.6.11
|
52
|
-
"@dxos/
|
53
|
-
"@dxos/
|
54
|
-
"@dxos/display-name": "0.6.11
|
55
|
-
"@dxos/echo-schema": "0.6.11
|
56
|
-
"@dxos/
|
57
|
-
"@dxos/
|
58
|
-
"@dxos/
|
59
|
-
"@dxos/
|
60
|
-
"@dxos/
|
61
|
-
"@dxos/
|
62
|
-
"@dxos/
|
50
|
+
"@dxos/async": "0.6.11",
|
51
|
+
"@dxos/automerge": "0.6.11",
|
52
|
+
"@dxos/context": "0.6.11",
|
53
|
+
"@dxos/debug": "0.6.11",
|
54
|
+
"@dxos/display-name": "0.6.11",
|
55
|
+
"@dxos/echo-schema": "0.6.11",
|
56
|
+
"@dxos/log": "0.6.11",
|
57
|
+
"@dxos/react-ui": "0.6.11",
|
58
|
+
"@dxos/react-ui-theme": "0.6.11",
|
59
|
+
"@dxos/protocols": "0.6.11",
|
60
|
+
"@dxos/react-hooks": "0.6.11",
|
61
|
+
"@dxos/util": "0.6.11",
|
62
|
+
"@dxos/invariant": "0.6.11"
|
63
63
|
},
|
64
64
|
"devDependencies": {
|
65
65
|
"@phosphor-icons/react": "^2.1.5",
|
@@ -82,21 +82,21 @@
|
|
82
82
|
"vite": "^5.3.4",
|
83
83
|
"vite-plugin-top-level-await": "^1.4.1",
|
84
84
|
"vite-plugin-wasm": "^3.3.0",
|
85
|
-
"@dxos/
|
86
|
-
"@dxos/
|
87
|
-
"@dxos/echo-signals": "0.6.11
|
88
|
-
"@dxos/echo-typegen": "0.6.11
|
89
|
-
"@dxos/keyboard": "0.6.11
|
90
|
-
"@dxos/random": "0.6.11
|
91
|
-
"@dxos/
|
92
|
-
"@dxos/react-
|
93
|
-
"@dxos/
|
85
|
+
"@dxos/automerge": "0.6.11",
|
86
|
+
"@dxos/config": "0.6.11",
|
87
|
+
"@dxos/echo-signals": "0.6.11",
|
88
|
+
"@dxos/echo-typegen": "0.6.11",
|
89
|
+
"@dxos/keyboard": "0.6.11",
|
90
|
+
"@dxos/random": "0.6.11",
|
91
|
+
"@dxos/react-client": "0.6.11",
|
92
|
+
"@dxos/react-ui": "0.6.11",
|
93
|
+
"@dxos/storybook-utils": "0.6.11"
|
94
94
|
},
|
95
95
|
"peerDependencies": {
|
96
96
|
"@phosphor-icons/react": "^2.1.5",
|
97
97
|
"react": "^18.0.0",
|
98
98
|
"react-dom": "^18.0.0",
|
99
|
-
"@dxos/react-client": "0.6.11
|
99
|
+
"@dxos/react-client": "0.6.11"
|
100
100
|
},
|
101
101
|
"publishConfig": {
|
102
102
|
"access": "public"
|
@@ -6,9 +6,9 @@ import '@dxos-theme';
|
|
6
6
|
|
7
7
|
import React, { useState } from 'react';
|
8
8
|
|
9
|
-
import { Toolbar as NaturalToolbar, Select, useThemeContext
|
9
|
+
import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
11
|
-
import {
|
11
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
12
12
|
|
13
13
|
import { Toolbar } from './components';
|
14
14
|
import {
|
@@ -39,7 +39,7 @@ const Story = ({ autoFocus, initialValue, placeholder, readonly }: StoryProps) =
|
|
39
39
|
editorInputMode ? InputModeExtensions[editorInputMode] : [],
|
40
40
|
createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
|
41
41
|
createMarkdownExtensions({ themeMode }),
|
42
|
-
createThemeExtensions({ themeMode }),
|
42
|
+
createThemeExtensions({ themeMode, syntaxHighlighting: true }),
|
43
43
|
decorateMarkdown(),
|
44
44
|
formattingKeymap(),
|
45
45
|
trackFormatting,
|
@@ -54,12 +54,10 @@ const Story = ({ autoFocus, initialValue, placeholder, readonly }: StoryProps) =
|
|
54
54
|
// Also not sure if view is even guaranteed to exist at this point.
|
55
55
|
return (
|
56
56
|
<div role='none' className={mx('fixed inset-0 flex flex-col')}>
|
57
|
-
<
|
58
|
-
<Toolbar.
|
59
|
-
|
60
|
-
|
61
|
-
</Toolbar.Root>
|
62
|
-
</Tooltip.Provider>
|
57
|
+
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
58
|
+
<Toolbar.Markdown />
|
59
|
+
<EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
|
60
|
+
</Toolbar.Root>
|
63
61
|
|
64
62
|
<div role='none' className='grow overflow-hidden'>
|
65
63
|
<div className={attentionSurface} ref={parentRef} />
|
@@ -100,7 +98,7 @@ const EditorInputModeToolbar = ({
|
|
100
98
|
|
101
99
|
export default {
|
102
100
|
title: 'react-ui-editor/InputMode',
|
103
|
-
decorators: [withTheme,
|
101
|
+
decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
|
104
102
|
parameters: { translations, layout: 'fullscreen' },
|
105
103
|
render: Story,
|
106
104
|
};
|
@@ -3,7 +3,7 @@
|
|
3
3
|
//
|
4
4
|
|
5
5
|
import '@dxos-theme';
|
6
|
-
|
6
|
+
import { javascript } from '@codemirror/lang-javascript';
|
7
7
|
import { markdown } from '@codemirror/lang-markdown';
|
8
8
|
import { openSearchPanel } from '@codemirror/search';
|
9
9
|
import { type Extension } from '@codemirror/state';
|
@@ -22,9 +22,9 @@ import { faker } from '@dxos/random';
|
|
22
22
|
import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
|
23
23
|
import { Button, DensityProvider, Input, useThemeContext } from '@dxos/react-ui';
|
24
24
|
import { baseSurface, mx, getSize } from '@dxos/react-ui-theme';
|
25
|
-
import {
|
25
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
26
26
|
|
27
|
-
import { editorContent, editorGutter } from './defaults';
|
27
|
+
import { editorContent, editorGutter, editorMonospace } from './defaults';
|
28
28
|
import {
|
29
29
|
InputModeExtensions,
|
30
30
|
annotations,
|
@@ -68,6 +68,15 @@ const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
|
|
68
68
|
|
69
69
|
const img = '';
|
70
70
|
|
71
|
+
const code = str(
|
72
|
+
'// Code',
|
73
|
+
'const Component = () => {',
|
74
|
+
' const x = 100;',
|
75
|
+
'',
|
76
|
+
' return () => <div>Test</div>;',
|
77
|
+
'};',
|
78
|
+
);
|
79
|
+
|
71
80
|
const content = {
|
72
81
|
tasks: str(
|
73
82
|
//
|
@@ -111,22 +120,9 @@ const content = {
|
|
111
120
|
'',
|
112
121
|
),
|
113
122
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
'```bash',
|
118
|
-
'$ ls -las',
|
119
|
-
'```',
|
120
|
-
'',
|
121
|
-
'```tsx',
|
122
|
-
'const Component = () => {',
|
123
|
-
' const x = 100;',
|
124
|
-
'',
|
125
|
-
' return () => <div>Test</div>;',
|
126
|
-
'};',
|
127
|
-
'```',
|
128
|
-
'',
|
129
|
-
),
|
123
|
+
typescript: code,
|
124
|
+
|
125
|
+
codeblocks: str('### Code', '', '```bash', '$ ls -las', '```', '', '```tsx', code, '```', ''),
|
130
126
|
|
131
127
|
comment: str('<!--', 'A comment', '-->', '', 'No comment.', 'Partial comment. <!-- comment. -->'),
|
132
128
|
|
@@ -195,12 +191,9 @@ const text = str(
|
|
195
191
|
content.tasks,
|
196
192
|
content.numbered,
|
197
193
|
|
198
|
-
'---',
|
199
|
-
content.headings,
|
200
|
-
|
201
194
|
'---',
|
202
195
|
'## Misc',
|
203
|
-
content.
|
196
|
+
content.codeblocks,
|
204
197
|
content.table,
|
205
198
|
content.image,
|
206
199
|
content.footer,
|
@@ -261,12 +254,15 @@ const renderLinkButton = (el: Element, url: string) => {
|
|
261
254
|
// Story
|
262
255
|
//
|
263
256
|
|
257
|
+
type DebugMode = 'syntax' | 'raw';
|
258
|
+
|
264
259
|
type StoryProps = {
|
265
260
|
id?: string;
|
266
|
-
debug?:
|
261
|
+
debug?: DebugMode;
|
267
262
|
text?: string;
|
268
263
|
readonly?: boolean;
|
269
264
|
placeholder?: string;
|
265
|
+
lineNumbers?: boolean;
|
270
266
|
onReady?: (view: EditorView) => void;
|
271
267
|
} & Pick<UseTextEditorProps, 'scrollTo' | 'selection' | 'extensions'>;
|
272
268
|
|
@@ -279,6 +275,7 @@ const Story = ({
|
|
279
275
|
placeholder = 'New document.',
|
280
276
|
scrollTo,
|
281
277
|
selection,
|
278
|
+
lineNumbers,
|
282
279
|
onReady,
|
283
280
|
}: StoryProps) => {
|
284
281
|
const [object] = useState(createEchoObject(create(Expando, { content: text ?? '' })));
|
@@ -290,16 +287,18 @@ const Story = ({
|
|
290
287
|
initialValue: text,
|
291
288
|
extensions: [
|
292
289
|
createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
|
293
|
-
createBasicExtensions({ readonly, placeholder, scrollPastEnd: true }),
|
290
|
+
createBasicExtensions({ readonly, placeholder, lineNumbers, scrollPastEnd: true }),
|
294
291
|
createMarkdownExtensions({ themeMode }),
|
295
292
|
createThemeExtensions({
|
296
293
|
themeMode,
|
294
|
+
syntaxHighlighting: true,
|
297
295
|
slots: {
|
298
296
|
content: {
|
299
297
|
className: editorContent,
|
300
298
|
},
|
301
299
|
},
|
302
300
|
}),
|
301
|
+
editorGutter,
|
303
302
|
extensions || [],
|
304
303
|
debug ? debugTree(setTree) : [],
|
305
304
|
],
|
@@ -318,7 +317,12 @@ const Story = ({
|
|
318
317
|
return (
|
319
318
|
<div className='flex w-full'>
|
320
319
|
<div role='none' className='flex w-full overflow-hidden' ref={parentRef} {...focusAttributes} />
|
321
|
-
{debug && (
|
320
|
+
{debug === 'raw' && (
|
321
|
+
<div className='w-[800px] border-l border-separator overflow-auto'>
|
322
|
+
<pre className='p-1 font-mono text-xs text-green-800 dark:text-green-200'>{view?.state.doc.toString()}</pre>
|
323
|
+
</div>
|
324
|
+
)}
|
325
|
+
{debug === 'syntax' && (
|
322
326
|
<div className='w-[800px] border-l border-separator overflow-auto'>
|
323
327
|
<pre className='p-1 font-mono text-xs text-green-800 dark:text-green-200'>
|
324
328
|
{JSON.stringify(tree, null, 2)}
|
@@ -331,7 +335,7 @@ const Story = ({
|
|
331
335
|
|
332
336
|
export default {
|
333
337
|
title: 'react-ui-editor/TextEditor',
|
334
|
-
decorators: [withTheme,
|
338
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
335
339
|
render: Story,
|
336
340
|
parameters: { translations, layout: 'fullscreen' },
|
337
341
|
};
|
@@ -345,8 +349,24 @@ const defaultExtensions: Extension[] = [
|
|
345
349
|
linkTooltip(renderLinkTooltip),
|
346
350
|
];
|
347
351
|
|
352
|
+
const allExtensions: Extension[] = [
|
353
|
+
autocomplete({
|
354
|
+
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
355
|
+
}),
|
356
|
+
decorateMarkdown({ numberedHeadings: { from: 2, to: 4 }, renderLinkButton, selectionChangeDelay: 100 }),
|
357
|
+
formattingKeymap(),
|
358
|
+
linkTooltip(renderLinkTooltip),
|
359
|
+
image(),
|
360
|
+
table(),
|
361
|
+
folding(),
|
362
|
+
];
|
363
|
+
|
348
364
|
export const Default = {
|
349
|
-
render: () => <Story text={text} extensions={defaultExtensions}
|
365
|
+
render: () => <Story text={text} extensions={defaultExtensions} />,
|
366
|
+
};
|
367
|
+
|
368
|
+
export const Everything = {
|
369
|
+
render: () => <Story text={text} extensions={allExtensions} selection={{ anchor: 99, head: 110 }} />,
|
350
370
|
};
|
351
371
|
|
352
372
|
export const Empty = {
|
@@ -390,7 +410,7 @@ const headings = str(
|
|
390
410
|
const global = new Map<string, EditorSelectionState>();
|
391
411
|
|
392
412
|
export const Folding = {
|
393
|
-
render: () => <Story text={text} extensions={[
|
413
|
+
render: () => <Story text={text} extensions={[folding()]} />,
|
394
414
|
};
|
395
415
|
|
396
416
|
export const Scrolling = {
|
@@ -445,7 +465,7 @@ export const Image = {
|
|
445
465
|
};
|
446
466
|
|
447
467
|
export const Code = {
|
448
|
-
render: () => <Story text={str(content.
|
468
|
+
render: () => <Story text={str(content.codeblocks, content.footer)} extensions={[decorateMarkdown()]} />,
|
449
469
|
};
|
450
470
|
|
451
471
|
export const Lists = {
|
@@ -466,7 +486,7 @@ export const OrderedList = {
|
|
466
486
|
};
|
467
487
|
|
468
488
|
export const TaskList = {
|
469
|
-
render: () => <Story text={str(content.tasks, content.footer)} extensions={[decorateMarkdown()]} debug />,
|
489
|
+
render: () => <Story text={str(content.tasks, content.footer)} extensions={[decorateMarkdown()]} debug='raw' />,
|
470
490
|
};
|
471
491
|
|
472
492
|
export const Table = {
|
@@ -486,6 +506,12 @@ export const CommentedOut = {
|
|
486
506
|
),
|
487
507
|
};
|
488
508
|
|
509
|
+
export const Typescript = {
|
510
|
+
render: () => (
|
511
|
+
<Story text={content.typescript} lineNumbers extensions={[editorMonospace, javascript({ typescript: true })]} />
|
512
|
+
),
|
513
|
+
};
|
514
|
+
|
489
515
|
//
|
490
516
|
// Custom
|
491
517
|
//
|
@@ -669,7 +695,7 @@ export const Typewriter = {
|
|
669
695
|
export const Blast = {
|
670
696
|
render: () => (
|
671
697
|
<Story
|
672
|
-
text={str('# Blast', '', content.paragraphs, content.
|
698
|
+
text={str('# Blast', '', content.paragraphs, content.codeblocks, content.paragraphs)}
|
673
699
|
extensions={[
|
674
700
|
typewriter({ items: typewriterItems }),
|
675
701
|
blast(
|
package/src/defaults.ts
CHANGED
@@ -8,7 +8,9 @@ import { mx } from '@dxos/react-ui-theme';
|
|
8
8
|
|
9
9
|
import { fontMono } from './styles';
|
10
10
|
|
11
|
-
|
11
|
+
// TODO(burdon): Define scrollMargins for fixed gutter?
|
12
|
+
// https://codemirror.net/docs/ref/#view.EditorView^scrollMargins
|
13
|
+
const margin = '!mt-[1rem]';
|
12
14
|
|
13
15
|
/**
|
14
16
|
* CodeMirror content width.
|
@@ -20,16 +22,16 @@ export const editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-
|
|
20
22
|
/**
|
21
23
|
* Margin for numbers.
|
22
24
|
*/
|
23
|
-
export const editorFullWidth = mx(margin
|
25
|
+
export const editorFullWidth = mx(margin);
|
24
26
|
|
25
27
|
export const editorWithToolbarLayout =
|
26
28
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
27
29
|
|
28
|
-
// TODO(burdon): Define scrollMargins for fixed gutter.
|
29
30
|
export const editorGutter = EditorView.theme({
|
30
31
|
// Match margin from content.
|
31
32
|
'.cm-gutters': {
|
32
33
|
marginTop: '16px',
|
34
|
+
paddingRight: '1rem',
|
33
35
|
},
|
34
36
|
});
|
35
37
|
|
@@ -10,12 +10,11 @@ import React, { useEffect, useState } from 'react';
|
|
10
10
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
11
11
|
import { BroadcastChannelNetworkAdapter } from '@dxos/automerge/automerge-repo-network-broadcastchannel';
|
12
12
|
import { Expando, create } from '@dxos/echo-schema';
|
13
|
-
import { type PublicKey } from '@dxos/keys';
|
14
13
|
import { Filter, DocAccessor, createDocAccessor, useSpace, useQuery, type Space } from '@dxos/react-client/echo';
|
15
14
|
import { useIdentity, type Identity } from '@dxos/react-client/halo';
|
16
|
-
import { ClientRepeater } from '@dxos/react-client/testing';
|
15
|
+
import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
|
17
16
|
import { useThemeContext } from '@dxos/react-ui';
|
18
|
-
import { withTheme } from '@dxos/storybook-utils';
|
17
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
19
18
|
|
20
19
|
import { editorContent } from '../../defaults';
|
21
20
|
import { useTextEditor } from '../../hooks';
|
@@ -95,12 +94,12 @@ const Story = () => {
|
|
95
94
|
export default {
|
96
95
|
title: 'react-ui-editor/Automerge',
|
97
96
|
component: Editor,
|
98
|
-
decorators: [withTheme],
|
97
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
99
98
|
render: () => <Story />,
|
100
|
-
parameters: { translations
|
99
|
+
parameters: { translations },
|
101
100
|
};
|
102
101
|
|
103
|
-
const EchoStory = ({ spaceKey }:
|
102
|
+
const EchoStory = ({ spaceKey }: ClientRepeatedComponentProps) => {
|
104
103
|
const identity = useIdentity();
|
105
104
|
const space = useSpace(spaceKey);
|
106
105
|
const [source, setSource] = useState<DocAccessor>();
|
@@ -4,9 +4,10 @@
|
|
4
4
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
6
6
|
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
7
|
-
import { bracketMatching } from '@codemirror/language';
|
7
|
+
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
8
8
|
import { searchKeymap } from '@codemirror/search';
|
9
9
|
import { EditorState, type Extension } from '@codemirror/state';
|
10
|
+
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
10
11
|
import {
|
11
12
|
EditorView,
|
12
13
|
type KeyBinding,
|
@@ -131,6 +132,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
131
132
|
export type ThemeExtensionsOptions = {
|
132
133
|
themeMode?: ThemeMode;
|
133
134
|
styles?: ThemeStyles;
|
135
|
+
syntaxHighlighting?: boolean;
|
134
136
|
slots?: {
|
135
137
|
editor?: {
|
136
138
|
className?: string;
|
@@ -147,13 +149,22 @@ const defaultThemeSlots = {
|
|
147
149
|
},
|
148
150
|
};
|
149
151
|
|
150
|
-
|
151
|
-
|
152
|
-
|
152
|
+
/**
|
153
|
+
* https://codemirror.net/examples/styling
|
154
|
+
*/
|
155
|
+
export const createThemeExtensions = ({
|
156
|
+
themeMode,
|
157
|
+
styles,
|
158
|
+
syntaxHighlighting: _syntaxHighlighting,
|
159
|
+
slots: _slots,
|
160
|
+
}: ThemeExtensionsOptions = {}): Extension => {
|
153
161
|
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
154
162
|
return [
|
155
163
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
156
164
|
EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
|
165
|
+
// https://github.com/codemirror/theme-one-dark
|
166
|
+
_syntaxHighlighting &&
|
167
|
+
(themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle)),
|
157
168
|
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
158
169
|
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
159
170
|
].filter(isNotFalsy);
|
@@ -4,28 +4,41 @@
|
|
4
4
|
|
5
5
|
import { codeFolding, foldGutter } from '@codemirror/language';
|
6
6
|
import { type Extension } from '@codemirror/state';
|
7
|
+
import { EditorView } from '@codemirror/view';
|
7
8
|
import React from 'react';
|
8
9
|
|
9
10
|
import { Icon } from '@dxos/react-ui';
|
10
11
|
import { getSize } from '@dxos/react-ui-theme';
|
11
12
|
|
12
|
-
import { renderRoot } from './util';
|
13
|
+
import { createElement, renderRoot } from './util';
|
13
14
|
|
14
15
|
export type FoldingOptions = {};
|
15
16
|
|
16
17
|
/**
|
17
18
|
* https://codemirror.net/examples/gutter
|
18
19
|
*/
|
20
|
+
// TODO(burdon): Remember folding state.
|
19
21
|
export const folding = (_props: FoldingOptions = {}): Extension => [
|
20
22
|
codeFolding({
|
21
|
-
placeholderDOM: () =>
|
23
|
+
placeholderDOM: () => {
|
24
|
+
return document.createElement('span'); // Collapse content.
|
25
|
+
},
|
22
26
|
}),
|
23
27
|
foldGutter({
|
24
28
|
markerDOM: (open) => {
|
25
29
|
return renderRoot(
|
26
|
-
|
27
|
-
<Icon icon='ph--caret-right--regular' classNames={[getSize(3), '
|
30
|
+
createElement('div', { className: 'flex h-full items-center' }),
|
31
|
+
<Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'mx-3 cursor-pointer', open && 'rotate-90']} />,
|
28
32
|
);
|
29
33
|
},
|
30
34
|
}),
|
35
|
+
EditorView.theme({
|
36
|
+
'.cm-foldGutter': {
|
37
|
+
opacity: 0.3,
|
38
|
+
transition: 'opacity 0.3s',
|
39
|
+
},
|
40
|
+
'.cm-foldGutter:hover': {
|
41
|
+
opacity: 1,
|
42
|
+
},
|
43
|
+
}),
|
31
44
|
];
|
@@ -5,11 +5,10 @@
|
|
5
5
|
import { completionKeymap } from '@codemirror/autocomplete';
|
6
6
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
8
|
-
import {
|
8
|
+
import { syntaxHighlighting } from '@codemirror/language';
|
9
9
|
import { languages } from '@codemirror/language-data';
|
10
10
|
import { lintKeymap } from '@codemirror/lint';
|
11
11
|
import { type Extension } from '@codemirror/state';
|
12
|
-
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
13
12
|
import { keymap } from '@codemirror/view';
|
14
13
|
|
15
14
|
import { type ThemeMode } from '@dxos/react-ui';
|
@@ -54,9 +53,6 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
54
53
|
],
|
55
54
|
}),
|
56
55
|
|
57
|
-
// https://github.com/codemirror/theme-one-dark
|
58
|
-
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
59
|
-
|
60
56
|
// Custom styles.
|
61
57
|
syntaxHighlighting(markdownHighlightStyle()),
|
62
58
|
|
@@ -17,6 +17,19 @@ import { table } from './table';
|
|
17
17
|
import { theme, type HeadingLevel } from '../../styles';
|
18
18
|
import { wrapWithCatch } from '../util';
|
19
19
|
|
20
|
+
/**
|
21
|
+
* Unicode characters.
|
22
|
+
* NOTE: Depends on font.
|
23
|
+
* https://www.compart.com/en/unicode (nice resource).
|
24
|
+
* https://en.wikipedia.org/wiki/List_of_Unicode_characters
|
25
|
+
*/
|
26
|
+
const Unicode = {
|
27
|
+
emDash: '\u2014',
|
28
|
+
bullet: '\u2022',
|
29
|
+
bulletSmall: '\u2219',
|
30
|
+
bulletSquare: '\u2b1d',
|
31
|
+
};
|
32
|
+
|
20
33
|
//
|
21
34
|
// Widgets
|
22
35
|
//
|
@@ -67,12 +80,18 @@ class CheckboxWidget extends WidgetType {
|
|
67
80
|
input.setAttribute('disabled', 'true');
|
68
81
|
} else {
|
69
82
|
input.onmousedown = (event: Event) => {
|
70
|
-
|
71
|
-
const
|
72
|
-
|
83
|
+
// Could be beginning of line.
|
84
|
+
const line = view.state.doc.lineAt(view.posAtDOM(span));
|
85
|
+
const text = view.state.sliceDoc(line.from, line.to);
|
86
|
+
const match = text.match(/^\s*- (\[[xX ]]).*/);
|
87
|
+
if (match) {
|
88
|
+
const [, checked] = match;
|
89
|
+
const pos = line.from + text.indexOf(checked);
|
90
|
+
this._checked = checked !== '[ ]';
|
73
91
|
view.dispatch({
|
74
92
|
changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
|
75
93
|
});
|
94
|
+
|
76
95
|
event.preventDefault();
|
77
96
|
}
|
78
97
|
};
|
@@ -246,17 +265,20 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
246
265
|
}
|
247
266
|
|
248
267
|
case 'ListItem': {
|
268
|
+
const line = state.doc.lineAt(node.from);
|
269
|
+
|
249
270
|
// Set indentation.
|
250
271
|
const list = getCurrentListLevel();
|
251
272
|
const width = list.type === 'OrderedList' ? orderedListIndentationWidth : bulletListIndentationWidth;
|
252
273
|
const offset = ((list.level ?? 0) + 1) * width;
|
253
|
-
const line = state.doc.lineAt(node.from);
|
254
274
|
if (node.from === line.to - 1) {
|
255
275
|
// Abort if only the hyphen is typed.
|
256
276
|
return false;
|
257
277
|
}
|
258
278
|
|
259
|
-
// Add line decoration for continuation indent.
|
279
|
+
// Add line decoration for the continuation indent.
|
280
|
+
// TODO(burdon): Bug if indentation is more than one indentation unit (e.g., 4 spaces) from the previous line.
|
281
|
+
|
260
282
|
deco.add(
|
261
283
|
line.from,
|
262
284
|
line.from,
|
@@ -268,32 +290,26 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
268
290
|
}),
|
269
291
|
);
|
270
292
|
|
271
|
-
// Remove indentation spaces.
|
272
|
-
const text = state.doc.sliceString(line.from, node.to);
|
273
|
-
const whitespace = text.match(/^ */)?.[0].length ?? 0;
|
274
|
-
if (whitespace) {
|
275
|
-
atomicDeco.add(line.from, line.from + whitespace, hide);
|
276
|
-
}
|
277
|
-
|
278
293
|
break;
|
279
294
|
}
|
280
295
|
|
281
296
|
case 'ListMark': {
|
297
|
+
const list = getCurrentListLevel();
|
298
|
+
|
282
299
|
// Look-ahead for task marker.
|
283
|
-
// NOTE: Requires space to exist (otherwise
|
300
|
+
// NOTE: Requires space to exist (otherwise the text is parsed as the start of a link).
|
284
301
|
const next = tree.resolve(node.to + 1, 1);
|
285
302
|
if (next?.name === 'TaskMarker') {
|
286
|
-
atomicDeco.add(node.from, node.to + 1, hide);
|
287
303
|
break;
|
288
304
|
}
|
289
305
|
|
290
|
-
const list = getCurrentListLevel();
|
291
|
-
|
292
306
|
// TODO(burdon): Option to make hierarchical; or a), i), etc.
|
293
|
-
const label = list.type === 'OrderedList' ? `${++list.number}.` :
|
307
|
+
const label = list.type === 'OrderedList' ? `${++list.number}.` : Unicode.bulletSmall;
|
308
|
+
const line = state.doc.lineAt(node.from);
|
309
|
+
const to = state.doc.sliceString(node.to, node.to + 1) === ' ' ? node.to + 1 : node.to;
|
294
310
|
atomicDeco.add(
|
295
|
-
|
296
|
-
|
311
|
+
line.from,
|
312
|
+
to,
|
297
313
|
Decoration.replace({
|
298
314
|
widget: new TextWidget(
|
299
315
|
label,
|
@@ -305,10 +321,11 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
305
321
|
}
|
306
322
|
|
307
323
|
case 'TaskMarker': {
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
324
|
+
const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
|
325
|
+
// Check if the next character is a space and if so, include it in the replacement.
|
326
|
+
const line = state.doc.lineAt(node.from);
|
327
|
+
const to = state.doc.sliceString(node.to, node.to + 1) === ' ' ? node.to + 1 : node.to;
|
328
|
+
atomicDeco.add(line.from, to, checked ? checkedTask : uncheckedTask);
|
312
329
|
break;
|
313
330
|
}
|
314
331
|
|
@@ -8,6 +8,21 @@ import { createRoot } from 'react-dom/client';
|
|
8
8
|
import { ThemeProvider } from '@dxos/react-ui';
|
9
9
|
import { defaultTx } from '@dxos/react-ui-theme';
|
10
10
|
|
11
|
+
export type ElementOptions = {
|
12
|
+
className?: string;
|
13
|
+
};
|
14
|
+
|
15
|
+
export const createElement = (tag: string, options?: ElementOptions, children?: ReactNode): HTMLElement => {
|
16
|
+
const el = document.createElement(tag);
|
17
|
+
if (options?.className) {
|
18
|
+
el.className = options.className;
|
19
|
+
}
|
20
|
+
if (children) {
|
21
|
+
el.append(...(Array.isArray(children) ? children : [children]));
|
22
|
+
}
|
23
|
+
return el;
|
24
|
+
};
|
25
|
+
|
11
26
|
export const renderRoot = (root: HTMLElement, node: ReactNode): HTMLElement => {
|
12
27
|
createRoot(root).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
|
13
28
|
return root;
|