@dxos/react-ui-editor 0.6.9 → 0.6.10-main.48c066e

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 (30) hide show
  1. package/dist/lib/browser/index.mjs +66 -62
  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 +1 -1
  5. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  6. package/dist/types/src/TextEditor.stories.d.ts +1 -1
  7. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/defaults.d.ts +5 -1
  9. package/dist/types/src/defaults.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts +3 -2
  11. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  15. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  16. package/dist/types/src/styles/theme.d.ts.map +1 -1
  17. package/dist/types/src/styles/tokens.d.ts +1 -2
  18. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  19. package/package.json +24 -27
  20. package/src/InputMode.stories.tsx +1 -1
  21. package/src/TextEditor.stories.tsx +3 -3
  22. package/src/defaults.ts +13 -5
  23. package/src/extensions/autocomplete.ts +4 -1
  24. package/src/extensions/automerge/automerge.stories.tsx +1 -1
  25. package/src/extensions/factories.ts +4 -4
  26. package/src/extensions/markdown/bundle.ts +3 -0
  27. package/src/extensions/markdown/decorate.ts +31 -25
  28. package/src/extensions/markdown/parser.test.ts +29 -0
  29. package/src/styles/theme.ts +26 -32
  30. package/src/styles/tokens.ts +4 -4
package/src/defaults.ts CHANGED
@@ -4,14 +4,23 @@
4
4
 
5
5
  import { EditorView } from '@codemirror/view';
6
6
 
7
+ import { mx } from '@dxos/react-ui-theme';
8
+
7
9
  import { getToken } from './styles';
8
10
 
11
+ const marginY = '!mt-[16px] !mb-[32px]';
12
+
9
13
  /**
10
14
  * CodeMirror content width.
11
15
  * 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
12
16
  * 50rem = 800px. Maximum content width for solo mode.
13
17
  */
14
- export const editorContent = '!mt-[16px] !mb-[32px] !mli-auto w-full max-w-[min(50rem,100%-4rem)]';
18
+ export const editorContent = mx(marginY, '!mli-auto w-full max-w-[min(50rem,100%-2rem)]');
19
+
20
+ /**
21
+ * Margin for numbers.
22
+ */
23
+ export const editorFullWidth = mx(marginY, '!ml-[3rem]');
15
24
 
16
25
  export const editorWithToolbarLayout =
17
26
  'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
@@ -21,10 +30,9 @@ export const editorGutter = EditorView.baseTheme({
21
30
  // Match margin from content.
22
31
  marginTop: '16px',
23
32
  marginBottom: '16px',
24
- // Inside within content margin.
25
- marginRight: '-32px',
26
- width: '32px',
27
- backgroundColor: 'transparent !important',
33
+ // TODO(burdon): Inset next to content.
34
+ // marginRight: `-${marginWidth}rem`,
35
+ // width: `${marginWidth}rem`,
28
36
  },
29
37
  });
30
38
 
