@dxos/react-ui-editor 0.4.5-main.f8d6c4e → 0.4.5

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 (58) hide show
  1. package/dist/lib/browser/index.mjs +464 -572
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +7 -5
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -0
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +2 -0
  9. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +2 -0
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  13. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +2 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  15. package/dist/types/src/extensions/cursor-line-margin.d.ts +8 -0
  16. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -0
  17. package/dist/types/src/extensions/index.d.ts +1 -0
  18. package/dist/types/src/extensions/index.d.ts.map +1 -1
  19. package/dist/types/src/extensions/markdown/decorate.d.ts +5 -0
  20. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
  21. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  22. package/dist/types/src/extensions/markdown/index.d.ts +1 -4
  23. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  24. package/dist/types/src/extensions/markdown/link.d.ts +1 -11
  25. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  26. package/dist/types/src/index.d.ts +2 -0
  27. package/dist/types/src/index.d.ts.map +1 -1
  28. package/dist/types/src/styles/layout.d.ts +2 -2
  29. package/dist/types/src/styles/layout.d.ts.map +1 -1
  30. package/dist/types/src/translations.d.ts +2 -0
  31. package/dist/types/src/translations.d.ts.map +1 -1
  32. package/package.json +29 -26
  33. package/src/components/TextEditor/MarkdownEditor.stories.tsx +29 -37
  34. package/src/components/TextEditor/hooks.stories.tsx +5 -17
  35. package/src/components/Toolbar/Toolbar.stories.tsx +4 -7
  36. package/src/components/Toolbar/Toolbar.tsx +79 -47
  37. package/src/extensions/cursor-line-margin.ts +92 -0
  38. package/src/extensions/index.ts +1 -0
  39. package/src/extensions/markdown/decorate.ts +301 -0
  40. package/src/extensions/markdown/formatting.test.ts +17 -0
  41. package/src/extensions/markdown/formatting.ts +67 -76
  42. package/src/extensions/markdown/index.ts +1 -4
  43. package/src/extensions/markdown/link.ts +23 -185
  44. package/src/index.ts +2 -0
  45. package/src/styles/layout.ts +3 -2
  46. package/src/translations.ts +5 -3
  47. package/dist/types/src/extensions/markdown/code.d.ts +0 -2
  48. package/dist/types/src/extensions/markdown/code.d.ts.map +0 -1
  49. package/dist/types/src/extensions/markdown/heading.d.ts +0 -6
  50. package/dist/types/src/extensions/markdown/heading.d.ts.map +0 -1
  51. package/dist/types/src/extensions/markdown/hr.d.ts +0 -2
  52. package/dist/types/src/extensions/markdown/hr.d.ts.map +0 -1
  53. package/dist/types/src/extensions/markdown/tasklist.d.ts +0 -3
  54. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +0 -1
  55. package/src/extensions/markdown/code.ts +0 -162
  56. package/src/extensions/markdown/heading.ts +0 -59
  57. package/src/extensions/markdown/hr.ts +0 -71
  58. package/src/extensions/markdown/tasklist.ts +0 -110
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;AAEhD,wBAkBE"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;AAEhD,wBAoBE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.4.5-main.f8d6c4e",
3
+ "version": "0.4.5",
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",
@@ -23,7 +23,7 @@
23
23
  "@codemirror/state": "^6.4.0",
24
24
  "@codemirror/theme-one-dark": "^6.1.2",
25
25
  "@codemirror/view": "^6.23.0",
26
- "@fluentui/react-tabster": "^9.18.0",
26
+ "@fluentui/react-tabster": "^9.19.0",
27
27
  "@lezer/common": "^1.2.1",
28
28
  "@lezer/highlight": "^1.2.0",
29
29
  "@lezer/markdown": "^1.2.0",
@@ -36,20 +36,20 @@
36
36
  "lodash.pick": "^4.4.0",
37
37
  "lodash.sortby": "^4.7.0",
38
38
  "style-mod": "^4.1.0",
