@dxos/react-ui-editor 0.10.0 → 0.11.0

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 (113) hide show
  1. package/dist/lib/index.mjs +1477 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +32 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/types/src/components/Editor/Editor.d.ts +22 -17
  6. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  7. package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
  8. package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
  9. package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
  10. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
  11. package/dist/types/src/components/Editor/index.d.ts +2 -0
  12. package/dist/types/src/components/Editor/index.d.ts.map +1 -1
  13. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
  14. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  15. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
  16. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
  18. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
  20. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
  22. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
  24. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  27. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
  28. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  29. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
  31. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  32. package/dist/types/src/components/index.d.ts +1 -0
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +0 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
  37. package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
  38. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
  40. package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
  41. package/dist/types/src/stories/Comments.stories.d.ts +53 -14
  42. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  43. package/dist/types/src/stories/Diff.stories.d.ts +32 -0
  44. package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
  45. package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
  46. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  47. package/dist/types/src/stories/Folding.stories.d.ts +18 -0
  48. package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
  49. package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
  50. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  51. package/dist/types/src/stories/Popover.stories.d.ts +3 -3
  52. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  53. package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
  54. package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
  55. package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
  56. package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
  57. package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
  58. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
  59. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  60. package/dist/types/src/stories/components/util.d.ts +16 -0
  61. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +40 -83
  64. package/src/components/Editor/Editor.tsx +142 -35
  65. package/src/components/Editor/EditorContext.ts +34 -0
  66. package/src/components/Editor/EditorView.tsx +9 -6
  67. package/src/components/Editor/index.ts +3 -0
  68. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
  69. package/src/components/EditorMenuProvider/menu.ts +4 -2
  70. package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
  71. package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
  72. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
  73. package/src/components/EditorPreviewProvider/index.ts +2 -0
  74. package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
  75. package/src/components/EditorToolbar/index.ts +1 -0
  76. package/src/components/EditorToolbar/lists.ts +2 -6
  77. package/src/components/EditorToolbar/view-mode.ts +41 -12
  78. package/src/components/index.ts +1 -0
  79. package/src/index.ts +0 -1
  80. package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
  81. package/src/stories/Automerge.stories.tsx +99 -78
  82. package/src/stories/Blocks.stories.tsx +83 -0
  83. package/src/stories/Comments.stories.tsx +273 -74
  84. package/src/stories/Diff.stories.tsx +48 -0
  85. package/src/stories/EditorToolbar.stories.tsx +2 -2
  86. package/src/stories/Experimental.stories.tsx +13 -2
  87. package/src/stories/Folding.stories.tsx +120 -0
  88. package/src/stories/Markdown.stories.tsx +1 -1
  89. package/src/stories/Outliner.stories.tsx +18 -7
  90. package/src/stories/Popover.stories.tsx +27 -5
  91. package/src/stories/Suggest.stories.tsx +199 -0
  92. package/src/stories/TrackChanges.stories.tsx +226 -0
  93. package/src/stories/Widgets.stories.tsx +162 -10
  94. package/src/stories/components/EditorStory.tsx +15 -6
  95. package/src/stories/components/util.tsx +40 -2
  96. package/dist/lib/browser/index.mjs +0 -1676
  97. package/dist/lib/browser/index.mjs.map +0 -7
  98. package/dist/lib/browser/meta.json +0 -1
  99. package/dist/lib/browser/translations.mjs +0 -39
  100. package/dist/lib/browser/translations.mjs.map +0 -7
  101. package/dist/lib/node-esm/index.mjs +0 -1678
  102. package/dist/lib/node-esm/index.mjs.map +0 -7
  103. package/dist/lib/node-esm/meta.json +0 -1
  104. package/dist/lib/node-esm/translations.mjs +0 -41
  105. package/dist/lib/node-esm/translations.mjs.map +0 -7
  106. package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
  107. package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
  108. package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
  109. package/dist/types/src/extensions/index.d.ts +0 -2
  110. package/dist/types/src/extensions/index.d.ts.map +0 -1
  111. package/src/extensions/assistant-extension.tsx +0 -223
  112. package/src/extensions/index.ts +0 -5
  113. /package/dist/types/src/{extensions → stories}/Assistant.stories.d.ts +0 -0
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { type Meta, type StoryObj } from '@storybook/react-vite';
13
- import React, { useEffect, useMemo, useState } from 'react';
13
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
14
14
  import { createPortal } from 'react-dom';
