@dxos/plugin-markdown 0.6.13 → 0.6.14-main.2b6a0f3

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 (124) hide show
  1. package/dist/lib/browser/MarkdownContainer-6VGH47F5.mjs +469 -0
  2. package/dist/lib/browser/MarkdownContainer-6VGH47F5.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-45N5MEOV.mjs +50 -0
  4. package/dist/lib/browser/chunk-45N5MEOV.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 +80 -119
  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-OXS7TME6.cjs +484 -0
  15. package/dist/lib/node/MarkdownContainer-OXS7TME6.cjs.map +7 -0
  16. package/dist/lib/node/{DocumentCard-EHJDDSRY.cjs → chunk-P7YU53RP.cjs} +16 -10
  17. package/dist/lib/node/chunk-P7YU53RP.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-VWQH4WC2.cjs → chunk-UJMOZCIA.cjs} +11 -8
  19. package/dist/lib/node/chunk-UJMOZCIA.cjs.map +7 -0
  20. package/dist/lib/node/chunk-W2YJVZ3N.cjs +72 -0
  21. package/dist/lib/node/chunk-W2YJVZ3N.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +114 -149
  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-IYX45C2U.mjs +470 -0
  30. package/dist/lib/node-esm/MarkdownContainer-IYX45C2U.mjs.map +7 -0
  31. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs +42 -0
  32. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs +17 -0
  34. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-UCNOGIBC.mjs +51 -0
  36. package/dist/lib/node-esm/chunk-UCNOGIBC.mjs.map +7 -0
  37. package/dist/lib/node-esm/index.mjs +494 -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 +4 -14
  48. package/dist/types/src/components/MarkdownEditor.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/Toolbar.stories.d.ts +4 -2
  50. package/dist/types/src/components/Toolbar.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/index.d.ts +1 -11
  52. package/dist/types/src/components/index.d.ts.map +1 -1
  53. package/dist/types/src/extensions.d.ts +11 -15
  54. package/dist/types/src/extensions.d.ts.map +1 -1
  55. package/dist/types/src/hooks/index.d.ts +2 -0
  56. package/dist/types/src/hooks/index.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useSelectCurrentThread.d.ts +6 -0
  58. package/dist/types/src/hooks/useSelectCurrentThread.d.ts.map +1 -0
  59. package/dist/types/src/meta.d.ts +4 -9
  60. package/dist/types/src/meta.d.ts.map +1 -1
  61. package/dist/types/src/types/document.d.ts +10 -1
  62. package/dist/types/src/types/document.d.ts.map +1 -1
  63. package/dist/types/src/types/types.d.ts +8 -9
  64. package/dist/types/src/types/types.d.ts.map +1 -1
  65. package/package.json +42 -36
  66. package/src/MarkdownPlugin.tsx +51 -98
  67. package/src/components/MarkdownContainer.tsx +111 -0
  68. package/src/components/MarkdownEditor.stories.tsx +34 -23
  69. package/src/components/MarkdownEditor.tsx +40 -76
  70. package/src/components/Toolbar.stories.tsx +14 -11
  71. package/src/components/index.ts +2 -14
  72. package/src/extensions.tsx +124 -67
  73. package/src/hooks/index.ts +5 -0
  74. package/src/hooks/useSelectCurrentThread.tsx +46 -0
  75. package/src/meta.ts +15 -0
  76. package/src/types/document.ts +12 -0
  77. package/src/types/types.ts +10 -7
  78. package/src/util.tsx +2 -2
  79. package/dist/lib/browser/DocumentCard-2P4EICBA.mjs +0 -11
  80. package/dist/lib/browser/DocumentEditor-GPWV3VN3.mjs +0 -11
  81. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs +0 -10
  82. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-354DCID5.mjs +0 -117
  84. package/dist/lib/browser/chunk-354DCID5.mjs.map +0 -7
  85. package/dist/lib/browser/chunk-4GGD6YJO.mjs +0 -19
  86. package/dist/lib/browser/chunk-4GGD6YJO.mjs.map +0 -7
  87. package/dist/lib/browser/chunk-7AF2JLK4.mjs +0 -164
  88. package/dist/lib/browser/chunk-7AF2JLK4.mjs.map +0 -7
  89. package/dist/lib/browser/chunk-CQJL4G4X.mjs.map +0 -7
  90. package/dist/lib/browser/chunk-RL7QY322.mjs +0 -86
  91. package/dist/lib/browser/chunk-RL7QY322.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-VUN4QKTT.mjs +0 -208
  93. package/dist/lib/browser/chunk-VUN4QKTT.mjs.map +0 -7
  94. package/dist/lib/node/DocumentCard-EHJDDSRY.cjs.map +0 -7
  95. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs +0 -29
  96. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs.map +0 -7
  97. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs +0 -31
  98. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs.map +0 -7
  99. package/dist/lib/node/chunk-7XIBNEI7.cjs +0 -238
  100. package/dist/lib/node/chunk-7XIBNEI7.cjs.map +0 -7
  101. package/dist/lib/node/chunk-KTYIOXL5.cjs +0 -149
  102. package/dist/lib/node/chunk-KTYIOXL5.cjs.map +0 -7
  103. package/dist/lib/node/chunk-Q4ZSCBQE.cjs +0 -114
  104. package/dist/lib/node/chunk-Q4ZSCBQE.cjs.map +0 -7
  105. package/dist/lib/node/chunk-RVGN72IX.cjs +0 -189
  106. package/dist/lib/node/chunk-RVGN72IX.cjs.map +0 -7
  107. package/dist/lib/node/chunk-TGMR2CKU.cjs +0 -52
  108. package/dist/lib/node/chunk-TGMR2CKU.cjs.map +0 -7
  109. package/dist/lib/node/chunk-VWQH4WC2.cjs.map +0 -7
  110. package/dist/types/src/components/DocumentCard.d.ts +0 -16
  111. package/dist/types/src/components/DocumentCard.d.ts.map +0 -1
  112. package/dist/types/src/components/DocumentEditor.d.ts +0 -14
  113. package/dist/types/src/components/DocumentEditor.d.ts.map +0 -1
  114. package/dist/types/src/components/HeadingMenu.d.ts +0 -13
  115. package/dist/types/src/components/HeadingMenu.d.ts.map +0 -1
  116. package/dist/types/src/components/Layout.d.ts +0 -6
  117. package/dist/types/src/components/Layout.d.ts.map +0 -1
  118. package/src/components/DocumentCard.tsx +0 -107
  119. package/src/components/DocumentEditor.tsx +0 -137
  120. package/src/components/HeadingMenu.tsx +0 -46
  121. package/src/components/Layout.tsx +0 -27
  122. package/src/meta.tsx +0 -19
  123. /package/dist/lib/{browser/DocumentCard-2P4EICBA.mjs.map → node-esm/meta.mjs.map} +0 -0
  124. /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,37 +45,22 @@ 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, {
67
53
  defaultViewMode: 'preview',
68
54
  toolbar: true,
69
- folding: false,
55
+ numberedHeadings: true,
56
+ folding: true,
70
57
  experimental: false,
71
58
  });