39
- "@dxos/context": "0.4.5-main.f8d6c4e",
40
- "@dxos/async": "0.4.5-main.f8d6c4e",
41
- "@dxos/debug": "0.4.5-main.f8d6c4e",
42
- "@dxos/automerge": "0.4.5-main.f8d6c4e",
43
- "@dxos/display-name": "0.4.5-main.f8d6c4e",
44
- "@dxos/echo-schema": "0.4.5-main.f8d6c4e",
45
- "@dxos/invariant": "0.4.5-main.f8d6c4e",
46
- "@dxos/log": "0.4.5-main.f8d6c4e",
47
- "@dxos/react-async": "0.4.5-main.f8d6c4e",
48
- "@dxos/react-ui": "0.4.5-main.f8d6c4e",
49
- "@dxos/react-ui-theme": "0.4.5-main.f8d6c4e",
50
- "@dxos/protocols": "0.4.5-main.f8d6c4e",
51
- "@dxos/text-model": "0.4.5-main.f8d6c4e",
52
- "@dxos/util": "0.4.5-main.f8d6c4e"
39
+ "@dxos/async": "0.4.5",
40
+ "@dxos/debug": "0.4.5",
41
+ "@dxos/automerge": "0.4.5",
42
+ "@dxos/context": "0.4.5",
43
+ "@dxos/echo-schema": "0.4.5",
44
+ "@dxos/display-name": "0.4.5",
45
+ "@dxos/protocols": "0.4.5",
46
+ "@dxos/invariant": "0.4.5",
47
+ "@dxos/log": "0.4.5",
48
+ "@dxos/react-ui": "0.4.5",
49
+ "@dxos/react-async": "0.4.5",
50
+ "@dxos/react-ui-theme": "0.4.5",
51
+ "@dxos/text-model": "0.4.5",
52
+ "@dxos/util": "0.4.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
@@ -65,7 +65,8 @@
65
65
  "@types/react": "^18.0.21",
66
66
  "@types/react-dom": "^18.0.6",
67
67
  "@types/react-test-renderer": "^17.0.2",
68
- "@vitest/ui": "^1.2.1",
68
+ "@vitest/browser": "^1.2.2",
69
+ "@vitest/ui": "^1.2.2",
69
70
  "deepsignal": "^1.5.0",
70
71
  "happy-dom": "^13.3.1",
71
72
  "jsdom": "^20.0.1",
@@ -76,20 +77,22 @@
76
77
  "vite-plugin-top-level-await": "^1.4.1",
77
78
  "vite-plugin-wasm": "^3.3.0",
78
79
  "vitest": "^1.2.2",
79
- "@dxos/echo-signals": "0.4.5-main.f8d6c4e",
80
- "@dxos/config": "0.4.5-main.f8d6c4e",
81
- "@dxos/echo-typegen": "0.4.5-main.f8d6c4e",
82
- "@dxos/random": "0.4.5-main.f8d6c4e",
83
- "@dxos/react-client": "0.4.5-main.f8d6c4e",
84
- "@dxos/keyboard": "0.4.5-main.f8d6c4e",
85
- "@dxos/react-ui": "0.4.5-main.f8d6c4e",
86
- "@dxos/storybook-utils": "0.4.5-main.f8d6c4e"
80
+ "@dxos/config": "0.4.5",
81
+ "@dxos/echo-signals": "0.4.5",
82
+ "@dxos/echo-typegen": "0.4.5",
83
+ "@dxos/keyboard": "0.4.5",
84
+ "@dxos/react-client": "0.4.5",
85
+ "@dxos/random": "0.4.5",
86
+ "@dxos/storybook-utils": "0.4.5",
87
+ "@dxos/react-ui": "0.4.5"
87
88
  },
88
89
  "peerDependencies": {
89
90
  "@phosphor-icons/react": "^2.0.5",
91
+ "@vitest/browser": "^1.2.2",
92
+ "@vitest/ui": "^1.2.2",
90
93
  "react": "^18.2.0",
91
94
  "react-dom": "^18.2.0",
92
- "@dxos/react-client": "0.4.5-main.f8d6c4e"
95
+ "@dxos/react-client": "0.4.5"
93
96
  },