15
15
 
16
16
  import { random } from '@dxos/random';
@@ -35,9 +35,11 @@ import { EditorPreviewProvider, useEditorPreview } from '../components';
35
35
  import { useTextEditor } from '../hooks';
36
36
  import { EditorStory } from './components';
37
37
 
38
- // ----------------------------------------------------------------------------
38
+ random.seed(123);
39
+
40
+ //
39
41
  // XmlTags helpers
40
- // ----------------------------------------------------------------------------
42
+ //
41
43
 
42
44
  const xmlRegistry = {
43
45
  test: {
@@ -85,9 +87,9 @@ const XmlTagsStory = ({ text }: { text?: string }) => {
85
87
  );
86
88
  };
87
89
 
88
- // ----------------------------------------------------------------------------
90
+ //
89
91
  // Preview helpers
90
- // ----------------------------------------------------------------------------
92
+ //
91
93
 
92
94
  const handlePreviewLookup = async ({ dxn, label }: PreviewLinkRef): Promise<PreviewLinkTarget> => {
93
95
  random.seed(dxn.split('').reduce((acc: number, char: string) => acc + char.charCodeAt(0), 1));
@@ -140,16 +142,82 @@ const PreviewBlockCard = ({ dxn, label }: XmlWidgetProps<{ dxn: string; label: s
140
142
  </Card.Header>
141
143
  {text && (
142
144
  <Card.Row>
143
- <Card.Text className='text-description'>{text}</Card.Text>
145
+ <Card.Text classNames='text-description'>{text}</Card.Text>
144
146
  </Card.Row>
145
147
  )}
146
148
  </Card.Root>
147
149
  );
148
150
  };
149
151
 
150
- // ----------------------------------------------------------------------------
152
+ /** Obsidian-style height suffix in the image label: `![Sketch|400](echo:/…)`. */
153
+ const parseBlockHeight = (label = ''): number | undefined => {
154
+ const match = /\|(\d+)$/.exec(label);
155
+ return match ? Number.parseInt(match[1], 10) : undefined;
156
+ };
157
+
158
+ /**
159
+ * Fixed-height block that mirrors the plugin-markdown sketch embed: it renders at exactly the height
160
+ * encoded in the label, so CM's reserved `estimatedHeight` and the measured height match — the setup
161
+ * that exercises the scroll/cull path (jitter, blank, flash, jump) without needing ECHO.
162
+ */
163
+ const FixedHeightPreview = ({ label, dxn }: XmlWidgetProps<{ label: string; dxn: string }>) => {
164
+ const height = parseBlockHeight(label) ?? 200;
165
+ return (
166
+ <div
167
+ style={{ height }}
168
+ className='grid place-items-center border border-separator rounded-md bg-base-surface text-description'
169
+ >
170
+ {label} · {height}px · {dxn}
171
+ </div>
172
+ );
173
+ };
174
+
175
+ /**
176
+ * Faithfully mimics a Surface-backed embed (the real plugin-markdown block): the content mounts
177
+ * asynchronously (like a Surface resolving) and runs an inner ResizeObserver (like the sketch's
178
+ * auto-fit) that re-lays-out whenever the container size changes — including the 0↔height blips that
179
+ * CM's scroll-culling produces as the node is detached and re-parented.
180
+ *
181
+ * NOTE: The scroll-*jump* on large scroll deltas is NOT specific to this async variant — verified via
182
+ * Playwright, `FixedHeightPreview` (plain inert div) jumps identically. It is CM's height-estimate
183
+ * re-anchor, not the widget. See `StubWidget` and CM #1727.
184
+ */
185
+ const SurfaceLikePreview = ({ label, dxn }: XmlWidgetProps<{ label: string; dxn: string }>) => {
186
+ const height = parseBlockHeight(label) ?? 200;
187
+ const ref = useRef<HTMLDivElement>(null);
188
+ const [resolved, setResolved] = useState(false);
189
+ // Async resolution, like a Surface looking up + mounting its component.
190
+ useEffect(() => {
191
+ const timer = setTimeout(() => setResolved(true), 30);
192
+ return () => clearTimeout(timer);
193
+ }, []);
194
+ // Inner ResizeObserver that does layout work on every size change, like the sketch's auto-fit.
195
+ useEffect(() => {
196
+ const element = ref.current;
197
+ if (!element) {
198
+ return;
199
+ }
200
+ const observer = new ResizeObserver(() => {
201
+ // Touch layout the way an auto-fit would (read forces reflow).
202
+ void element.clientHeight;
203
+ });
204
+ observer.observe(element);
205
+ return () => observer.disconnect();
206
+ }, []);
207
+ return (
208
+ <div
209
+ ref={ref}
210
+ style={{ height }}
211
+ className='grid place-items-center border border-separator rounded-md bg-base-surface text-description'
212
+ >
213
+ {resolved ? `Surface ${label} · ${height}px · ${dxn}` : 'resolving…'}
214
+ </div>
215
+ );
216
+ };
217
+
218
+ //
151
219
  // Meta
152
- // ----------------------------------------------------------------------------
220
+ //
153
221
 
154
222
  const meta = {
155
223
  title: 'ui/react-ui-editor/Widgets',
@@ -161,9 +229,9 @@ export default meta;
161
229
 
162
230
  type Story = StoryObj<typeof meta>;
163
231
 
164
- // ----------------------------------------------------------------------------
232
+ //
165
233
  // Stories
166
- // ----------------------------------------------------------------------------
234
+ //
167
235
 
168
236
  const xmlTagsText = trim`
169
237
  # XML Tags
@@ -197,6 +265,8 @@ const previewText = trim`
197
265
 
198
266
  ![ECHO](echo:/echo)
199
267
 
268
+ Text
269
+
200
270
  `;
201
271
 
202
272
  /**
@@ -239,3 +309,85 @@ export const Preview: Story = {
239
309
  );
240
310
  },
241
311
  };
312
+
313
+ const filler = (marker: string) =>
314
+ Array.from({ length: 20 }, (_, index) => `${marker} ${index + 1} - ${random.lorem.paragraphs()}`).join('\n\n');
315
+
316
+ const previewScrollText = [
317
+ '# Preview blocks (scroll test)',
318
+ filler('Above'),
319
+ '![Sketch|400](echo:/sketch)',
320
+ filler('Below'),
321
+ ].join('\n\n');
322
+
323
+ /**
324
+ * A fixed-height (400px) `echo:` block embed in a long scrollable document. Sets `estimatedHeight`
325
+ * from the label so CM reserves the right space, matching the plugin-markdown sketch embed. Use this
326
+ * to reproduce scroll behavior: scroll the block off-screen and back, and past the viewport edges.
327
+ */
328
+ export const PreviewScroll: Story = {
329
+ render: () => {
330
+ const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);
331
+ const extensions = useMemo(
332
+ () => [
333
+ xmlTags({
334
+ registry: {
335
+ 'dxn-preview': {
336
+ block: true,
337
+ urlSchemes: ['dxn:', 'echo:'],
338
+ estimatedHeight: ({ label }: XmlWidgetProps<{ label?: string }>) => parseBlockHeight(label),
339
+ Component: FixedHeightPreview,
340
+ },
341
+ },
342
+ setWidgets,
343
+ }),
344
+ ],
345
+ [],
346
+ );
347
+
348
+ return (
349
+ <>
350
+ <EditorStory text={previewScrollText} extensions={extensions} />
351
+ {widgets.map(({ id, root, Component, props }) => (
352
+ <div key={id}>{createPortal(<Component {...props} />, root)}</div>
353
+ ))}
354
+ </>
355
+ );
356
+ },
357
+ };
358
+
359
+ /**
360
+ * Same long scrollable document as `PreviewScroll`, but the block renders a Surface-like component
361
+ * (async mount + inner ResizeObserver). Use this to reproduce the scroll jump that only appears with
362
+ * Surface-backed embeds, and to verify fixes against it.
363
+ */
364
+ export const PreviewScrollSurface: Story = {
365
+ render: () => {
366
+ const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);
367
+ const extensions = useMemo(
368
+ () => [
369
+ xmlTags({
370
+ registry: {
371
+ 'dxn-preview': {
372
+ block: true,
373
+ urlSchemes: ['dxn:', 'echo:'],
374
+ estimatedHeight: ({ label }: XmlWidgetProps<{ label?: string }>) => parseBlockHeight(label),
375
+ Component: SurfaceLikePreview,
376
+ },
377
+ },
378
+ setWidgets,
379
+ }),
380
+ ],
381
+ [],
382
+ );
383
+
384
+ return (
385
+ <>
386
+ <EditorStory text={previewScrollText} extensions={extensions} />
387
+ {widgets.map(({ id, root, Component, props }) => (
388
+ <div key={id}>{createPortal(<Component {...props} />, root)}</div>
389
+ ))}
390
+ </>
391
+ );
392
+ },
393
+ };
@@ -9,7 +9,6 @@ import { Obj } from '@dxos/echo';
9
9
  import { TestSchema } from '@dxos/echo/testing';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { PublicKey } from '@dxos/keys';
12
- import { log } from '@dxos/log';
13
12
  import { useMergeRefs, useThemeContext } from '@dxos/react-ui';
14
13
  import { useAttentionAttributes } from '@dxos/react-ui-attention';
15
14
  import { Syntax } from '@dxos/react-ui-syntax-highlighter';
@@ -50,6 +49,7 @@ export const EditorStory = forwardRef<EditorController, EditorStoryProps>(
50
49
  ({ debug, debugCustom, text, extensions: extensionsProp, ...props }, forwardedRef) => {
51
50
  const controllerRef = useRef<EditorController>(null);
52
51
  const mergedRef = useMergeRefs([controllerRef, forwardedRef]);
52
+ const view = controllerRef.current?.view;
53
53
 
54
54
  const attentionAttrs = useAttentionAttributes('test-panel');
55
55
  const [tree, setTree] = useState<DebugNode>();
@@ -60,14 +60,13 @@ export const EditorStory = forwardRef<EditorController, EditorStoryProps>(
60
60
  [debug, extensionsProp],
61
61
  );
62
62
 
63
- const view = controllerRef.current?.view;
64
63
  return (
65
64
  <div className={mx('dx-container grid', debug && 'grid-cols-2 lg:grid-cols-[1fr_600px]')}>
66
65
  <EditorComponent ref={mergedRef} object={object} text={text} extensions={extensions} {...props} />
67
66
 
68
67
  {debug && (
69
68
  <div
70
- className='grid h-full auto-rows-fr border-l border-separator divide-y divide-separator overflow-hidden'
69
+ className='grid h-full auto-rows-fr border-l border-separator divide-y divide-subdued-separator overflow-hidden'
71
70
  {...attentionAttrs}
72
71
  >
73
72
  {view && debugCustom?.(view)}
@@ -124,8 +123,19 @@ const EditorComponent = forwardRef<EditorController, EditorStoryProps>(
124
123
  selection,
125
124
  initialValue: text,
126
125
  extensions: [
127
- createBasicExtensions({ readOnly, placeholder, lineNumbers, scrollPastEnd: true, search: true }),
128
- createThemeExtensions({ monospace, themeMode, syntaxHighlighting: true, slots }),
126
+ createBasicExtensions({
127
+ lineNumbers,
128
+ placeholder,
129
+ readOnly,
130
+ scrollPastEnd: true,
131
+ search: true,
132
+ }),
133
+ createThemeExtensions({
134
+ monospace,
135
+ slots,
136
+ syntaxHighlighting: true,
137
+ themeMode,
138
+ }),
129
139
  createMarkdownExtensions(),
130
140
  extensions || [],
131
141
  ],
@@ -135,7 +145,6 @@ const EditorComponent = forwardRef<EditorController, EditorStoryProps>(
135
145
 
136
146
  // External controller.
137
147
  useImperativeHandle(forwardedRef, () => {
138
- log.info('view updated', { id });
139
148
  return createEditorController(view);
140
149
  }, [id, view]);
141
150
 
@@ -13,18 +13,56 @@ import {
13
13
  folding,
14
14
  formattingKeymap,
15
15
  image,
16
+ join,
16
17
  linkTooltip,
17
18
  table,
18
19
  } from '@dxos/ui-editor';
19
20
  import { type RenderCallback } from '@dxos/ui-editor/types';
20
21
  import { safeUrl } from '@dxos/util';
21
22
 
23
+ random.seed(1);
24
+
22
25
  import { str } from '../../util';
23
26
 
24
27
  export const num = () => random.number.int({ min: 0, max: 9999 }).toLocaleString();
25
28
 
26
29
  export const img = '![dxos](https://dxos.network/dxos-logotype-blue.png)';
27
30
 
31
+ export type GenerateListOptions = {
32
+ /** Maximum nesting depth. */
33
+ depth?: number;
34
+ /** Children per node — a fixed count or an inclusive `[min, max]` range. */
35
+ children?: number | [number, number];
36
+ /** Emit task markers (`- [ ]`) rather than plain bullets (`- `). */
37
+ task?: boolean;
38
+ /** Spaces of indentation per level. */
39
+ indent?: number;
40
+ /** Content generator for each item (default: a lorem sentence). */
41
+ content?: () => string;
42
+ };
43
+
44
+ /**
45
+ * Generates a hierarchical markdown list for the outliner stories. Deterministic under `random.seed`.
46
+ */
47
+ export const generateList = ({
48
+ depth = 3,
49
+ children = [1, 3],
50
+ task = true,
51
+ indent = 2,
52
+ content = () => random.lorem.sentence(),
53
+ }: GenerateListOptions = {}): string => {
54
+ const count = (value: number | [number, number]): number =>
55
+ Array.isArray(value) ? random.number.int({ min: value[0], max: value[1] }) : value;
56
+
57
+ const build = (level: number): string[] =>
58
+ Array.from({ length: count(children) }).flatMap(() => {
59
+ const line = ' '.repeat(indent * level) + (task ? '- [ ] ' : '- ') + content();
60
+ return level + 1 < depth ? [line, ...build(level + 1)] : [line];
61
+ });
62
+
63
+ return join(...build(0));
64
+ };
65
+
28
66
  export const code = str(
29
67
  // prettier-ignore
30
68
  '// Code',
@@ -231,13 +269,13 @@ export const renderLinkButton: RenderCallback<{ url: string }> = (el, { url }) =
231
269
  export const defaultExtensions: Extension[] = [
232
270
  decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
233
271
  formattingKeymap(),
234
- linkTooltip(renderLinkTooltip),
272
+ linkTooltip({ render: renderLinkTooltip }),
235
273
  ];
236
274
 
237
275
  export const allExtensions: Extension[] = [
238
276
  decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100, numberedHeadings: { from: 2, to: 4 } }),
239
277
  formattingKeymap(),
240
- linkTooltip(renderLinkTooltip),
278
+ linkTooltip({ render: renderLinkTooltip }),
241
279
  image(),
242
280
  table(),
243
281
  folding(),