@dxos/plugin-markdown 0.6.12 → 0.6.13-main.548ca8d

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 (120) hide show
  1. package/dist/lib/browser/MarkdownContainer-52FJDCTV.mjs +467 -0
  2. package/dist/lib/browser/MarkdownContainer-52FJDCTV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-4MPY6KRJ.mjs +50 -0
  4. package/dist/lib/browser/chunk-4MPY6KRJ.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-DRJ3FPYF.mjs +15 -0
  6. package/dist/lib/browser/chunk-DRJ3FPYF.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-CQJL4G4X.mjs → chunk-US5O2P3R.mjs} +4 -2
  8. package/dist/lib/browser/chunk-US5O2P3R.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +77 -117
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/meta.mjs +1 -1
  13. package/dist/lib/browser/types/index.mjs +6 -4
  14. package/dist/lib/node/MarkdownContainer-5XPB5VP5.cjs +482 -0
  15. package/dist/lib/node/MarkdownContainer-5XPB5VP5.cjs.map +7 -0
  16. package/dist/lib/node/chunk-MOF6UCLA.cjs +72 -0
  17. package/dist/lib/node/chunk-MOF6UCLA.cjs.map +7 -0
  18. package/dist/lib/node/{DocumentCard-EHJDDSRY.cjs → chunk-P7YU53RP.cjs} +16 -10
  19. package/dist/lib/node/chunk-P7YU53RP.cjs.map +7 -0
  20. package/dist/lib/node/{chunk-VWQH4WC2.cjs → chunk-UJMOZCIA.cjs} +11 -8
  21. package/dist/lib/node/chunk-UJMOZCIA.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +111 -147
  23. package/dist/lib/node/index.cjs.map +4 -4
  24. package/dist/lib/node/meta.cjs +3 -3
  25. package/dist/lib/node/meta.cjs.map +1 -1
  26. package/dist/lib/node/meta.json +1 -1
  27. package/dist/lib/node/types/index.cjs +8 -6
  28. package/dist/lib/node/types/index.cjs.map +2 -2
  29. package/dist/lib/node-esm/MarkdownContainer-ILCO3PDV.mjs +468 -0
  30. package/dist/lib/node-esm/MarkdownContainer-ILCO3PDV.mjs.map +7 -0
  31. package/dist/lib/node-esm/chunk-CD634NG3.mjs +51 -0
  32. package/dist/lib/node-esm/chunk-CD634NG3.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs +42 -0
  34. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs +17 -0
  36. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs.map +7 -0
  37. package/dist/lib/node-esm/index.mjs +493 -0
  38. package/dist/lib/node-esm/index.mjs.map +7 -0
  39. package/dist/lib/node-esm/meta.json +1 -0
  40. package/dist/lib/node-esm/meta.mjs +10 -0
  41. package/dist/lib/node-esm/types/index.mjs +15 -0
  42. package/dist/types/src/MarkdownPlugin.d.ts.map +1 -1
  43. package/dist/types/src/components/MarkdownContainer.d.ts +15 -0
  44. package/dist/types/src/components/MarkdownContainer.d.ts.map +1 -0
  45. package/dist/types/src/components/MarkdownEditor.d.ts +8 -3
  46. package/dist/types/src/components/MarkdownEditor.d.ts.map +1 -1
  47. package/dist/types/src/components/MarkdownEditor.stories.d.ts +3 -3
  48. package/dist/types/src/components/MarkdownEditor.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/index.d.ts +2 -11
  50. package/dist/types/src/components/index.d.ts.map +1 -1
  51. package/dist/types/src/extensions.d.ts +11 -15
  52. package/dist/types/src/extensions.d.ts.map +1 -1
  53. package/dist/types/src/hooks/index.d.ts +2 -0
  54. package/dist/types/src/hooks/index.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useSelectCurrentThread.d.ts +6 -0
  56. package/dist/types/src/hooks/useSelectCurrentThread.d.ts.map +1 -0
  57. package/dist/types/src/meta.d.ts +4 -9
  58. package/dist/types/src/meta.d.ts.map +1 -1
  59. package/dist/types/src/types/document.d.ts +10 -1
  60. package/dist/types/src/types/document.d.ts.map +1 -1
  61. package/dist/types/src/types/types.d.ts +8 -9
  62. package/dist/types/src/types/types.d.ts.map +1 -1
  63. package/package.json +41 -36
  64. package/src/MarkdownPlugin.tsx +49 -97
  65. package/src/components/MarkdownContainer.tsx +108 -0
  66. package/src/components/MarkdownEditor.stories.tsx +11 -5
  67. package/src/components/MarkdownEditor.tsx +40 -75
  68. package/src/components/index.ts +2 -14
  69. package/src/extensions.tsx +124 -67
  70. package/src/hooks/index.ts +5 -0
  71. package/src/hooks/useSelectCurrentThread.tsx +46 -0
  72. package/src/meta.ts +15 -0
  73. package/src/types/document.ts +12 -0
  74. package/src/types/types.ts +10 -7
  75. package/dist/lib/browser/DocumentCard-2P4EICBA.mjs +0 -11
  76. package/dist/lib/browser/DocumentEditor-GPWV3VN3.mjs +0 -11
  77. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs +0 -10
  78. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs.map +0 -7
  79. package/dist/lib/browser/chunk-354DCID5.mjs +0 -117
  80. package/dist/lib/browser/chunk-354DCID5.mjs.map +0 -7
  81. package/dist/lib/browser/chunk-4GGD6YJO.mjs +0 -19
  82. package/dist/lib/browser/chunk-4GGD6YJO.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-7AF2JLK4.mjs +0 -164
  84. package/dist/lib/browser/chunk-7AF2JLK4.mjs.map +0 -7
  85. package/dist/lib/browser/chunk-CQJL4G4X.mjs.map +0 -7
  86. package/dist/lib/browser/chunk-RL7QY322.mjs +0 -86
  87. package/dist/lib/browser/chunk-RL7QY322.mjs.map +0 -7
  88. package/dist/lib/browser/chunk-VUN4QKTT.mjs +0 -208
  89. package/dist/lib/browser/chunk-VUN4QKTT.mjs.map +0 -7
  90. package/dist/lib/node/DocumentCard-EHJDDSRY.cjs.map +0 -7
  91. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs +0 -29
  92. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs.map +0 -7
  93. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs +0 -31
  94. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs.map +0 -7
  95. package/dist/lib/node/chunk-7XIBNEI7.cjs +0 -238
  96. package/dist/lib/node/chunk-7XIBNEI7.cjs.map +0 -7
  97. package/dist/lib/node/chunk-KTYIOXL5.cjs +0 -149
  98. package/dist/lib/node/chunk-KTYIOXL5.cjs.map +0 -7
  99. package/dist/lib/node/chunk-Q4ZSCBQE.cjs +0 -114
  100. package/dist/lib/node/chunk-Q4ZSCBQE.cjs.map +0 -7
  101. package/dist/lib/node/chunk-RVGN72IX.cjs +0 -189
  102. package/dist/lib/node/chunk-RVGN72IX.cjs.map +0 -7
  103. package/dist/lib/node/chunk-TGMR2CKU.cjs +0 -52
  104. package/dist/lib/node/chunk-TGMR2CKU.cjs.map +0 -7
  105. package/dist/lib/node/chunk-VWQH4WC2.cjs.map +0 -7
  106. package/dist/types/src/components/DocumentCard.d.ts +0 -16
  107. package/dist/types/src/components/DocumentCard.d.ts.map +0 -1
  108. package/dist/types/src/components/DocumentEditor.d.ts +0 -14
  109. package/dist/types/src/components/DocumentEditor.d.ts.map +0 -1
  110. package/dist/types/src/components/HeadingMenu.d.ts +0 -13
  111. package/dist/types/src/components/HeadingMenu.d.ts.map +0 -1
  112. package/dist/types/src/components/Layout.d.ts +0 -6
  113. package/dist/types/src/components/Layout.d.ts.map +0 -1
  114. package/src/components/DocumentCard.tsx +0 -107
  115. package/src/components/DocumentEditor.tsx +0 -137
  116. package/src/components/HeadingMenu.tsx +0 -46
  117. package/src/components/Layout.tsx +0 -27
  118. package/src/meta.tsx +0 -19
  119. /package/dist/lib/{browser/DocumentCard-2P4EICBA.mjs.map → node-esm/meta.mjs.map} +0 -0
  120. /package/dist/lib/{browser/DocumentEditor-GPWV3VN3.mjs.map → node-esm/types/index.mjs.map} +0 -0