94
97
  "publishConfig": {
95
98
  "access": "public"
@@ -19,19 +19,15 @@ import { withTheme } from '@dxos/storybook-utils';
19
19
  import { MarkdownEditor, type TextEditorProps } from './TextEditor';
20
20
  import {
21
21
  type CommentsOptions,
22
- type LinkOptions,
23
22
  autocomplete,
24
23
  blast,
25
- code,
24
+ decorateMarkdown,
26
25
  comments,
27
26
  defaultOptions,
28
- heading,
29
- hr,
30
27
  image,
31
- link,
28
+ linkTooltip,
32
29
  mention,
33
30
  table,
34
- tasklist,
35
31
  typewriter,
36
32
  useComments,
37
33
  formatting,
@@ -47,9 +43,11 @@ const str = (...lines: string[]) => lines.join('\n');
47
43
 
48
44
  const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
49
45
 
50
- // prettier-ignore
46
+ const img = '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)';
47
+
51
48
  const text = {
52
49
  tasks: str(
50
+ //
53
51
  '## Tasks',
54
52
  '',
55
53
  '- [x] decorator',
@@ -61,6 +59,7 @@ const text = {
61
59
  ),
62
60
 
63
61
  list: str(
62
+ //
64
63
  '## List',
65
64
  '',
66
65
  '- new york',
@@ -70,12 +69,13 @@ const text = {
70
69
  ),
71
70
 
72
71
  numbered: str(
72
+ //
73
73
  '## Numbered',
74
74
  '',
75
75
  '1. one',
76
76
  '2. two',
77
77
  '3. three',
78
- ''
78
+ '',
79
79
  ),
80
80
 
81
81
  code: str(
@@ -115,7 +115,7 @@ const text = {
115
115
  '',
116
116
  ),
117
117
 
118
- image: str('# Image', '', '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)'),
118
+ image: str('# Image', '', img),
119
119
 
120
120
  headings: str(
121
121
  ...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
@@ -123,7 +123,7 @@ const text = {
123
123
 
124
124
  paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
125
125
 
126
- footer: str('', '', '', '', '')
126
+ footer: str('', '', '', '', ''),
127
127
  };
128
128
 
129
129
  const document = str(
@@ -173,7 +173,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
173
173
  const hover =
174
174
  'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
175
175
 
176
- const onHoverLinkTooltip: LinkOptions['onHover'] = (el, url) => {
176
+ const renderLinkTooltip = (el: Element, url: string) => {
177
177
  const web = new URL(url);
178
178
  createRoot(el).render(
179
179
  <StrictMode>
@@ -206,7 +206,7 @@ const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
206
206
  );
207
207
  };
208
208
 
209
- const onRenderLink: LinkOptions['onRender'] = (el, url) => {
209
+ const renderLinkButton = (el: Element, url: string) => {
210
210
  createRoot(el).render(
211
211
  <StrictMode>
212
212
  <a href={url} target='_blank' rel='noreferrer' className={hover}>
@@ -257,14 +257,11 @@ const defaults = [
257
257
  autocomplete({
258
258
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
259
259
  }),
260
- code(),
260
+ decorateMarkdown({ renderLinkButton }),
261
261
  formatting(),
262
- heading(),
263
- hr(),
264
262
  image(),
265
- link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
263
+ linkTooltip(renderLinkTooltip),
266
264
  table(),
267
- tasklist(),
268
265
  ];
269
266
 
270
267
  export const Default = {
@@ -281,6 +278,11 @@ export const NoExtensions = {
281
278
 
282
279
  const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
283
280
 
281
+ const largeWithImages = faker.helpers
282
+ .multiple(() => [faker.lorem.paragraph({ min: 12, max: 16 }), img], { count: 20 })
283
+ .flatMap((x) => x)
284
+ .join('\n\n');
285
+
284
286
  export const Empty = {
285
287
  render: () => <Story />,
286
288
  };
@@ -289,26 +291,25 @@ export const Scrolling = {
289
291
  render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
290
292
  };
291
293
 
292
- export const Links = {
293
- render: () => (
294
- <Story
295
- text={str(text.links, text.footer)}
296
- extensions={[link({ onHover: onHoverLinkTooltip, onRender: onRenderLink })]}
297
- />
298
- ),
294
+ export const ScrollingWithImages = {
295
+ render: () => <Story text={str('# Large Document', '', largeWithImages)} extensions={[image()]} />,
299
296
  };
300
297
 
301
- export const Code = {
302
- render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
298
+ export const Links = {
299
+ render: () => <Story text={str(text.links, text.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
303
300
  };
304
301
 
305
302
  export const Image = {
306
303
  render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
307
304
  };
308
305
 
306
+ export const Code = {
307
+ render: () => <Story text={str(text.code, text.footer)} extensions={[decorateMarkdown()]} />,
308
+ };
309
+
309
310
  export const Lists = {
310
311
  render: () => (
311
- <Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
312
+ <Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[decorateMarkdown()]} />
312
313
  ),
313
314
  };
314
315
 
@@ -330,7 +331,7 @@ export const Autocomplete = {
330
331
  <Story
331
332
  text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
332
333
  extensions={[
333
- link({ onRender: onRenderLink }),
334
+ decorateMarkdown({ renderLinkButton }),
334
335
  autocomplete({
335
336
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
336
337
  }),
@@ -388,15 +389,6 @@ export const Comments = {
388
389
  },
389
390
  };
390
391
 
391
- export const HorizontalRule = {
392
- render: () => (
393
- <Story
394
- text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
395
- extensions={[hr()]}
396
- />
397
- ),
398
- };
399
-
400
392
  export const Vim = {
401
393
  render: () => (
402
394
  <Story
@@ -14,19 +14,15 @@ import { TextEditor } from './TextEditor';
14
14
  import {
15
15
  type EditorMode,
16
16
  EditorModes,
17
- code,
17
+ decorateMarkdown,
18
18
  createMarkdownExtensions,
19
19
  formatting,
20
- heading,
21
- hr,
22
20
  image,
23
- link,
24
21
  table,
25
- tasklist,
26
22
  useFormattingState,
27
23
  } from '../../extensions';
28
24
  import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
29
- import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
25
+ import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
30
26
  import { markdownTheme } from '../../themes';
31
27
  import translations from '../../translations';
32
28
  import { Toolbar } from '../Toolbar';
@@ -56,20 +52,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
56
52
  themeMode,
57
53
  theme: markdownTheme,
58
54
  slots: {
59
- editor: { className: editorFillLayoutEditor },
55
+ content: { className: '!p-4' },
60
56
  },
61
57
  }),
62
58
  // TODO(burdon): Move lineWrapping.
63
59
  createMarkdownExtensions({ placeholder, lineWrapping: true }),
64
60
  // TODO(burdon): Move into markdown bundle (with React callbacks).
65
- code(),
61
+ decorateMarkdown(),
66
62
  formatting(),
67
- heading(),
68
- hr(),
69
63
  image(),
70
- link(),
71
64
  table(),
72
- tasklist(),
73
65
  trackFormatting,
74
66
  ],
75
67
  [editorMode, themeMode, placeholder, trackFormatting, readonly],
@@ -87,11 +79,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
87
79
  <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
88
80
  </Toolbar.Root>
89
81
  <div role='none' className='overflow-y-auto'>
90
- <div
91
- role='textbox'
92
- className={mx(textBlockWidth, attentionSurface, editorFillLayoutRoot, 'p-4')}
93
- ref={parentRef}
94
- />
82
+ <div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
95
83
  </div>
96
84
  </div>
97
85
  );
@@ -14,13 +14,11 @@ import { withTheme } from '@dxos/storybook-utils';
14
14
 
15
15
  import { Toolbar } from './Toolbar';
16
16
  import {
17
- code,
17
+ decorateMarkdown,
18
18
  comments,
19
19
  formatting,
20
- heading,
21
20
  image,
22
21
  table,
23
- tasklist,
24
22
  useComments,
25
23
  useFormattingState,
26
24
  } from '../../extensions';
@@ -39,7 +37,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
39
37
  const [formattingState, formattingObserver] = useFormattingState();
40
38
  const extensions = useMemo(
41
39
  () => [
42
- code(),
40
+ decorateMarkdown(),
43
41
  comments({
44
42
  onCreate: ({ cursor }) => {
45
43
  const id = PublicKey.random().toHex();
@@ -48,10 +46,8 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
48
46
  },
49
47
  }),
50
48
  formatting(),
51
- heading(),
52
49
  image(),
53
50
  table(),
54
- tasklist(),
55
51
  formattingObserver,
56
52
  ],
57
53
  [],
@@ -76,8 +72,9 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
76
72
  model={model}
77
73
  extensions={extensions}
78
74
  slots={{
79
- root: { className: mx(textBlockWidth, editorFillLayoutRoot, 'pli-2') },
75
+ root: { className: mx(textBlockWidth, editorFillLayoutRoot) },
80
76
  editor: { className: editorFillLayoutEditor },
77
+ content: { className: '!pli-2' },
81
78
  }}
82
79
  />
83
80
  </div>
@@ -33,6 +33,7 @@ import {
33
33
  Select,
34
34
  type ThemedClassName,
35
35
  Toolbar as NaturalToolbar,
36
+ Tooltip,
36
37
  type ToolbarToggleGroupItemProps,
37
38
  useTranslation,
38
39
  } from '@dxos/react-ui';
@@ -41,6 +42,8 @@ import { getSize } from '@dxos/react-ui-theme';
41
42
  import { type Formatting } from '../../extensions';
42
43
  import { translationKey } from '../../translations';
43
44
 
45
+ const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
46
+
44
47
  const HeadingIcons: { [key: string]: Icon } = {
45
48
  '0': Paragraph,
46
49
  '1': TextHOne,
@@ -112,10 +115,20 @@ type ToolbarButtonProps = ToolbarToggleGroupItemProps & { Icon: Icon };
112
115
 
113
116
  const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
114
117
  return (
115
- <NaturalToolbar.ToggleGroupItem variant='ghost' {...props} classNames={buttonStyles}>
116
- <Icon className={iconStyles} />
117
- <span className='sr-only'>{children}</span>
118
- </NaturalToolbar.ToggleGroupItem>
118
+ <Tooltip.Root>
119
+ <Tooltip.Trigger asChild>
120
+ <NaturalToolbar.ToggleGroupItem variant='ghost' {...props} classNames={buttonStyles}>
121
+ <Icon className={iconStyles} />
122
+ <span className='sr-only'>{children}</span>
123
+ </NaturalToolbar.ToggleGroupItem>
124
+ </Tooltip.Trigger>
125
+ <Tooltip.Portal>
126
+ <Tooltip.Content {...tooltipProps}>
127
+ {children}
128
+ <Tooltip.Arrow />
129
+ </Tooltip.Content>
130
+ </Tooltip.Portal>
131
+ </Tooltip.Root>
119
132
  );
120
133
  };
121
134
 
@@ -129,40 +142,50 @@ const MarkdownHeading = () => {
129
142
  const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : undefined;
130
143
  const HeadingIcon = HeadingIcons[value ?? '0'];
131
144
  return (
132
- <Select.Root
133
- disabled={value === null}
134
- value={value ?? '0'}
135
- onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
136
- >
137
- <NaturalToolbar.Button asChild>
138
- <Select.TriggerButton variant='ghost' classNames={buttonStyles}>
139
- <span className='sr-only'>{t('heading label')}</span>
140
- <HeadingIcon className={iconStyles} />
141
- </Select.TriggerButton>
142
- </NaturalToolbar.Button>
143
- <Select.Portal>
144
- <Select.Content>
145
- <Select.ScrollUpButton />
146
- <Select.Viewport>
147
- <Select.Option value='0'>
148
- <Paragraph className={iconStyles} />
149
- </Select.Option>
150
- {[1, 2, 3, 4, 5, 6].map((level) => (
151
- <Select.Option key={level} value={String(level)}>
152
- {level === 1 && <TextHOne className={iconStyles} />}
153
- {level === 2 && <TextHTwo className={iconStyles} />}
154
- {level === 3 && <TextHThree className={iconStyles} />}
155
- {level === 4 && <TextHFour className={iconStyles} />}
156
- {level === 5 && <TextHFive className={iconStyles} />}
157
- {level === 6 && <TextHSix className={iconStyles} />}
145
+ <Tooltip.Root>
146
+ <Select.Root
147
+ disabled={value === null}
148
+ value={value ?? '0'}
149
+ onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
150
+ >
151
+ <Tooltip.Trigger asChild>
152
+ <NaturalToolbar.Button asChild>
153
+ <Select.TriggerButton variant='ghost' classNames={buttonStyles}>
154
+ <span className='sr-only'>{t('heading label')}</span>
155
+ <HeadingIcon className={iconStyles} />
156
+ </Select.TriggerButton>
157
+ </NaturalToolbar.Button>
158
+ </Tooltip.Trigger>
159
+ <Select.Portal>
160
+ <Select.Content>
161
+ <Select.ScrollUpButton />
162
+ <Select.Viewport>
163
+ <Select.Option value='0'>
164
+ <Paragraph className={iconStyles} />
158
165
  </Select.Option>
159
- ))}
160
- </Select.Viewport>
161
- <Select.ScrollDownButton />
162
- <Select.Arrow />
163
- </Select.Content>
164
- </Select.Portal>
165
- </Select.Root>
166
+ {[1, 2, 3, 4, 5, 6].map((level) => (
167
+ <Select.Option key={level} value={String(level)}>
168
+ {level === 1 && <TextHOne className={iconStyles} />}
169
+ {level === 2 && <TextHTwo className={iconStyles} />}
170
+ {level === 3 && <TextHThree className={iconStyles} />}
171
+ {level === 4 && <TextHFour className={iconStyles} />}
172
+ {level === 5 && <TextHFive className={iconStyles} />}
173
+ {level === 6 && <TextHSix className={iconStyles} />}
174
+ </Select.Option>
175
+ ))}
176
+ </Select.Viewport>
177
+ <Select.ScrollDownButton />
178
+ <Select.Arrow />
179
+ </Select.Content>
180
+ </Select.Portal>
181
+ </Select.Root>
182
+ <Tooltip.Portal>
183
+ <Tooltip.Content {...tooltipProps}>
184
+ {t('heading label')}
185
+ <Tooltip.Arrow />
186
+ </Tooltip.Content>
187
+ </Tooltip.Portal>
188
+ </Tooltip.Root>
166
189
  );
167
190
  };
168
191
 
@@ -233,7 +256,6 @@ const markdownBlocks: ButtonProps[] = [
233
256
  type: 'codeblock',
234
257
  Icon: CodeBlock,
235
258
  getState: (state) => state.blockType === 'codeblock',
236
- disabled: (state) => !state.blankLine,
237
259
  },
238
260
  {
239
261
  type: 'table',
@@ -277,15 +299,25 @@ const MarkdownExtended = () => {
277
299
  const { onAction } = useToolbarContext('MarkdownStyles');
278
300
  const { t } = useTranslation(translationKey);
279
301
  return (
280
- <NaturalToolbar.Button
281
- variant='ghost'
282
- data-testid='editor.toolbar.comment'
283
- onClick={() => onAction?.({ type: 'comment' })}
284
- classNames={buttonStyles}
285
- >
286
- <ChatText className={iconStyles} />
287
- <span className='sr-only'>{t('comment label')}</span>
288
- </NaturalToolbar.Button>
302
+ <Tooltip.Root>
303
+ <Tooltip.Trigger asChild>
304
+ <NaturalToolbar.Button
305
+ variant='ghost'
306
+ data-testid='editor.toolbar.comment'
307
+ onClick={() => onAction?.({ type: 'comment' })}
308
+ classNames={buttonStyles}
309
+ >
310
+ <ChatText className={iconStyles} />
311
+ <span className='sr-only'>{t('comment label')}</span>
312
+ </NaturalToolbar.Button>
313
+ </Tooltip.Trigger>
314
+ <Tooltip.Portal>
315
+ <Tooltip.Content {...tooltipProps}>
316
+ {t('comment label')}
317
+ <Tooltip.Arrow />
318
+ </Tooltip.Content>
319
+ </Tooltip.Portal>
320
+ </Tooltip.Root>
289
321
  );
290
322
  };
291
323
 
@@ -0,0 +1,92 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // Based upon @acheronfail’s implementation, fetched 16 Feb 2024
6
+ // https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
7
+
8
+ import { type EditorState, type Extension, Facet, Transaction } from '@codemirror/state';
9
+ import { EditorView } from '@codemirror/view';
10
+
11
+ /**
12
+ * Number of lines above/below the cursor to keep visible in the viewport.
13
+ * Defaults to 3.
14
+ */
15
+ export const cursorLineMarginFacet = Facet.define<number, number>({
16
+ combine: (input) => input[0] ?? 3,
17
+ });
18
+
19
+ const annotation = 'cursorLineMargin';
20
+ const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
21
+ // seems to the best approximation of CM5's `cursorScrollMargin`
22
+ // https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448
23
+ export const cursorLineMargin: Extension = EditorView.updateListener.of(
24
+ ({ transactions, state, selectionSet, startState, view }) => {
25
+ // make sure we don't trigger an infinite loop and ignore our own changes
26
+ if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
27
+ return;
28
+ }
29
+
30
+ const s = state;
31
+ const { main } = s.selection;
32
+ const cursorLineMargin = s.facet(cursorLineMarginFacet);
33
+
34
+ // editor rect
35
+ const rect = view.dom.getBoundingClientRect();
36
+ // top and bottom pixel positions of the visible region of the editor
37
+ const viewportTop = rect.top - view.documentTop;
38
+ const viewportBottom = rect.bottom - view.documentTop;
39
+
40
+ // line with main selection
41
+ const mainLine = lineAtPos(s, main.head);
42
+ // top most visible line
43
+ const _visTopLine = lineAtPos(s, view.lineBlockAtHeight(viewportTop).from);
44
+
45
+ // bottom most visible line
46
+ // NOTE: calculating this is slightly more complex - if the editor is
47
+ // larger than all the lines in it, and the `scrollPastEnd()` extension is
48
+ // enabled, then we need to calculate where the bottom most visible line
49
+ // should be if it extended all the way to the bottom of the editor
50
+ const botLineBlk = view.lineBlockAtHeight(viewportBottom);
51
+ const visBotLineDoc = lineAtPos(s, Math.min(botLineBlk.to, s.doc.length));
52
+ const visBotLine =
53
+ botLineBlk.bottom >= viewportBottom
54
+ ? visBotLineDoc
55
+ : visBotLineDoc + Math.floor((viewportBottom - botLineBlk.bottom) / view.defaultLineHeight);
56
+
57
+ // const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
58
+ const needsScrollTop = false;
59
+ const needsScrollBot = mainLine >= visBotLine - cursorLineMargin;
60
+
61
+ // the scroll margins are overlapping
62
+ if (needsScrollTop && needsScrollBot) {
63
+ console.warn('is cursorScrollMargin too large for the editor size?');
64
+ return;
65
+ }
66
+
67
+ // no need to scroll, we're in between scroll regions
68
+ if (!needsScrollTop && !needsScrollBot) {
69
+ return;
70
+ }
71
+
72
+ // if we're within the margin, but moving the other way, then don't scroll
73
+ if (selectionSet) {
74
+ const { head } = startState.selection.main;
75
+ const oldMainLine = lineAtPos(startState, head);
76
+ if (needsScrollTop && oldMainLine < mainLine) {
77
+ return;
78
+ }
79
+ if (needsScrollBot && oldMainLine > mainLine) {
80
+ return;
81
+ }
82
+ }
83
+
84
+ view.dispatch({
85
+ annotations: Transaction.userEvent.of(annotation),
86
+ effects: EditorView.scrollIntoView(s.selection.main.head, {
87
+ y: needsScrollTop ? 'start' : 'end',
88
+ yMargin: view.defaultLineHeight * cursorLineMargin,
89
+ }),
90
+ });
91
+ },
92
+ );
@@ -10,6 +10,7 @@ export * from './basic';
10
10
  export * from './blast';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
+ export * from './cursor-line-margin';
13
14
  export * from './listener';
14
15
  export * from './markdown';
15
16
  export * from './mention';