@dxos/react-ui-editor 0.6.11-staging.a542fc9 → 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.
Files changed (33) hide show
  1. package/dist/lib/browser/index.mjs +126 -73
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  5. package/dist/types/src/TextEditor.stories.d.ts +10 -2
  6. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  7. package/dist/types/src/defaults.d.ts.map +1 -1
  8. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +0 -1
  9. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/factories.d.ts +5 -1
  11. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  13. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  14. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  15. package/dist/types/src/extensions/util/react.d.ts +4 -0
  16. package/dist/types/src/extensions/util/react.d.ts.map +1 -1
  17. package/dist/types/src/hooks/useTextEditor.d.ts +2 -2
  18. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  19. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  20. package/dist/types/src/styles/theme.d.ts.map +1 -1
  21. package/package.json +24 -24
  22. package/src/InputMode.stories.tsx +8 -10
  23. package/src/TextEditor.stories.tsx +58 -32
  24. package/src/defaults.ts +5 -3
  25. package/src/extensions/automerge/automerge.stories.tsx +5 -6
  26. package/src/extensions/factories.ts +15 -4
  27. package/src/extensions/folding.tsx +17 -4
  28. package/src/extensions/markdown/bundle.ts +1 -5
  29. package/src/extensions/markdown/decorate.ts +40 -23
  30. package/src/extensions/util/react.tsx +15 -0
  31. package/src/hooks/useTextEditor.ts +3 -5
  32. package/src/styles/markdown.ts +0 -2
  33. 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-staging.a542fc9",
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-staging.a542fc9",
51
- "@dxos/automerge": "0.6.11-staging.a542fc9",
52
- "@dxos/display-name": "0.6.11-staging.a542fc9",
53
- "@dxos/context": "0.6.11-staging.a542fc9",
54
- "@dxos/echo-schema": "0.6.11-staging.a542fc9",
55
- "@dxos/debug": "0.6.11-staging.a542fc9",
56
- "@dxos/invariant": "0.6.11-staging.a542fc9",
57
- "@dxos/log": "0.6.11-staging.a542fc9",
58
- "@dxos/protocols": "0.6.11-staging.a542fc9",
59
- "@dxos/react-async": "0.6.11-staging.a542fc9",
60
- "@dxos/react-ui": "0.6.11-staging.a542fc9",
61
- "@dxos/util": "0.6.11-staging.a542fc9",
62
- "@dxos/react-ui-theme": "0.6.11-staging.a542fc9"
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/automerge": "0.6.11-staging.a542fc9",
86
- "@dxos/config": "0.6.11-staging.a542fc9",
87
- "@dxos/echo-signals": "0.6.11-staging.a542fc9",
88
- "@dxos/keyboard": "0.6.11-staging.a542fc9",
89
- "@dxos/random": "0.6.11-staging.a542fc9",
90
- "@dxos/echo-typegen": "0.6.11-staging.a542fc9",
91
- "@dxos/react-client": "0.6.11-staging.a542fc9",
92
- "@dxos/storybook-utils": "0.6.11-staging.a542fc9",
93
- "@dxos/react-ui": "0.6.11-staging.a542fc9"
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-staging.a542fc9"
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, Tooltip } from '@dxos/react-ui';
9
+ import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
- import { withFullscreen, withTheme } from '@dxos/storybook-utils';
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
- <Tooltip.Provider>
58
- <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
59
- <Toolbar.Markdown />
60
- <EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
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, withFullscreen()],
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 { withFullscreen, withTheme } from '@dxos/storybook-utils';
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 = '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)';
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
- code: str(
115
- '### Code',
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.code,
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?: boolean;
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, withFullscreen()],
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} selection={{ anchor: 99, head: 110 }} />,
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={[editorGutter, folding()]} />,
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.code, content.footer)} extensions={[decorateMarkdown()]} />,
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.code, content.paragraphs)}
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
- const margin = '!mt-[16px]';
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, '!ml-[3rem]');
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, layout: 'fullscreen' },
99
+ parameters: { translations },
101
100
  };
102
101
 
103
- const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
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
- // TODO(burdon): Should only have one baseTheme?
151
- // https://codemirror.net/examples/styling
152
- export const createThemeExtensions = ({ themeMode, styles, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
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: () => document.createElement('div'),
23
+ placeholderDOM: () => {
24
+ return document.createElement('span'); // Collapse content.
25
+ },
22
26
  }),
23
27
  foldGutter({
24
28
  markerDOM: (open) => {
25
29
  return renderRoot(
26
- document.createElement('div'),
27
- <Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'm-2 cursor-pointer', open && 'rotate-90']} />,
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 { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
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
- const pos = view.posAtDOM(span);
71
- const text = view.state.sliceDoc(pos, pos + 3);
72
- if (text === (this._checked ? '[x]' : '[ ]')) {
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 processes as a link).
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
- node.from,
296
- node.to + 1,
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
- if (!editingRange(state, node, focus)) {
309
- const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
310
- atomicDeco.add(node.from, node.to + 1, checked ? checkedTask : uncheckedTask);
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;