@dxos/react-ui-editor 0.6.11-staging.e6894a4 → 0.6.12-main.5cc132e

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 (43) 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/automerge/automerge.test.d.ts.map +1 -1
  11. package/dist/types/src/extensions/factories.d.ts +5 -1
  12. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  13. package/dist/types/src/extensions/folding.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/extensions/markdown/formatting.test.d.ts.map +1 -1
  17. package/dist/types/src/extensions/util/react.d.ts +4 -0
  18. package/dist/types/src/extensions/util/react.d.ts.map +1 -1
  19. package/dist/types/src/hooks/useTextEditor.d.ts +2 -2
  20. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  21. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  22. package/dist/types/src/styles/theme.d.ts.map +1 -1
  23. package/package.json +30 -25
  24. package/src/InputMode.stories.tsx +8 -10
  25. package/src/TextEditor.stories.tsx +58 -32
  26. package/src/defaults.ts +5 -3
  27. package/src/extensions/automerge/automerge.stories.tsx +5 -6
  28. package/src/extensions/automerge/{automerge.spec.tsx → automerge.test.tsx} +1 -0
  29. package/src/extensions/automerge/automerge.ts +1 -1
  30. package/src/extensions/factories.ts +15 -4
  31. package/src/extensions/folding.tsx +17 -4
  32. package/src/extensions/markdown/bundle.ts +1 -5
  33. package/src/extensions/markdown/changes.test.ts +1 -3
  34. package/src/extensions/markdown/decorate.ts +40 -23
  35. package/src/extensions/markdown/formatting.test.ts +1 -3
  36. package/src/extensions/markdown/parser.test.ts +1 -2
  37. package/src/extensions/util/react.tsx +15 -0
  38. package/src/hooks/useTextEditor.ts +3 -5
  39. package/src/styles/markdown.ts +0 -2
  40. package/src/styles/theme.ts +12 -8
  41. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +0 -2
  42. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +0 -1
  43. package/src/extensions/automerge/automerge.test.ts +0 -13
@@ -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
 
@@ -4,9 +4,7 @@
4
4
 
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { EditorState, type StateCommand } from '@codemirror/state';
7
- import { expect } from 'chai';
8
-
9
- import { describe, test } from '@dxos/test';
7
+ import { describe, expect, test } from 'vitest';
10
8
 
11
9
  import {
12
10
  addBlockquote,
@@ -5,8 +5,7 @@
5
5
  // @ts-ignore
6
6
  import { testTree } from '@lezer/generator/test';
7
7
  import { parser } from '@lezer/markdown';
8
-
9
- import { describe, test } from '@dxos/test';
8
+ import { describe, test } from 'vitest';
10
9
 
11
10
  describe('parser', () => {
12
11
  // test.only('list-mark', () => {
@@ -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;
@@ -17,7 +17,7 @@ import {
17
17
  } from 'react';
18
18
 
19
19
  import { log } from '@dxos/log';
20
- import { isNotFalsy, type MaybeFunction } from '@dxos/util';
20
+ import { getProviderValue, isNotFalsy, type MaybeProvider } from '@dxos/util';
21
21
 
22
22
  import { createEditorStateTransaction, documentId, editorInputMode, type EditorSelection } from '../extensions';
23
23
  import { logChanges } from '../util';
@@ -57,13 +57,11 @@ let instanceCount = 0;
57
57
  * Hook for creating editor.
58
58
  */
59
59
  export const useTextEditor = (
60
- props: MaybeFunction<UseTextEditorProps> = {},
60
+ props: MaybeProvider<UseTextEditorProps> = {},
61
61
  deps: DependencyList = [],
62
62
  ): UseTextEditor => {
63
63
  const { id, initialValue, extensions, autoFocus, scrollTo, selection, moveToEndOfLine, debug } =
64
- useMemo<UseTextEditorProps>(() => {
65
- return typeof props === 'function' ? props() : props;
66
- }, deps ?? []);
64
+ useMemo<UseTextEditorProps>(() => getProviderValue(props), deps ?? []);
67
65
 
68
66
  // NOTE: Increments by 2 in strict mode.
69
67
  const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
@@ -16,8 +16,6 @@ const headings: Record<HeadingLevel, string> = {
16
16
  6: 'text-md',
17
17
  };
18
18
 
19
- // TODO(burdon): Define theme as facet (used in multiple extensions).
20
- // TODO(burdon): Organize theme styles for widgets.
21
19
  export const theme = {
22
20
  mark: 'opacity-50',
23
21
  code: 'font-mono !no-underline text-neutral-700 dark:text-neutral-300',
@@ -73,16 +73,19 @@ export const defaultTheme: ThemeStyles = {
73
73
  * NOTE: Gutters should have the same top margin as the content.
74
74
  */
75
75
  '.cm-gutters': {
76
- background: 'unset',
76
+ background: 'var(--surface-bg)',
77
77
  },
78
78
  '.cm-gutter': {},
79
+ '.cm-gutter.cm-lineNumbers .cm-gutterElement': {
80
+ minWidth: '40px',
81
+ alignContent: 'center',
82
+ },
83
+ /**
84
+ * Height is set to match the corresponding line.
85
+ */
79
86
  '.cm-gutterElement': {
87
+ alignItems: 'center',
80
88
  fontSize: '16px',
81
- lineHeight: 1.5,
82
- },
83
-
84
- '.cm-lineNumbers': {
85
- minWidth: '36px',
86
89
  },
87
90
 
88
91
  /**
@@ -198,13 +201,14 @@ export const defaultTheme: ThemeStyles = {
198
201
  * </div>
199
202
  * </div
200
203
  */
201
- // TODO(burdon): Apply react-ui-theme or replace panel.
204
+ // TODO(burdon): Implement custom panel (with icon buttons).
202
205
  '.cm-panels': {},
203
206
  '.cm-panel': {
204
207
  fontFamily: fontBody,
205
- backgroundColor: 'var(--dx-base)',
208
+ backgroundColor: 'var(--surface-bg)',
206
209
  },
207
210
  '.cm-panel input, .cm-panel button, .cm-panel label': {
211
+ color: 'var(--dx-subdued)',
208
212
  fontFamily: fontBody,
209
213
  fontSize: '14px',
210
214
  all: 'unset',
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=automerge.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"automerge.spec.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/automerge/automerge.spec.tsx"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { expect } from 'chai';
6
-
7
- import { describe, test } from '@dxos/test';
8
-
9
- describe('Test', () => {
10
- test('checks sanity', async () => {
11
- expect(true).to.be.true;
12
- });
13
- });