@@ -9,6 +9,7 @@
9
9
  import {
10
10
  autocompletion,
11
11
  completionKeymap,
12
+ type CompletionSource,
12
13
  type Completion,
13
14
  type CompletionContext,
14
15
  type CompletionResult,
@@ -20,13 +21,14 @@ export type AutocompleteResult = Completion;
20
21
 
21
22
  export type AutocompleteOptions = {
22
23
  activateOnTyping?: boolean;
24
+ override?: CompletionSource[];
23
25
  onSearch?: (text: string) => Completion[];
24
26
  };
25
27
 
26
28
  /**
27
29
  * Autocomplete extension.
28
30
  */
29
- export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions = {}) => {
31
+ export const autocomplete = ({ activateOnTyping, override, onSearch }: AutocompleteOptions = {}) => {
30
32
  const extentions = [
31
33
  // https://codemirror.net/docs/ref/#view.keymap
32
34
  // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
@@ -37,6 +39,7 @@ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions
37
39
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
38
40
  autocompletion({
39
41
  activateOnTyping,
42
+ override,
40
43
 
41
44
  // closeOnBlur: false,
42
45
  // defaultKeymap: false,
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxosTheme';
5
+ import '@dxos-theme';
6
6
 
7
7
  import '@preact/signals-react';
8
8
  import React, { useEffect, useState } from 'react';
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, historyKeymap, standardKeymap } from '@codemirror/commands';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { bracketMatching } from '@codemirror/language';
8
8
  import { searchKeymap } from '@codemirror/search';
9
9
  import { EditorState, type Extension } from '@codemirror/state';
@@ -95,7 +95,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
95
95
  props.bracketMatching && bracketMatching(),
96
96
  props.closeBrackets && closeBrackets(),
97
97
  props.dropCursor && dropCursor(),
98
- props.drawSelection && drawSelection(),
98
+ props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
99
99
  props.highlightActiveLine && highlightActiveLine(),
100
100
  props.history && history(),
101
101
  props.lineNumbers && lineNumbers(),
@@ -109,9 +109,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
109
109
  keymap.of(
110
110
  [
111
111
  ...((props.keymap && keymaps[props.keymap]) ?? []),
112
- // NOTE: Tab configured by markdown extension.
112
+ // NOTE: Tabs are also configured by markdown extension.
113
113
  // https://codemirror.net/docs/ref/#commands.indentWithTab
114
- // ...(props.indentWithTab ? [indentWithTab] : []),
114
+ ...(props.indentWithTab ? [indentWithTab] : []),
115
115
  // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
116
116
  ...(props.closeBrackets ? closeBracketsKeymap : []),
117
117
  // https://codemirror.net/docs/ref/#commands.historyKeymap
@@ -44,6 +44,9 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
44
44
  // Languages for syntax highlighting fenced code blocks.
45
45
  codeLanguages: languages,
46
46
 
47
+ // Don't complete HTML tags.
48
+ completeHTMLTags: false,
49
+
47
50
  // Parser extensions.
48
51
  extensions: [
49
52
  // GFM provided by default.
@@ -145,7 +145,7 @@ const autoHideTags = new Set([
145
145
  type NumberingLevel = { type: string; from: number; to: number; level: number; number: number };
146
146
 
147
147
  const bulletListIndentationWidth = 24;
148
- const orderedListIndentationWidth = 32; // TODO(burdon): Make variable length based on number of digits.
148
+ const orderedListIndentationWidth = 36; // TODO(burdon): Make variable length based on number of digits.
149
149
 
150
150
  const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boolean) => {
151
151
  const deco = new RangeSetBuilder<Decoration>();
@@ -182,8 +182,9 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
182
182
  return listLevels[listLevels.length - 1];
183
183
  };
184
184
 
185
+ // const count = 0;
185
186
  const enterNode = (node: SyntaxNodeRef) => {
186
- // console.log('##', { node: node.name, from: node.from, to: node.to });
187
+ // console.log(`[${count++}]`, { node: node.name, from: node.from, to: node.to });
187
188
  switch (node.name) {
188
189
  // ATXHeading > HeaderMark > Paragraph
189
190
  // NOTE: Numbering requires processing the entire document since otherwise only the visible range will be
@@ -253,50 +254,55 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
253
254
  const list = getCurrentList();
254
255
  const width = list.type === 'OrderedList' ? orderedListIndentationWidth : bulletListIndentationWidth;
255
256
  const offset = ((list.level ?? 0) + 1) * width;
256
- const start = state.doc.lineAt(node.from);
257
+ const line = state.doc.lineAt(node.from);
258
+ if (node.from === line.to - 1) {
259
+ // Abort if only the hyphen is typed.
260
+ return false;
261
+ }
257
262
 
263
+ // Add line decoration to indent.
258
264
  deco.add(
259
- start.from,
260
- start.from,
265
+ line.from,
266
+ line.from,
261
267
  Decoration.line({
262
268
  class: 'cm-list-item',
263
269
  attributes: {
264
- // Subtract 0.25em to account for the space CM adds to Paragraph nodes following the ListItem.
265
- // Note: This makes the cursor appear to be left of the margin.
266
- style: `padding-left: ${offset}px; text-indent: calc(-${width}px - 0.25em);`,
270
+ style: `padding-left: ${offset}px; text-indent: -${width}px;`,
267
271
  },
268
272
  }),
269
273
  );
270
274
 
271
275
  // Remove indentation spaces.
272
- // TODO(burdon): Replace whitespace with atomic block. Parse ListMark inline.
273
- const line = state.doc.sliceString(start.from, node.to);
274
- const whitespace = line.match(/^ */)?.[0].length ?? 0;
276
+ const text = state.doc.sliceString(line.from, node.to);
277
+ const whitespace = text.match(/^ */)?.[0].length ?? 0;
275
278
  if (whitespace) {
276
- atomicDeco.add(start.from, start.from + whitespace, hide);
279
+ atomicDeco.add(line.from, line.from + whitespace, hide);
277
280
  }
278
281
 
279
- // const mark = node.node.firstChild!;
280
- // console.log(mark?.name);
281
- // if (mark?.name === 'ListMark') {}
282
282
  break;
283
283
  }
284
284
 
285
285
  case 'ListMark': {
286
286
  // Look-ahead for task marker.
287
- const task = tree.resolve(node.to + 1, 1).name === 'TaskMarker';
288
- if (task) {
289
- atomicDeco.add(node.from, node.to, hide);
287
+ const next = tree.resolve(node.to + 1, 1);
288
+ if (next?.name === 'TaskMarker') {
289
+ atomicDeco.add(node.from, node.to + 1, hide);
290
290
  break;
291
291
  }
292
292
 
293
- // TODO(burdon): Cursor stops for 1 character when moving back into number (but not dashes).
294
- // TODO(burdon): Option to make hierarchical; or a, b, c. etc.
295
293
  const list = getCurrentList();
296
- const label = list.type === 'OrderedList' ? `${++list.number}.` : '-';
294
+
295
+ // Abort unless followed by space.
296
+ const text = state.doc.sliceString(node.from, node.to + 1);
297
+ if (list.type === 'BulletList' && text[1] !== ' ') {
298
+ return false;
299
+ }
300
+
301
+ // TODO(burdon): Option to make hierarchical; or a), i), etc.
302
+ const label = list.type === 'OrderedList' ? `${++list.number}.` : '•';
297
303
  atomicDeco.add(
298
304
  node.from,
299
- node.to,
305
+ node.to + 1,
300
306
  Decoration.replace({
301
307
  widget: new TextWidget(
302
308
  label,
@@ -310,8 +316,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
310
316
  case 'TaskMarker': {
311
317
  if (!editingRange(state, node, focus)) {
312
318
  const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
313
- atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task-checkbox' }));
314
- atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
319
+ atomicDeco.add(node.from, node.to + 1, checked ? checkedTask : uncheckedTask);
315
320
  }
316
321
  break;
317
322
  }
@@ -574,7 +579,7 @@ const formattingStyles = EditorView.baseTheme({
574
579
 
575
580
  '& .cm-task': {
576
581
  display: 'inline-block',
577
- width: `calc(${bulletListIndentationWidth}px - 0.25em)`,
582
+ width: `${bulletListIndentationWidth}px`,
578
583
  color: getToken('extend.colors.blue.500'),
579
584
  },
580
585
  '& .cm-task-checkbox': {
@@ -587,6 +592,7 @@ const formattingStyles = EditorView.baseTheme({
587
592
  '& .cm-list-mark': {
588
593
  display: 'inline-block',
589
594
  textAlign: 'right',
595
+ paddingRight: '0.5em',
590
596
  fontVariant: 'tabular-nums',
591
597
  },
592
598
  '& .cm-list-mark-bullet': {
@@ -9,6 +9,34 @@ import { parser } from '@lezer/markdown';
9
9
  import { describe, test } from '@dxos/test';
10
10
 
11
11
  describe('parser', () => {
12
+ // test.only('list-mark', () => {
13
+ // const newParser = parser.configure({
14
+ // parseBlock: [
15
+ // {
16
+ // name: 'ListItem',
17
+ // parse: (cx, line) => {
18
+ // console.log(`[${line.text}]`, cx.lineStart, line.text.length);
19
+ // // line.skipSpace(1);
20
+ // return true;
21
+ // },
22
+ // },
23
+ // ],
24
+ // });
25
+ //
26
+ // {
27
+ // const result = newParser.parse(' - ');
28
+ // testTree(result, 'Document(BulletList(ListItem(ListMark)))');
29
+ // }
30
+ // {
31
+ // const result = newParser.parse('-x');
32
+ // testTree(result, 'Document(Paragraph)');
33
+ // }
34
+ // {
35
+ // const result = newParser.parse('- x');
36
+ // testTree(result, 'Document(BulletList(ListItem(ListMark,Paragraph)))');
37
+ // }
38
+ // });
39
+
12
40
  // https://www.markdownguide.org/basic-syntax/#lists-1
13
41
  test('lists', () => {
14
42
  // Indented list must have 4 spaces.
@@ -25,6 +53,7 @@ describe('parser', () => {
25
53
  '1. one',
26
54
  ].join('\n'),
27
55
  );
56
+
28
57
  testTree(
29
58
  result,
30
59
  [
@@ -9,8 +9,8 @@ import { getToken } from './tokens';
9
9
  export type ThemeStyles = Record<string, StyleSpec>;
10
10
 
11
11
  // TODO(burdon): Factor out theme.
12
- // TODO(burdon): Can we use @apply and import css file?
13
- // https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply?
12
+ // TODO(burdon): Factor out extension-specific logic.
13
+ // TODO(burdon): Remove getToken and use var/semantic colors.
14
14
 
15
15
  /**
16
16
  * Minimal styles.
@@ -82,29 +82,14 @@ export const defaultTheme: ThemeStyles = {
82
82
  * NOTE: Gutters should have the same top margin as the content.
83
83
  */
84
84
  '.cm-gutters': {
85
- background: 'transparent',
85
+ background: 'var(--dx-base)',
86
86
  },
87
87
  '.cm-gutter': {},
88
88
  '.cm-gutterElement': {
89
+ fontSize: '16px',
89
90
  lineHeight: 1.5,
90
91
  },
91
92
 
92
- //
93
- // Cursor
94
- //
95
- '&light .cm-cursor, &light .cm-dropCursor': {
96
- borderLeft: '2px solid black',
97
- },
98
- '&dark .cm-cursor, &dark .cm-dropCursor': {
99
- borderLeft: '2px solid white',
100
- },
101
- '&light .cm-placeholder': {
102
- color: getToken('extend.semanticColors.description.light', 'rgba(0,0,0,.2)'),
103
- },
104
- '&dark .cm-placeholder': {
105
- color: getToken('extend.semanticColors.description.dark', 'rgba(255,255,255,.2)'),
106
- },
107
-
108
93
  //
109
94
  // line
110
95
  //
@@ -112,7 +97,7 @@ export const defaultTheme: ThemeStyles = {
112
97
  paddingInline: 0,
113
98
  },
114
99
  '.cm-activeLine': {
115
- background: 'transparent',
100
+ background: 'var(--dx-hoverSurface)',
116
101
  },
117
102
 
118
103
  //
@@ -123,20 +108,31 @@ export const defaultTheme: ThemeStyles = {
123
108
  },
124
109
 
125
110
  //
126
- // Selection
111
+ // Cursor
127
112
  //
128
-
129
- '&light .cm-selectionBackground': {
130
- background: getToken('extend.colors.primary.100'),
113
+ '&light .cm-cursor, &light .cm-dropCursor': {
114
+ borderLeft: '2px solid black',
115
+ },
116
+ '&dark .cm-cursor, &dark .cm-dropCursor': {
117
+ borderLeft: '2px solid white',
118
+ },
119
+ '&light .cm-placeholder': {
120
+ color: getToken('extend.semanticColors.description.light', 'rgba(0,0,0,.2)'),
131
121
  },
132
- '&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
133
- background: getToken('extend.colors.primary.200'),
122
+ '&dark .cm-placeholder': {
123
+ color: getToken('extend.semanticColors.description.dark', 'rgba(255,255,255,.2)'),
134
124
  },
135
- '&dark .cm-selectionBackground': {
136
- background: getToken('extend.colors.primary.700'),
125
+
126
+ //
127
+ // Selection
128
+ //
129
+
130
+ '.cm-selectionBackground': {
131
+ background: 'var(--dx-selectionSurface) !important',
137
132
  },
138
- '&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
139
- background: getToken('extend.colors.primary.600'),
133
+
134
+ '.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
135
+ background: 'var(--dx-selectionSurface) !important',
140
136
  },
141
137
 
142
138
  //
@@ -274,8 +270,6 @@ export const defaultTheme: ThemeStyles = {
274
270
  },
275
271
  },
276
272
 
277
- // TODO(burdon): Factor out element specific logic.
278
-
279
273
  //
280
274
  // table
281
275
  //
@@ -4,14 +4,14 @@
4
4
 
5
5
  import get from 'lodash.get';
6
6
 
7
- import { tailwindConfig, type TailwindConfig } from '@dxos/react-ui-theme';
7
+ import { tokens } from '@dxos/react-ui-theme';
8
8
 
9
- const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
9
+ (window as any).__tokens = tokens;
10
10
 
11
11
  /**
12
- * @deprecated
13
- * Replace with CSS vars.
12
+ * Returns the tailwind token value.
14
13
  */
14
+ // TODO(burdon): Replace with CSS vars.
15
15
  export const getToken = (path: string, defaultValue?: string | string[]): string => {
16
16
  const value = get(tokens, path, defaultValue);
17
17
  return value?.toString() ?? '';