72
59
 
73
60
  const state = new LocalStorageStore<MarkdownPluginState>(MARKDOWN_PLUGIN, { extensionProviders: [], viewMode: {} });
74
61
 
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
- };
62
+ const getViewMode = (id: string) => (id && state.values.viewMode[id]) || settings.values.defaultViewMode;
63
+ const setViewMode = (id: string, viewMode: EditorViewMode) => (state.values.viewMode[id] = viewMode);
82
64
 
83
65
  return {
84
66
  meta,
@@ -109,7 +91,7 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
109
91
 
110
92
  markdownExtensionPlugins(plugins).forEach((plugin) => {
111
93
  const { extensions } = plugin.provides.markdown;
112
- state.values.extensionProviders.push(extensions);
94
+ state.values.extensionProviders?.push(extensions);
113
95
  });
114
96
  },
115
97
  provides: {
@@ -117,10 +99,9 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
117
99
  metadata: {
118
100
  records: {
119
101
  [DocumentType.typename]: {
120
- label: (object: any) => (object instanceof DocumentType ? object.name ?? object.fallbackName : undefined),
102
+ label: (object: any) => (object instanceof DocumentType ? object.name || object.fallbackName : undefined),
121
103
  placeholder: ['document title placeholder', { ns: MARKDOWN_PLUGIN }],
122
- icon: (props: IconProps) => <TextAa {...props} />,
123
- iconSymbol: 'ph--text-aa--regular',
104
+ icon: 'ph--text-aa--regular',
124
105
  graphProps: {
125
106
  managesAutofocus: true,
126
107
  },
@@ -134,6 +115,12 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
134
115
  echo: {
135
116
  schema: [DocumentType, TextType],
136
117
  },
118
+ space: {
119
+ onSpaceCreate: {
120
+ label: ['create document label', { ns: MARKDOWN_PLUGIN }],
121
+ action: MarkdownAction.CREATE,
122
+ },
123
+ },
137
124
  graph: {
138
125
  builder: (plugins) => {
139
126
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
@@ -167,8 +154,7 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
167
154
  },
168
155
  properties: {
169
156
  label: ['create document label', { ns: MARKDOWN_PLUGIN }],
170
- icon: (props: IconProps) => <TextAa {...props} />,
171
- iconSymbol: 'ph--text-aa--regular',
157
+ icon: 'ph--text-aa--regular',
172
158
  testId: 'markdownPlugin.createObject',
173
159
  },
174
160
  },
@@ -241,13 +227,10 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
241
227
  ],
242
228
  },
243
229
  thread: {
244
- // TODO(Zan): How to better handle the type predicate?
245
230
  predicate: (obj) => obj instanceof DocumentType,
246
231
  createSort: (doc: DocumentType) => {
247
232
  const accessor = doc.content ? createDocAccessor(doc.content, ['content']) : undefined;
248
-
249
233
  if (!accessor) {
250
- log.warn('No accessor found for document content.');
251
234
  return (_) => 0;
252
235
  }
253
236
 
@@ -256,75 +239,45 @@ export const MarkdownPlugin = (): PluginDefinition<MarkdownPluginProvides> => {
256
239
  return range?.start ?? Number.MAX_SAFE_INTEGER;
257
240
  };
258
241
 
259
- return (anchorA: string, anchorB: string) => getStartPosition(anchorA) - getStartPosition(anchorB);
242
+ return (anchorA: string | undefined, anchorB: string | undefined): number => {
243
+ if (anchorA === undefined || anchorB === undefined) {
244
+ return 0;
245
+ }
246
+ const posA = getStartPosition(anchorA);
247
+ const posB = getStartPosition(anchorB);
248
+ return posA - posB;
249
+ };
260
250
  },
261
251
  },
262
252
  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
-
253
+ component: ({ data, role }) => {
271
254
  switch (role) {
272
255
  case 'section':
273
256
  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
- }
257
+ // TODO(burdon): Normalize types (from FilesPlugin).
258
+ const doc = getDoc(data.active) ?? getDoc(data.object);
259
+ const { id, object } = isEditorModel(data.object)
260
+ ? { id: data.object.id, object: data.object }
261
+ : doc
262
+ ? { id: fullyQualifiedId(doc), object: doc }
263
+ : {};
305
264
 
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;
265
+ if (!id || !object) {
266
+ return null;
326
267
  }
327
- break;
268
+
269
+ return (
270
+ <MarkdownContainer
271
+ id={id}
272
+ object={object}
273
+ role={role}
274
+ coordinate={data.coordinate as LayoutCoordinate}
275
+ settings={settings.values}
276
+ extensionProviders={state.values.extensionProviders}
277
+ viewMode={getViewMode(id)}
278
+ onViewModeChange={setViewMode}
279
+ />
280
+ );
328
281
  }
329
282
 
330
283
  case 'settings': {
@@ -0,0 +1,111 @@
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): Move toolbar here.
26
+ // TODO(burdon): Factor out difference for ECHO and non-ECHO objects; i.e., single component.
27
+ const MarkdownContainer = ({ id, role, object, settings, ...props }: MarkdownContainerProps) => {
28
+ const scrollPastEnd = role === 'article';
29
+ if (object instanceof DocumentType) {
30
+ return (
31
+ <DocumentEditor
32
+ id={fullyQualifiedId(object)}
33
+ role={role}
34
+ document={object}
35
+ settings={settings}
36
+ scrollPastEnd={scrollPastEnd}
37
+ {...props}
38
+ />
39
+ );
40
+ } else {
41
+ return (
42
+ <MarkdownEditor
43
+ id={id}
44
+ role={role}
45
+ initialValue={object.text}
46
+ toolbar={settings.toolbar}
47
+ scrollPastEnd={scrollPastEnd}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ };
53
+
54
+ type DocumentEditorProps = Omit<MarkdownContainerProps, 'object'> & { document: DocumentType } & Pick<
55
+ MarkdownEditorProps,
56
+ 'id' | 'scrollPastEnd'
57
+ >;
58
+
59
+ export const DocumentEditor = ({
60
+ id,
61
+ document: doc,
62
+ extensionProviders,
63
+ settings,
64
+ viewMode,
65
+ ...props
66
+ }: DocumentEditorProps) => {
67
+ const space = getSpace(doc);
68
+ const initialValue = useMemo(() => doc.content?.content, [doc.content]);
69
+ const extensions = useExtensions({ extensionProviders, document: doc, settings, viewMode });
70
+
71
+ // Migrate gradually to `fallbackName`.
72
+ useEffect(() => {
73
+ if (!doc.fallbackName && doc.content?.content) {
74
+ doc.fallbackName = getFallbackName(doc.content.content);
75
+ }
76
+ }, [doc, doc.content]);
77
+
78
+ // Restore last selection and scroll point.
79
+ const { scrollTo, selection } = useMemo<EditorSelectionState>(
80
+ () => localStorageStateStoreAdapter.getState(id) ?? {},
81
+ [id, doc],
82
+ );
83
+
84
+ // File dragging.
85
+ const fileManagerPlugin = useResolvePlugin(parseFileManagerPlugin);
86
+ const handleFileUpload = useMemo(() => {
87
+ if (space === undefined || fileManagerPlugin?.provides.file.upload === undefined) {
88
+ return undefined;
89
+ }
90
+
91
+ // TODO(burdon): Re-order props: space, file.
92
+ return async (file: File) => fileManagerPlugin?.provides?.file?.upload?.(file, space);
93
+ }, [space, fileManagerPlugin]);
94
+
95
+ return (
96
+ <MarkdownEditor
97
+ id={id}
98
+ initialValue={initialValue}
99
+ extensions={extensions}
100
+ scrollTo={scrollTo}
101
+ selection={selection}
102
+ toolbar={settings.toolbar}
103
+ inputMode={settings.editorInputMode}
104
+ viewMode={viewMode}
105
+ onFileUpload={handleFileUpload}
106
+ {...props}
107
+ />
108
+ );
109
+ };
110
+
111
+ export default MarkdownContainer;
@@ -3,41 +3,42 @@
3
3
  //
4
4
 
5
5
  import '@dxos-theme';
6
- import React, { useMemo, type FC } from 'react';
7
6
 
8
- import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
9
- import { automerge } from '@dxos/react-ui-editor';
7
+ import { type Meta } from '@storybook/react';
8
+ import React, { useMemo } from 'react';
9
+
10
+ import { createDocAccessor, createObject } from '@dxos/react-client/echo';
11
+ import { Main } from '@dxos/react-ui';
12
+ import { editorWithToolbarLayout, automerge } from '@dxos/react-ui-editor';
13
+ import { topbarBlockPaddingStart } from '@dxos/react-ui-theme';
10
14
  import { withLayout, withTheme } from '@dxos/storybook-utils';
11
15
 
12
- import { MainLayout } from './Layout';
13
- import { MarkdownEditor } from './MarkdownEditor';
16
+ import { MarkdownEditor, type MarkdownEditorProps } from './MarkdownEditor';
17
+
18
+ const content = Array.from({ length: 100 })
19
+ .map((_, i) => `Line ${i + 1}`)
20
+ .join('\n');
14
21
 
15
- const Story: FC<{
16
- content: string;
22
+ type StoryProps = MarkdownEditorProps & {
23
+ content?: string;
17
24
  toolbar?: boolean;
18
- }> = ({ content = '# Test', toolbar }) => {
19
- const doc = useMemo(() => createEchoObject({ content }), [content]);
25
+ };
26
+
27
+ const DefaultStory = ({ content = '# Test', toolbar }: StoryProps) => {
28
+ const doc = useMemo(() => createObject({ content }), [content]);
20
29
  const extensions = useMemo(() => [automerge(createDocAccessor(doc, ['content']))], [doc]);
21
30
 
22
31
  return (
23
- <MainLayout toolbar={toolbar}>
32
+ <Main.Content
33
+ bounce
34
+ data-toolbar={toolbar ? 'enabled' : 'disabled'}
35
+ classNames={[topbarBlockPaddingStart, editorWithToolbarLayout]}
36
+ >
24
37
  <MarkdownEditor id='test' initialValue={doc.content} extensions={extensions} toolbar={toolbar} />
25
- </MainLayout>
38
+ </Main.Content>
26
39
  );
27
40
  };
28
41
 
29
- export default {
30
- title: 'plugin-markdown/EditorMain',
31
- component: MarkdownEditor,
32
- decorators: [withTheme, withLayout({ tooltips: true })],
33
- render: Story,
34
- parameters: { layout: 'fullscreen' },
35
- };
36
-
37
- const content = Array.from({ length: 100 })
38
- .map((_, i) => `Line ${i + 1}`)
39
- .join('\n');
40
-
41
42
  export const Default = {
42
43
  args: {
43
44
  content,
@@ -50,3 +51,13 @@ export const WithToolbar = {
50
51
  toolbar: true,
51
52
  },
52
53
  };
54
+
55
+ const meta: Meta<typeof MarkdownEditor> = {
56
+ title: 'plugins/plugin-markdown/EditorMain',
57
+ component: MarkdownEditor,
58
+ render: DefaultStory,
59
+ decorators: [withTheme, withLayout({ tooltips: true })],
60
+ parameters: { layout: 'fullscreen' },
61
+ };
62
+
63
+ export default meta;
@@ -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);
@@ -201,29 +169,27 @@ export const MarkdownEditor = ({
201
169
  return (
202
170
  <div
203
171
  role='none'
204
- // TODO(burdon): Move role logic out of here (see sheet, table, sketch, etc.)
205
172
  {...(role === 'section'
206
173
  ? { className: 'flex flex-col' }
207
174
  : {
208
- className: 'contents group/editor',
209
- ...(isDirectlyAttended && { 'aria-current': 'location' }),
175
+ className: 'contents',
176
+ // TODO(wittjosiah): Factor out to `useAttendableAttributes`?
177
+ ...(hasAttention && { 'aria-current': 'location' }),
178
+ ...attendableAttributes,
210
179
  })}
211
180
  >
212
181
  {toolbar && (
213
- <div role='none' className={mx('flex shrink-0 justify-center overflow-x-auto', attentionFragment)}>
182
+ <div role='none' className='flex shrink-0 justify-center overflow-x-auto attention-surface'>
214
183
  <Toolbar.Root
215
184
  classNames={
216
185
  role === 'section'
217
186
  ? [
218
187
  textBlockWidth,
219
188
  'z-[2] group-focus-within/section:visible',
220
- !isDirectlyAttended && 'invisible',
189
+ !hasAttention && 'invisible',
221
190
  sectionToolbarLayout,
222
191
  ]
223
- : [
224
- textBlockWidth,
225
- 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
226
- ]
192
+ : [textBlockWidth]
227
193
  }
228
194
  state={formattingState && { ...formattingState, ...commentsState }}
229
195
  onAction={handleAction}
@@ -247,8 +213,8 @@ export const MarkdownEditor = ({
247
213
  : mx(
248
214
  'flex is-full bs-full overflow-hidden',
249
215
  focusRing,
250
- attentionFragment,
251
- 'focus-visible:ring-inset',
216
+ 'focus-visible:ring-inset attention-surface',
217
+ 'p-0.5', // TODO(burdon): Handle padding for focusRing consistently.
252
218
  'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
253
219
  )
254
220
  }
@@ -268,5 +234,3 @@ const useTest = (view?: EditorView) => {
268
234
  }
269
235
  }, [view]);
270
236
  };
271
-
272
- export default MarkdownEditor;