@@ -2,11 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type IconProps, TextAa } from '@phosphor-icons/react';
6
- import React, { type Ref } from 'react';
5
+ import { TextAa } from '@phosphor-icons/react';
6
+ import React from 'react';
7
7
 
8
8
  import {
9
- isObject,
10
9
  parseIntentPlugin,
11
10
  resolvePlugin,
12
11
  LayoutAction,
@@ -16,7 +15,6 @@ import {
16
15
  } from '@dxos/app-framework';
17
16
  import { create } from '@dxos/echo-schema';
18
17
  import { LocalStorageStore } from '@dxos/local-storage';
19
- import { log } from '@dxos/log';
20
18
  import { parseClientPlugin } from '@dxos/plugin-client';
21
19
  import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
22
20
  import { SpaceAction } from '@dxos/plugin-space';
@@ -34,12 +32,11 @@ import {
34
32
  EditorViewModes,
35
33
  translations as editorTranslations,
36
34
  } from '@dxos/react-ui-editor';
37
- import { isTileComponentProps } from '@dxos/react-ui-mosaic';
38
35
 
39
- import { type DocumentItemProps, DocumentCard, DocumentEditor, MarkdownEditor, MarkdownSettings } from './components';
36
+ import { MarkdownContainer, MarkdownSettings } from './components';
40
37
  import meta, { MARKDOWN_PLUGIN } from './meta';
41
38
  import translations from './translations';
42
- import { DocumentType, TextType } from './types';
39
+ import { DocumentType, isEditorModel, TextType } from './types';
43
40
  import {
44
41
  type MarkdownPluginProvides,
45
42
  type MarkdownSettingsProps,
@@ -48,19 +45,8 @@ import {
48
45
  } from './types';
49
46
  import { markdownExtensionPlugins, serializer } from './util';
50
47
 
51
- /**
52
- * Checks if an object conforms to the interface needed to render an editor.
53
- */
54
- const isEditorModel = (data: any): data is { id: string; text: string } => {
55
- return (
56
- data &&
57
- typeof data === 'object' &&
58
- 'id' in data &&
59
- typeof data.id === 'string' &&
60
- 'text' in data &&
61
- typeof data.text === 'string'
62
- );
63
- };
48
+ // TODO(burdon): Normalize active/object.
49
+ const getDoc = (object: any) => (object instanceof DocumentType ? object : undefined);
64
50
 
65
51
  export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
66
52
  const settings = new LocalStorageStore<MarkdownSettingsProps>(MARKDOWN_PLUGIN, {
@@ -72,13 +58,8 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
72
58
 
73
59
  const state = new LocalStorageStore<MarkdownPluginState>(MARKDOWN_PLUGIN, { extensionProviders: [], viewMode: {} });
74
60
 
75
- const getViewMode = (id?: string) => {
76
- return (id && state.values.viewMode[id]) || settings.values.defaultViewMode;
77
- };
78
-
79
- const setViewMode = (id: string, nextViewMode: EditorViewMode) => {
80
- state.values.viewMode[id] = nextViewMode;
81
- };
61
+ const getViewMode = (id: string) => (id && state.values.viewMode[id]) || settings.values.defaultViewMode;
62
+ const setViewMode = (id: string, viewMode: EditorViewMode) => (state.values.viewMode[id] = viewMode);
82
63
 
83
64
  return {
84
65
  meta,
@@ -109,7 +90,7 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
109
90
 
110
91
  markdownExtensionPlugins(plugins).forEach((plugin) => {
111
92
  const { extensions } = plugin.provides.markdown;
112
- state.values.extensionProviders.push(extensions);
93
+ state.values.extensionProviders?.push(extensions);
113
94
  });
114
95
  },
115
96
  provides: {
@@ -117,10 +98,9 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
117
98
  metadata: {
118
99
  records: {
119
100
  [DocumentType.typename]: {
120
- label: (object: any) => (object instanceof DocumentType ? object.name ?? object.fallbackName : undefined),
101
+ label: (object: any) => (object instanceof DocumentType ? object.name || object.fallbackName : undefined),
121
102
  placeholder: ['document title placeholder', { ns: MARKDOWN_PLUGIN }],
122
- icon: (props: IconProps) => <TextAa {...props} />,
123
- iconSymbol: 'ph--text-aa--regular',
103
+ icon: 'ph--text-aa--regular',
124
104
  graphProps: {
125
105
  managesAutofocus: true,
126
106
  },
@@ -134,6 +114,12 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
134
114
  echo: {
135
115
  schema: [DocumentType, TextType],
136
116
  },
117
+ space: {
118
+ onSpaceCreate: {
119
+ label: ['create document label', { ns: MARKDOWN_PLUGIN }],
120
+ action: MarkdownAction.CREATE,
121
+ },
122
+ },
137
123
  graph: {
138
124
  builder: (plugins) => {
139
125
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
@@ -167,8 +153,7 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
167
153
  },
168
154
  properties: {
169
155
  label: ['create document label', { ns: MARKDOWN_PLUGIN }],
170
- icon: (props: IconProps) => <TextAa {...props} />,
171
- iconSymbol: 'ph--text-aa--regular',
156
+ icon: 'ph--text-aa--regular',
172
157
  testId: 'markdownPlugin.createObject',
173
158
  },
174
159
  },
@@ -241,13 +226,10 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
241
226
  ],
242
227
  },
243
228
  thread: {
244
- // TODO(Zan): How to better handle the type predicate?
245
229
  predicate: (obj) => obj instanceof DocumentType,
246
230
  createSort: (doc: DocumentType) => {
247
231
  const accessor = doc.content ? createDocAccessor(doc.content, ['content']) : undefined;
248
-
249
232
  if (!accessor) {
250
- log.warn('No accessor found for document content.');
251
233
  return (_) => 0;
252
234
  }
253
235
 
@@ -256,75 +238,45 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
256
238
  return range?.start ?? Number.MAX_SAFE_INTEGER;
257
239
  };
258
240
 
259
- return (anchorA: string, anchorB: string) => getStartPosition(anchorA) - getStartPosition(anchorB);
241
+ return (anchorA: string | undefined, anchorB: string | undefined): number => {
242
+ if (anchorA === undefined || anchorB === undefined) {
243
+ return 0;
244
+ }
245
+ const posA = getStartPosition(anchorA);
246
+ const posB = getStartPosition(anchorB);
247
+ return posA - posB;
248
+ };
260
249
  },
261
250
  },
262
251
  surface: {
263
- component: ({ data, role, ...props }, forwardedRef) => {
264
- const doc =
265
- data.active instanceof DocumentType
266
- ? data.active
267
- : data.object instanceof DocumentType
268
- ? data.object
269
- : undefined;
270
-
252
+ component: ({ data, role }) => {
271
253
  switch (role) {
272
254
  case 'section':
273
255
  case 'article': {
274
- if (doc && doc.content) {
275
- return (
276
- <DocumentEditor
277
- role={role}
278
- coordinate={data.coordinate as LayoutCoordinate}
279
- document={doc}
280
- extensionProviders={state.values.extensionProviders}
281
- settings={settings.values}
282
- scrollPastEnd
283
- viewMode={getViewMode(fullyQualifiedId(doc))}
284
- onViewModeChange={setViewMode}
285
- />
286
- );
287
- } else if (isEditorModel(data.object)) {
288
- return (
289
- <MarkdownEditor
290
- id={data.object.id}
291
- role={role}
292
- coordinate={data.coordinate as LayoutCoordinate}
293
- initialValue={data.object.text}
294
- extensionProviders={state.values.extensionProviders}
295
- inputMode={settings.values.editorInputMode}
296
- toolbar={settings.values.toolbar}
297
- scrollPastEnd
298
- viewMode={getViewMode(data.object.id)}
299
- onViewModeChange={setViewMode}
300
- />
301
- );
302
- }
303
- break;
304
- }
256
+ // TODO(burdon): Normalize types (from FilesPlugin).
257
+ const doc = getDoc(data.active) ?? getDoc(data.object);
258
+ const { id, object } = isEditorModel(data.object)
259
+ ? { id: data.object.id, object: data.object }
260
+ : doc
261
+ ? { id: fullyQualifiedId(doc), object: doc }
262
+ : {};
305
263
 
306
- case 'card': {
307
- if (
308
- isObject(data.content) &&
309
- typeof data.content.id === 'string' &&
310
- data.content.object instanceof DocumentType
311
- ) {
312
- // isTileComponentProps is a type guard for these props.
313
- // `props` will not pass this guard without transforming `data` into `item`.
314
- const cardProps = {
315
- ...props,
316
- item: {
317
- id: data.content.id,
318
- object: data.content.object,
319
- color: typeof data.content.color === 'string' ? data.content.color : undefined,
320
- } as DocumentItemProps,
321
- };
322
-
323
- return isTileComponentProps(cardProps) ? (
324
- <DocumentCard {...cardProps} settings={settings.values} ref={forwardedRef as Ref<HTMLDivElement>} />
325
- ) : null;
264
+ if (!id || !object) {
265
+ return null;
326
266
  }
327
- break;
267
+
268
+ return (
269
+ <MarkdownContainer
270
+ id={id}
271
+ object={object}
272
+ role={role}
273
+ coordinate={data.coordinate as LayoutCoordinate}
274
+ settings={settings.values}
275
+ extensionProviders={state.values.extensionProviders}
276
+ viewMode={getViewMode(id)}
277
+ onViewModeChange={setViewMode}
278
+ />
279
+ );
328
280
  }
329
281
 
330
282
  case 'settings': {
@@ -0,0 +1,108 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useMemo } from 'react';
6
+
7
+ import { useResolvePlugin, parseFileManagerPlugin } from '@dxos/app-framework';
8
+ import { fullyQualifiedId, getSpace } from '@dxos/react-client/echo';
9
+ import { localStorageStateStoreAdapter, type EditorSelectionState } from '@dxos/react-ui-editor';
10
+
11
+ import { MarkdownEditor, type MarkdownEditorProps } from './MarkdownEditor';
12
+ import { useExtensions } from '../extensions';
13
+ import { DocumentType, type MarkdownSettingsProps } from '../types';
14
+ import { getFallbackName } from '../util';
15
+
16
+ export type MarkdownContainerProps = Pick<
17
+ MarkdownEditorProps,
18
+ 'role' | 'coordinate' | 'extensionProviders' | 'viewMode' | 'onViewModeChange'
19
+ > & {
20
+ id: string;
21
+ object: DocumentType | any;
22
+ settings: MarkdownSettingsProps;
23
+ };
24
+
25
+ // TODO(burdon): Factor out difference for ECHO and non-ECHO objects; i.e., single component.
26
+ const MarkdownContainer = ({ role, id, object, settings, ...props }: MarkdownContainerProps) => {
27
+ const scrollPastEnd = role === 'article';
28
+ if (object instanceof DocumentType) {
29
+ return (
30
+ <DocumentEditor
31
+ id={fullyQualifiedId(object)}
32
+ document={object}
33
+ settings={settings}
34
+ scrollPastEnd={scrollPastEnd}
35
+ {...props}
36
+ />
37
+ );
38
+ } else {
39
+ return (
40
+ <MarkdownEditor
41
+ id={id}
42
+ initialValue={object.text}
43
+ toolbar={settings.toolbar}
44
+ scrollPastEnd={scrollPastEnd}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+ };
50
+
51
+ type DocumentEditorProps = Omit<MarkdownContainerProps, 'object'> & { document: DocumentType } & Pick<
52
+ MarkdownEditorProps,
53
+ 'id' | 'scrollPastEnd'
54
+ >;
55
+
56
+ export const DocumentEditor = ({
57
+ id,
58
+ document: doc,
59
+ extensionProviders,
60
+ settings,
61
+ viewMode,
62
+ ...props
63
+ }: DocumentEditorProps) => {
64
+ const space = getSpace(doc);
65
+ const initialValue = useMemo(() => doc.content?.content, [doc.content]);
66
+ const extensions = useExtensions({ extensionProviders, document: doc, settings, viewMode });
67
+
68
+ // Migrate gradually to `fallbackName`.
69
+ useEffect(() => {
70
+ if (!doc.fallbackName && doc.content?.content) {
71
+ doc.fallbackName = getFallbackName(doc.content.content);
72
+ }
73
+ }, [doc, doc.content]);
74
+
75
+ // Restore last selection and scroll point.
76
+ const { scrollTo, selection } = useMemo<EditorSelectionState>(
77
+ () => localStorageStateStoreAdapter.getState(id) ?? {},
78
+ [id, doc],
79
+ );
80
+
81
+ // File dragging.
82
+ const fileManagerPlugin = useResolvePlugin(parseFileManagerPlugin);
83
+ const handleFileUpload = useMemo(() => {
84
+ if (space === undefined || fileManagerPlugin?.provides.file.upload === undefined) {
85
+ return undefined;
86
+ }
87
+
88
+ // TODO(burdon): Re-order props: space, file.
89
+ return async (file: File) => fileManagerPlugin?.provides?.file?.upload?.(file, space);
90
+ }, [space, fileManagerPlugin]);
91
+
92
+ return (
93
+ <MarkdownEditor
94
+ id={id}
95
+ initialValue={initialValue}
96
+ extensions={extensions}
97
+ scrollTo={scrollTo}
98
+ selection={selection}
99
+ toolbar={settings.toolbar}
100
+ inputMode={settings.editorInputMode}
101
+ viewMode={viewMode}
102
+ onFileUpload={handleFileUpload}
103
+ {...props}
104
+ />
105
+ );
106
+ };
107
+
108
+ export default MarkdownContainer;
@@ -3,13 +3,15 @@
3
3
  //
4
4
 
5
5
  import '@dxos-theme';
6
+
6
7
  import React, { useMemo, type FC } from 'react';
7
8
 
8
9
  import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
9
- import { automerge } from '@dxos/react-ui-editor';
10
+ import { Main } from '@dxos/react-ui';
11
+ import { editorWithToolbarLayout, automerge } from '@dxos/react-ui-editor';
12
+ import { topbarBlockPaddingStart } from '@dxos/react-ui-theme';
10
13
  import { withLayout, withTheme } from '@dxos/storybook-utils';
11
14
 
12
- import { MainLayout } from './Layout';
13
15
  import { MarkdownEditor } from './MarkdownEditor';
14
16
 
15
17
  const Story: FC<{
@@ -20,9 +22,13 @@ const Story: FC<{
20
22
  const extensions = useMemo(() => [automerge(createDocAccessor(doc, ['content']))], [doc]);
21
23
 
22
24
  return (
23
- <MainLayout toolbar={toolbar}>
25
+ <Main.Content
26
+ bounce
27
+ data-toolbar={toolbar ? 'enabled' : 'disabled'}
28
+ classNames={[topbarBlockPaddingStart, editorWithToolbarLayout]}
29
+ >
24
30
  <MarkdownEditor id='test' initialValue={doc.content} extensions={extensions} toolbar={toolbar} />
25
- </MainLayout>
31
+ </Main.Content>
26
32
  );
27
33
  };
28
34
 
@@ -30,8 +36,8 @@ export default {
30
36
  title: 'plugin-markdown/EditorMain',
31
37
  component: MarkdownEditor,
32
38
  decorators: [withTheme, withLayout({ tooltips: true })],
33
- render: Story,
34
39
  parameters: { layout: 'fullscreen' },
40
+ render: Story,
35
41
  };
36
42
 
37
43
  const content = Array.from({ length: 100 })
@@ -3,61 +3,47 @@
3
3
  //
4
4
 
5
5
  import { openSearchPanel } from '@codemirror/search';
6
- import { EditorView } from '@codemirror/view';
6
+ import { type EditorView } from '@codemirror/view';
7
7
  import React, { useMemo, useEffect, useCallback } from 'react';
8
8
 
9
- import {
10
- type FileInfo,
11
- LayoutAction,
12
- type LayoutCoordinate,
13
- useResolvePlugin,
14
- useIntentResolver,
15
- parseLayoutPlugin,
16
- useIntentDispatcher,
17
- } from '@dxos/app-framework';
18
- import { parseAttentionPlugin } from '@dxos/plugin-attention';
9
+ import { type FileInfo, LayoutAction, type LayoutCoordinate, useIntentDispatcher } from '@dxos/app-framework';
19
10
  import { useThemeContext, useTranslation } from '@dxos/react-ui';
11
+ import { useAttendableAttributes, useAttention } from '@dxos/react-ui-attention';
20
12
  import {
21
13
  type Action,
22
14
  type DNDOptions,
23
15
  type EditorViewMode,
24
16
  type EditorInputMode,
25
- type UseTextEditorProps,
26
17
  Toolbar,
18
+ type UseTextEditorProps,
27
19
  createBasicExtensions,
28
20
  createMarkdownExtensions,
29
21
  createThemeExtensions,
30
22
  dropFile,
23
+ editorContent,
24
+ editorGutter,
31
25
  processAction,
32
26
  useActionHandler,
33
27
  useCommentState,
34
28
  useCommentClickListener,
35
29
  useFormattingState,
36
30
  useTextEditor,
37
- editorContent,
38
- editorGutter,
39
- Cursor,
40
- setSelection,
41
31
  } from '@dxos/react-ui-editor';
42
32
  import { sectionToolbarLayout } from '@dxos/react-ui-stack';
43
33
  import { textBlockWidth, focusRing, mx } from '@dxos/react-ui-theme';
44
- import { nonNullable } from '@dxos/util';
34
+ import { isNotFalsy, nonNullable } from '@dxos/util';
45
35
 
36
+ import { useSelectCurrentThread } from '../hooks';
46
37
  import { MARKDOWN_PLUGIN } from '../meta';
47
- import type { MarkdownPluginState } from '../types';
48
-
49
- const attentionFragment = mx(
50
- 'group-focus-within/editor:attention-surface group-[[aria-current]]/editor:attention-surface',
51
- 'group-focus-within/editor:border-separator',
52
- );
38
+ import { type MarkdownPluginState } from '../types';
53
39
 
54
40
  const DEFAULT_VIEW_MODE: EditorViewMode = 'preview';
55
41
 
56
42
  export type MarkdownEditorProps = {
57
43
  id: string;
44
+ role?: string;
58
45
  coordinate?: LayoutCoordinate;
59
46
  inputMode?: EditorInputMode;
60
- role?: string;
61
47
  scrollPastEnd?: boolean;
62
48
  toolbar?: boolean;
63
49
  viewMode?: EditorViewMode;
@@ -66,6 +52,12 @@ export type MarkdownEditorProps = {
66
52
  } & Pick<UseTextEditorProps, 'initialValue' | 'scrollTo' | 'selection' | 'extensions'> &
67
53
  Partial<Pick<MarkdownPluginState, 'extensionProviders'>>;
68
54
 
55
+ /**
56
+ * Base markdown editor component.
57
+ *
58
+ * This component provides all the features of the markdown editor that do no depend on ECHO.
59
+ * This allows it to be used as a common editor for markdown content on arbitrary backends (e.g. files).
60
+ */
69
61
  export const MarkdownEditor = ({
70
62
  id,
71
63
  role = 'article',
@@ -83,52 +75,27 @@ export const MarkdownEditor = ({
83
75
  const { t } = useTranslation(MARKDOWN_PLUGIN);
84
76
  const { themeMode } = useThemeContext();
85
77
  const dispatch = useIntentDispatcher();
86
- const attentionPlugin = useResolvePlugin(parseAttentionPlugin);
87
- const layoutPlugin = useResolvePlugin(parseLayoutPlugin);
88
- const attended = Array.from(attentionPlugin?.provides.attention?.attended ?? []);
89
- const isDirectlyAttended = attended.length === 1 && attended[0] === id;
90
78
  const [formattingState, formattingObserver] = useFormattingState();
79
+ const attendableAttributes = useAttendableAttributes(id);
80
+ const { hasAttention } = useAttention(id);
91
81
 
92
82
  // Extensions from other plugins.
93
- const providerExtensions = useMemo(() => extensionProviders?.map((provider) => provider({})), [extensionProviders]);
83
+ // TODO(burdon): Reconcile with DocumentEditor.useExtensions.
84
+ const providerExtensions = useMemo(
85
+ () => extensionProviders?.flatMap((provider) => provider({})).filter(nonNullable),
86
+ [extensionProviders],
87
+ );
94
88
 
95
89
  // TODO(Zan): Move these into thread plugin as well?
96
90
  const [commentsState, commentObserver] = useCommentState();
97
91
  const onCommentClick = useCallback(() => {
98
- void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
92
+ void dispatch({
93
+ action: LayoutAction.SET_LAYOUT,
94
+ data: { element: 'complementary', state: true },
95
+ });
99
96
  }, [dispatch]);
100
97
  const commentClickObserver = useCommentClickListener(onCommentClick);
101
98
 
102
- // Focus the space that references the comment.
103
- useIntentResolver(MARKDOWN_PLUGIN, ({ action, data }) => {
104
- switch (action) {
105
- // TODO(burdon): Use fully qualified ids everywhere.
106
- case LayoutAction.SCROLL_INTO_VIEW: {
107
- if (editorView && data?.id === id && data?.cursor) {
108
- // TODO(burdon): We need typed intents.
109
- const range = Cursor.getRangeFromCursor(editorView.state, data.cursor);
110
- if (range) {
111
- const selection = editorView.state.selection.main.from !== range.from ? { anchor: range.from } : undefined;
112
- const effects = [
113
- // NOTE: This does not use the DOM scrollIntoView function.
114
- EditorView.scrollIntoView(range.from, { y: 'start', yMargin: 96 }),
115
- ];
116
- if (selection) {
117
- // Update the editor selection to get bi-directional highlighting.
118
- effects.push(setSelection.of({ current: id }));
119
- }
120
-
121
- editorView.dispatch({
122
- effects,
123
- selection: selection ? { anchor: range.from } : undefined,
124
- });
125
- }
126
- }
127
- break;
128
- }
129
- }
130
- });
131
-
132
99
  // Drag files.
133
100
  const handleDrop: DNDOptions['onDrop'] = async (view, { files }) => {
134
101
  const file = files[0];
@@ -161,16 +128,16 @@ export const MarkdownEditor = ({
161
128
  slots: { content: { className: editorContent } },
162
129
  }),
163
130
  editorGutter,
164
- role !== 'section' && onFileUpload ? dropFile({ onDrop: handleDrop }) : [],
131
+ role !== 'section' && onFileUpload && dropFile({ onDrop: handleDrop }),
165
132
  providerExtensions,
166
133
  extensions,
167
- ].filter(nonNullable),
134
+ ].filter(isNotFalsy),
168
135
  ...(role !== 'section' && {
169
136
  id,
170
137
  scrollTo,
171
138
  selection,
172
139
  // TODO(wittjosiah): Autofocus based on layout is racy.
173
- autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
140
+ // autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
174
141
  moveToEndOfLine: true,
175
142
  }),
176
143
  }),
@@ -178,6 +145,7 @@ export const MarkdownEditor = ({
178
145
  );
179
146
 
180
147
  useTest(editorView);
148
+ useSelectCurrentThread(editorView, id);
181
149
 
182
150
  // Toolbar handler.
183
151
  const handleToolbarAction = useActionHandler(editorView);
@@ -205,25 +173,24 @@ export const MarkdownEditor = ({
205
173
  {...(role === 'section'
206
174
  ? { className: 'flex flex-col' }
207
175
  : {
208
- className: 'contents group/editor',
209
- ...(isDirectlyAttended && { 'aria-current': 'location' }),
176
+ className: 'contents',
177
+ // TODO(wittjosiah): Factor out to `useAttendableAttributes`?
178
+ ...(hasAttention && { 'aria-current': 'location' }),
179
+ ...attendableAttributes,
210
180
  })}
211
181
  >
212
182
  {toolbar && (
213
- <div role='none' className={mx('flex shrink-0 justify-center overflow-x-auto', attentionFragment)}>
183
+ <div role='none' className='flex shrink-0 justify-center overflow-x-auto attention-surface'>
214
184
  <Toolbar.Root
215
185
  classNames={
216
186
  role === 'section'
217
187
  ? [
218
188
  textBlockWidth,
219
189
  'z-[2] group-focus-within/section:visible',
220
- !isDirectlyAttended && 'invisible',
190
+ !hasAttention && 'invisible',
221
191
  sectionToolbarLayout,
222
192
  ]
223
- : [
224
- textBlockWidth,
225
- 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
226
- ]
193
+ : [textBlockWidth]
227
194
  }
228
195
  state={formattingState && { ...formattingState, ...commentsState }}
229
196
  onAction={handleAction}
@@ -247,8 +214,8 @@ export const MarkdownEditor = ({
247
214
  : mx(
248
215
  'flex is-full bs-full overflow-hidden',
249
216
  focusRing,
250
- attentionFragment,
251
- 'focus-visible:ring-inset',
217
+ 'focus-visible:ring-inset attention-surface',
218
+ 'p-0.5', // TODO(burdon): Handle padding for focusRing consistently.
252
219
  'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
253
220
  )
254
221
  }
@@ -268,5 +235,3 @@ const useTest = (view?: EditorView) => {
268
235
  }
269
236
  }, [view]);
270
237
  };
271
-
272
- export default MarkdownEditor;
@@ -2,20 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { type LazyExoticComponent } from 'react';
5
+ import React from 'react';
6
6
 
7
- import { type DocumentEditor as DocumentEditorType } from './DocumentEditor';
8
-
9
- export { type DocumentCardProps, type DocumentItemProps } from './DocumentCard';
10
-
11
- export * from './DocumentCard';
12
- export * from './DocumentEditor';
13
- export * from './MarkdownEditor';
14
- export * from './HeadingMenu';
15
- export * from './Layout';
16
7
  export * from './MarkdownSettings';
17
8
 
18
- // Lazily load components for content surfaces.
19
- export const DocumentCard = React.lazy(() => import('./DocumentCard'));
20
- export const DocumentEditor: LazyExoticComponent<DocumentEditorType> = React.lazy(() => import('./DocumentEditor'));
21
- export const MarkdownEditor = React.lazy(() => import('./MarkdownEditor'));
9
+ export const MarkdownContainer = React.lazy(() => import('./MarkdownContainer'));