@dxos/plugin-markdown 0.6.13 → 0.6.14-main.1366248

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 (127) hide show
  1. package/dist/lib/browser/MarkdownContainer-J5BZUIVL.mjs +472 -0
  2. package/dist/lib/browser/MarkdownContainer-J5BZUIVL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-4X6YX3KU.mjs +15 -0
  4. package/dist/lib/browser/chunk-4X6YX3KU.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-PV4AWYWK.mjs +52 -0
  6. package/dist/lib/browser/chunk-PV4AWYWK.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-CQJL4G4X.mjs → chunk-VZAGHNHU.mjs} +4 -2
  8. package/dist/lib/browser/chunk-VZAGHNHU.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +84 -125
  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-UYBYWCRU.cjs +487 -0
  15. package/dist/lib/node/MarkdownContainer-UYBYWCRU.cjs.map +7 -0
  16. package/dist/lib/node/chunk-2A5P424C.cjs +74 -0
  17. package/dist/lib/node/chunk-2A5P424C.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-VWQH4WC2.cjs → chunk-BHPFK7YI.cjs} +11 -8
  19. package/dist/lib/node/chunk-BHPFK7YI.cjs.map +7 -0
  20. package/dist/lib/node/{DocumentCard-EHJDDSRY.cjs → chunk-PHHIPRJC.cjs} +16 -10
  21. package/dist/lib/node/chunk-PHHIPRJC.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +116 -153
  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-P3EAZ3OR.mjs +473 -0
  30. package/dist/lib/node-esm/MarkdownContainer-P3EAZ3OR.mjs.map +7 -0
  31. package/dist/lib/node-esm/chunk-BABK7FMW.mjs +17 -0
  32. package/dist/lib/node-esm/chunk-BABK7FMW.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-EREAR7QS.mjs +53 -0
  34. package/dist/lib/node-esm/chunk-EREAR7QS.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-OEMU3XY7.mjs +42 -0
  36. package/dist/lib/node-esm/chunk-OEMU3XY7.mjs.map +7 -0
  37. package/dist/lib/node-esm/index.mjs +492 -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 +13 -8
  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 -14
  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/dist/types/src/util.d.ts.map +1 -1
  66. package/package.json +42 -45
  67. package/src/MarkdownPlugin.tsx +56 -114
  68. package/src/components/MarkdownContainer.tsx +109 -0
  69. package/src/components/MarkdownEditor.stories.tsx +34 -23
  70. package/src/components/MarkdownEditor.tsx +48 -80
  71. package/src/components/MarkdownSettings.tsx +15 -15
  72. package/src/components/Toolbar.stories.tsx +14 -11
  73. package/src/components/index.ts +2 -14
  74. package/src/extensions.tsx +139 -66
  75. package/src/hooks/index.ts +5 -0
  76. package/src/hooks/useSelectCurrentThread.tsx +46 -0
  77. package/src/meta.ts +15 -0
  78. package/src/translations.ts +1 -1
  79. package/src/types/document.ts +12 -0
  80. package/src/types/types.ts +10 -7
  81. package/src/util.tsx +6 -4
  82. package/dist/lib/browser/DocumentCard-2P4EICBA.mjs +0 -11
  83. package/dist/lib/browser/DocumentEditor-GPWV3VN3.mjs +0 -11
  84. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs +0 -10
  85. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs.map +0 -7
  86. package/dist/lib/browser/chunk-354DCID5.mjs +0 -117
  87. package/dist/lib/browser/chunk-354DCID5.mjs.map +0 -7
  88. package/dist/lib/browser/chunk-4GGD6YJO.mjs +0 -19
  89. package/dist/lib/browser/chunk-4GGD6YJO.mjs.map +0 -7
  90. package/dist/lib/browser/chunk-7AF2JLK4.mjs +0 -164
  91. package/dist/lib/browser/chunk-7AF2JLK4.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-CQJL4G4X.mjs.map +0 -7
  93. package/dist/lib/browser/chunk-RL7QY322.mjs +0 -86
  94. package/dist/lib/browser/chunk-RL7QY322.mjs.map +0 -7
  95. package/dist/lib/browser/chunk-VUN4QKTT.mjs +0 -208
  96. package/dist/lib/browser/chunk-VUN4QKTT.mjs.map +0 -7
  97. package/dist/lib/node/DocumentCard-EHJDDSRY.cjs.map +0 -7
  98. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs +0 -29
  99. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs.map +0 -7
  100. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs +0 -31
  101. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs.map +0 -7
  102. package/dist/lib/node/chunk-7XIBNEI7.cjs +0 -238
  103. package/dist/lib/node/chunk-7XIBNEI7.cjs.map +0 -7
  104. package/dist/lib/node/chunk-KTYIOXL5.cjs +0 -149
  105. package/dist/lib/node/chunk-KTYIOXL5.cjs.map +0 -7
  106. package/dist/lib/node/chunk-Q4ZSCBQE.cjs +0 -114
  107. package/dist/lib/node/chunk-Q4ZSCBQE.cjs.map +0 -7
  108. package/dist/lib/node/chunk-RVGN72IX.cjs +0 -189
  109. package/dist/lib/node/chunk-RVGN72IX.cjs.map +0 -7
  110. package/dist/lib/node/chunk-TGMR2CKU.cjs +0 -52
  111. package/dist/lib/node/chunk-TGMR2CKU.cjs.map +0 -7
  112. package/dist/lib/node/chunk-VWQH4WC2.cjs.map +0 -7
  113. package/dist/types/src/components/DocumentCard.d.ts +0 -16
  114. package/dist/types/src/components/DocumentCard.d.ts.map +0 -1
  115. package/dist/types/src/components/DocumentEditor.d.ts +0 -14
  116. package/dist/types/src/components/DocumentEditor.d.ts.map +0 -1
  117. package/dist/types/src/components/HeadingMenu.d.ts +0 -13
  118. package/dist/types/src/components/HeadingMenu.d.ts.map +0 -1
  119. package/dist/types/src/components/Layout.d.ts +0 -6
  120. package/dist/types/src/components/Layout.d.ts.map +0 -1
  121. package/src/components/DocumentCard.tsx +0 -107
  122. package/src/components/DocumentEditor.tsx +0 -137
  123. package/src/components/HeadingMenu.tsx +0 -46
  124. package/src/components/Layout.tsx +0 -27
  125. package/src/meta.tsx +0 -19
  126. /package/dist/lib/{browser/DocumentCard-2P4EICBA.mjs.map → node-esm/meta.mjs.map} +0 -0
  127. /package/dist/lib/{browser/DocumentEditor-GPWV3VN3.mjs.map → node-esm/types/index.mjs.map} +0 -0
@@ -3,69 +3,63 @@
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, 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,
17
+ type EditorSelectionState,
18
+ type EditorStateStore,
26
19
  Toolbar,
20
+ type UseTextEditorProps,
27
21
  createBasicExtensions,
28
22
  createMarkdownExtensions,
29
23
  createThemeExtensions,
30
24
  dropFile,
25
+ editorContent,
26
+ editorGutter,
31
27
  processAction,
32
28
  useActionHandler,
33
29
  useCommentState,
34
30
  useCommentClickListener,
35
31
  useFormattingState,
36
32
  useTextEditor,
37
- editorContent,
38
- editorGutter,
39
- Cursor,
40
- setSelection,
41
33
  } from '@dxos/react-ui-editor';
42
34
  import { sectionToolbarLayout } from '@dxos/react-ui-stack';
43
35
  import { textBlockWidth, focusRing, mx } from '@dxos/react-ui-theme';
44
- import { nonNullable } from '@dxos/util';
36
+ import { isNotFalsy, nonNullable } from '@dxos/util';
45
37
 
38
+ import { useSelectCurrentThread } from '../hooks';
46
39
  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
- );
40
+ import { type MarkdownPluginState } from '../types';
53
41
 
54
42
  const DEFAULT_VIEW_MODE: EditorViewMode = 'preview';
55
43
 
56
44
  export type MarkdownEditorProps = {
57
45
  id: string;
58
- coordinate?: LayoutCoordinate;
59
- inputMode?: EditorInputMode;
60
46
  role?: string;
47
+ inputMode?: EditorInputMode;
61
48
  scrollPastEnd?: boolean;
62
49
  toolbar?: boolean;
63
50
  viewMode?: EditorViewMode;
51
+ editorStateStore?: EditorStateStore;
64
52
  onViewModeChange?: (id: string, mode: EditorViewMode) => void;
65
53
  onFileUpload?: (file: File) => Promise<FileInfo | undefined>;
66
- } & Pick<UseTextEditorProps, 'initialValue' | 'scrollTo' | 'selection' | 'extensions'> &
54
+ } & Pick<UseTextEditorProps, 'initialValue' | 'extensions'> &
67
55
  Partial<Pick<MarkdownPluginState, 'extensionProviders'>>;
68
56
 
57
+ /**
58
+ * Base markdown editor component.
59
+ *
60
+ * This component provides all the features of the markdown editor that do no depend on ECHO.
61
+ * This allows it to be used as a common editor for markdown content on arbitrary backends (e.g. files).
62
+ */
69
63
  export const MarkdownEditor = ({
70
64
  id,
71
65
  role = 'article',
@@ -73,62 +67,39 @@ export const MarkdownEditor = ({
73
67
  extensions,
74
68
  extensionProviders,
75
69
  scrollPastEnd,
76
- scrollTo,
77
- selection,
78
70
  toolbar,
79
71
  viewMode,
72
+ editorStateStore,
80
73
  onFileUpload,
81
74
  onViewModeChange,
82
75
  }: MarkdownEditorProps) => {
83
76
  const { t } = useTranslation(MARKDOWN_PLUGIN);
84
77
  const { themeMode } = useThemeContext();
85
78
  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
79
  const [formattingState, formattingObserver] = useFormattingState();
80
+ const attendableAttributes = useAttendableAttributes(id);
81
+ const { hasAttention } = useAttention(id);
82
+
83
+ // Restore last selection and scroll point.
84
+ const { scrollTo, selection } = useMemo<EditorSelectionState>(() => editorStateStore?.getState(id) ?? {}, [id]);
91
85
 
92
86
  // Extensions from other plugins.
93
- const providerExtensions = useMemo(() => extensionProviders?.map((provider) => provider({})), [extensionProviders]);
87
+ // TODO(burdon): Reconcile with DocumentEditor.useExtensions.
88
+ const providerExtensions = useMemo(
89
+ () => extensionProviders?.flatMap((provider) => provider({})).filter(nonNullable),
90
+ [extensionProviders],
91
+ );
94
92
 
95
93
  // TODO(Zan): Move these into thread plugin as well?
96
94
  const [commentsState, commentObserver] = useCommentState();
97
95
  const onCommentClick = useCallback(() => {
98
- void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
96
+ void dispatch({
97
+ action: LayoutAction.SET_LAYOUT,
98
+ data: { element: 'complementary', state: true },
99
+ });
99
100
  }, [dispatch]);
100
101
  const commentClickObserver = useCommentClickListener(onCommentClick);
101
102
 
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
103
  // Drag files.
133
104
  const handleDrop: DNDOptions['onDrop'] = async (view, { files }) => {
134
105
  const file = files[0];
@@ -161,16 +132,16 @@ export const MarkdownEditor = ({
161
132
  slots: { content: { className: editorContent } },
162
133
  }),
163
134
  editorGutter,
164
- role !== 'section' && onFileUpload ? dropFile({ onDrop: handleDrop }) : [],
135
+ role !== 'section' && onFileUpload && dropFile({ onDrop: handleDrop }),
165
136
  providerExtensions,
166
137
  extensions,
167
- ].filter(nonNullable),
138
+ ].filter(isNotFalsy),
168
139
  ...(role !== 'section' && {
169
140
  id,
170
141
  scrollTo,
171
142
  selection,
172
143
  // TODO(wittjosiah): Autofocus based on layout is racy.
173
- autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
144
+ // autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
174
145
  moveToEndOfLine: true,
175
146
  }),
176
147
  }),
@@ -178,6 +149,7 @@ export const MarkdownEditor = ({
178
149
  );
179
150
 
180
151
  useTest(editorView);
152
+ useSelectCurrentThread(editorView, id);
181
153
 
182
154
  // Toolbar handler.
183
155
  const handleToolbarAction = useActionHandler(editorView);
@@ -201,29 +173,27 @@ export const MarkdownEditor = ({
201
173
  return (
202
174
  <div
203
175
  role='none'
204
- // TODO(burdon): Move role logic out of here (see sheet, table, sketch, etc.)
205
176
  {...(role === 'section'
206
177
  ? { className: 'flex flex-col' }
207
178
  : {
208
- className: 'contents group/editor',
209
- ...(isDirectlyAttended && { 'aria-current': 'location' }),
179
+ className: 'contents',
180
+ // TODO(wittjosiah): Factor out to `useAttendableAttributes`?
181
+ ...(hasAttention && { 'aria-current': 'location' }),
182
+ ...attendableAttributes,
210
183
  })}
211
184
  >
212
185
  {toolbar && (
213
- <div role='none' className={mx('flex shrink-0 justify-center overflow-x-auto', attentionFragment)}>
186
+ <div role='none' className='flex shrink-0 justify-center overflow-x-auto attention-surface'>
214
187
  <Toolbar.Root
215
188
  classNames={
216
189
  role === 'section'
217
190
  ? [
218
191
  textBlockWidth,
219
192
  'z-[2] group-focus-within/section:visible',
220
- !isDirectlyAttended && 'invisible',
193
+ !hasAttention && 'invisible',
221
194
  sectionToolbarLayout,
222
195
  ]
223
- : [
224
- textBlockWidth,
225
- 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
226
- ]
196
+ : [textBlockWidth]
227
197
  }
228
198
  state={formattingState && { ...formattingState, ...commentsState }}
229
199
  onAction={handleAction}
@@ -247,8 +217,8 @@ export const MarkdownEditor = ({
247
217
  : mx(
248
218
  'flex is-full bs-full overflow-hidden',
249
219
  focusRing,
250
- attentionFragment,
251
- 'focus-visible:ring-inset',
220
+ 'focus-visible:ring-inset attention-surface',
221
+ 'p-0.5', // TODO(burdon): Handle padding for focusRing consistently.
252
222
  'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
253
223
  )
254
224
  }
@@ -268,5 +238,3 @@ const useTest = (view?: EditorView) => {
268
238
  }
269
239
  }, [view]);
270
240
  };
271
-
272
- export default MarkdownEditor;
@@ -4,8 +4,8 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { SettingsValue } from '@dxos/plugin-settings';
8
7
  import { Input, Select, useTranslation } from '@dxos/react-ui';
8
+ import { DeprecatedFormInput } from '@dxos/react-ui-data';
9
9
  import { type EditorInputMode, EditorInputModes, type EditorViewMode, EditorViewModes } from '@dxos/react-ui-editor';
10
10
 
11
11
  import { MARKDOWN_PLUGIN } from '../meta';
@@ -17,7 +17,7 @@ export const MarkdownSettings = ({ settings }: { settings: MarkdownSettingsProps
17
17
  // TODO(wittjosiah): Add skill test confirmation for entering vim mode.
18
18
  return (
19
19
  <>
20
- <SettingsValue label={t('default view mode label')}>
20
+ <DeprecatedFormInput label={t('default view mode label')}>
21
21
  <Select.Root
22
22
  value={settings.defaultViewMode}
23
23
  onValueChange={(value) => {
@@ -37,9 +37,9 @@ export const MarkdownSettings = ({ settings }: { settings: MarkdownSettingsProps
37
37
  </Select.Content>
38
38
  </Select.Portal>
39
39
  </Select.Root>
40
- </SettingsValue>
40
+ </DeprecatedFormInput>
41
41
 
42
- <SettingsValue label={t('editor input mode label')}>
42
+ <DeprecatedFormInput label={t('editor input mode label')}>
43
43
  <Select.Root
44
44
  value={settings.editorInputMode ?? 'default'}
45
45
  onValueChange={(value) => {
@@ -59,31 +59,31 @@ export const MarkdownSettings = ({ settings }: { settings: MarkdownSettingsProps
59
59
  </Select.Content>
60
60
  </Select.Portal>
61
61
  </Select.Root>
62
- </SettingsValue>
62
+ </DeprecatedFormInput>
63
63
 
64
- <SettingsValue label={t('settings toolbar label')}>
64
+ <DeprecatedFormInput label={t('settings toolbar label')}>
65
65
  <Input.Switch checked={settings.toolbar} onCheckedChange={(checked) => (settings.toolbar = !!checked)} />
66
- </SettingsValue>
66
+ </DeprecatedFormInput>
67
67
 
68
- <SettingsValue label={t('settings numbered headings label')}>
68
+ <DeprecatedFormInput label={t('settings numbered headings label')}>
69
69
  <Input.Switch
70
70
  checked={settings.numberedHeadings}
71
71
  onCheckedChange={(checked) => (settings.numberedHeadings = !!checked)}
72
72
  />
73
- </SettingsValue>
73
+ </DeprecatedFormInput>
74
74
 
75
- <SettingsValue label={t('settings folding label')}>
75
+ <DeprecatedFormInput label={t('settings folding label')}>
76
76
  <Input.Switch checked={settings.folding} onCheckedChange={(checked) => (settings.folding = !!checked)} />
77
- </SettingsValue>
77
+ </DeprecatedFormInput>
78
78
 
79
- <SettingsValue label={t('settings experimental label')}>
79
+ <DeprecatedFormInput label={t('settings experimental label')}>
80
80
  <Input.Switch
81
81
  checked={settings.experimental}
82
82
  onCheckedChange={(checked) => (settings.experimental = !!checked)}
83
83
  />
84
- </SettingsValue>
84
+ </DeprecatedFormInput>
85
85
 
86
- <SettingsValue
86
+ <DeprecatedFormInput
87
87
  label={t('settings debug label')}
88
88
  secondary={
89
89
  settings.debug ? (
@@ -99,7 +99,7 @@ export const MarkdownSettings = ({ settings }: { settings: MarkdownSettingsProps
99
99
  }
100
100
  >
101
101
  <Input.Switch checked={settings.debug} onCheckedChange={(checked) => (settings.debug = !!checked)} />
102
- </SettingsValue>
102
+ </DeprecatedFormInput>
103
103
  </>
104
104
  );
105
105
  };
@@ -4,12 +4,13 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { type Meta } from '@storybook/react';
7
8
  import React, { type FC, useState } from 'react';
8
9
 
9
10
  import { create } from '@dxos/echo-schema';
10
11
  import { PublicKey } from '@dxos/keys';
11
12
  import { faker } from '@dxos/random';
12
- import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
13
+ import { createDocAccessor, createObject } from '@dxos/react-client/echo';
13
14
  import { useThemeContext } from '@dxos/react-ui';
14
15
  import {
15
16
  type Action,
@@ -37,9 +38,9 @@ import { TextType } from '../types';
37
38
 
38
39
  faker.seed(101);
39
40
 
40
- const Story: FC<{ content: string }> = ({ content }) => {
41
+ const DefaultStory: FC<{ content?: string }> = ({ content = '' }) => {
41
42
  const { themeMode } = useThemeContext();
42
- const [text] = useState(createEchoObject(create(TextType, { content })));
43
+ const [text] = useState(createObject(create(TextType, { content })));
43
44
  const [formattingState, formattingObserver] = useFormattingState();
44
45
  const [viewMode, setViewMode] = useState<EditorViewMode>('preview');
45
46
  const { parentRef, view } = useTextEditor(() => {
@@ -91,14 +92,6 @@ const Story: FC<{ content: string }> = ({ content }) => {
91
92
  );
92
93
  };
93
94
 
94
- export default {
95
- title: 'react-ui-editor/Toolbar',
96
- component: Toolbar,
97
- decorators: [withTheme, withLayout({ tooltips: true })],
98
- parameters: { translations, layout: 'fullscreen' },
99
- render: (args: any) => <Story {...args} />,
100
- } as any;
101
-
102
95
  const content = [
103
96
  '# Demo',
104
97
  '',
@@ -114,3 +107,13 @@ export const Default = {
114
107
  content,
115
108
  },
116
109
  };
110
+
111
+ const meta: Meta<typeof Toolbar.Root> = {
112
+ title: 'plugins/plugin-markdown/Toolbar',
113
+ component: Toolbar.Root,
114
+ render: DefaultStory as any,
115
+ decorators: [withTheme, withLayout({ tooltips: true })],
116
+ parameters: { translations, layout: 'fullscreen' },
117
+ };
118
+
119
+ export default meta;
@@ -2,20 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { type LazyExoticComponent } from 'react';
5
+ import { lazy } 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 = lazy(() => import('./MarkdownContainer'));
@@ -2,63 +2,133 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { ArrowSquareDown, ArrowSquareOut, type Icon } from '@phosphor-icons/react';
6
- import React, { type AnchorHTMLAttributes, StrictMode } from 'react';
5
+ import React, { type AnchorHTMLAttributes, type ReactNode, useMemo } from 'react';
7
6
  import { createRoot } from 'react-dom/client';
8
7
 
9
- import { type IntentDispatcher, NavigationAction } from '@dxos/app-framework';
8
+ import { type IntentDispatcher, NavigationAction, useIntentDispatcher } from '@dxos/app-framework';
10
9
  import { invariant } from '@dxos/invariant';
11
- import { fullyQualifiedId, type Query } from '@dxos/react-client/echo';
10
+ import { createDocAccessor, fullyQualifiedId, getSpace, type Query } from '@dxos/react-client/echo';
11
+ import { useIdentity } from '@dxos/react-client/halo';
12
+ import { Icon, ThemeProvider } from '@dxos/react-ui';
12
13
  import {
13
14
  type AutocompleteResult,
14
- type Extension,
15
+ type EditorStateStore,
15
16
  type EditorViewMode,
17
+ type Extension,
18
+ InputModeExtensions,
19
+ createDataExtensions,
16
20
  autocomplete,
17
21
  decorateMarkdown,
22
+ folding,
23
+ formattingKeymap,
18
24
  linkTooltip,
25
+ listener,
26
+ selectionState,
19
27
  typewriter,
20
- formattingKeymap,
21
- InputModeExtensions,
22
- folding,
23
28
  } from '@dxos/react-ui-editor';
24
- import { getSize, mx } from '@dxos/react-ui-theme';
25
- import { isNotFalsy, nonNullable } from '@dxos/util';
29
+ import { defaultTx } from '@dxos/react-ui-theme';
30
+ import { isNotFalsy } from '@dxos/util';
26
31
 
27
- import { type DocumentType, type MarkdownSettingsProps } from './types';
32
+ import { type DocumentType, type MarkdownPluginState, type MarkdownSettingsProps } from './types';
33
+ import { setFallbackName } from './util';
28
34
 
29
- export type ExtensionsOptions = {
30
- viewMode?: EditorViewMode;
31
- settings?: MarkdownSettingsProps;
35
+ type ExtensionsOptions = {
32
36
  document?: DocumentType;
33
- debug?: boolean;
34
- experimental?: boolean;
35
- numberedHeadings?: boolean;
36
- folding?: boolean;
37
- query?: Query<DocumentType>;
38
37
  dispatch?: IntentDispatcher;
38
+ query?: Query<DocumentType>;
39
+ settings: MarkdownSettingsProps;
40
+ viewMode?: EditorViewMode;
41
+ editorStateStore?: EditorStateStore;
39
42
  };
40
43
 
41
- /**
42
- * Create extension instances for editor.
43
- */
44
- export const createBaseExtensions = ({
45
- viewMode,
46
- settings,
44
+ // TODO(burdon): Merge with createBaseExtensions below.
45
+ export const useExtensions = ({
47
46
  document,
48
- query,
49
- dispatch,
50
- }: ExtensionsOptions): Extension[] => {
51
- const extensions: Extension[] = [];
47
+ settings,
48
+ viewMode,
49
+ editorStateStore,
50
+ extensionProviders,
51
+ }: ExtensionsOptions & Pick<MarkdownPluginState, 'extensionProviders'>): Extension[] => {
52
+ const dispatch = useIntentDispatcher();
53
+ const identity = useIdentity();
54
+ const space = getSpace(document);
55
+
56
+ // TODO(wittjosiah): Autocomplete is not working and this query is causing performance issues.
57
+ // TODO(burdon): Unsubscribe.
58
+ // const query = space?.db.query(Filter.schema(DocumentType));
59
+ // query?.subscribe();
60
+ const baseExtensions = useMemo(
61
+ () =>
62
+ createBaseExtensions({
63
+ document,
64
+ settings,
65
+ viewMode,
66
+ dispatch,
67
+ // query,
68
+ }),
69
+ [
70
+ document,
71
+ viewMode,
72
+ dispatch,
73
+ settings,
74
+ settings.editorInputMode,
75
+ settings.folding,
76
+ settings.numberedHeadings,
77
+ settings.debug,
78
+ settings.typewriter,
79
+ ],
80
+ );
52
81
 
53
82
  //
54
- // Editor mode.
83
+ // External extensions from other plugins.
55
84
  //
56
- if (settings?.editorInputMode) {
57
- const extension = InputModeExtensions[settings.editorInputMode];
58
- if (extension) {
59
- extensions.push(extension);
60
- }
61
- }
85
+ const pluginExtensions = useMemo<Extension[] | undefined>(
86
+ () =>
87
+ extensionProviders?.reduce((acc: Extension[], provider) => {
88
+ const extension = typeof provider === 'function' ? provider({ document }) : provider;
89
+ if (extension) {
90
+ acc.push(extension);
91
+ }
92
+
93
+ return acc;
94
+ }, []),
95
+ [extensionProviders],
96
+ );
97
+
98
+ //
99
+ // Basic plugins.
100
+ //
101
+ return useMemo<Extension[]>(
102
+ () =>
103
+ [
104
+ // NOTE: Data extensions must be first so that automerge is updated before other extensions compute their state.
105
+ document &&
106
+ createDataExtensions({
107
+ id: document.id,
108
+ text: document.content && createDocAccessor(document.content, ['content']),
109
+ space,
110
+ identity,
111
+ }),
112
+ selectionState(editorStateStore),
113
+ document &&
114
+ listener({
115
+ onChange: (text) => setFallbackName(document, text),
116
+ }),
117
+ baseExtensions,
118
+ pluginExtensions,
119
+ ].filter(isNotFalsy),
120
+ [baseExtensions, pluginExtensions, document, document?.content, space, identity],
121
+ );
122
+ };
123
+
124
+ /**
125
+ * Create extension instances for editor.
126
+ */
127
+ const createBaseExtensions = ({ document, dispatch, settings, query, viewMode }: ExtensionsOptions): Extension[] => {
128
+ const extensions: Extension[] = [
129
+ settings.editorInputMode && InputModeExtensions[settings.editorInputMode],
130
+ settings.folding && folding(),
131
+ ].filter(isNotFalsy);
62
132
 
63
133
  //
64
134
  // Markdown
@@ -69,7 +139,7 @@ export const createBaseExtensions = ({
69
139
  formattingKeymap(),
70
140
  decorateMarkdown({
71
141
  selectionChangeDelay: 100,
72
- numberedHeadings: settings?.numberedHeadings ? { from: 2 } : undefined,
142
+ numberedHeadings: settings.numberedHeadings ? { from: 2 } : undefined,
73
143
  // TODO(wittjosiah): For internal links, consider ignoring the link text and rendering the label of the object being linked to.
74
144
  renderLinkButton:
75
145
  dispatch && document
@@ -98,7 +168,6 @@ export const createBaseExtensions = ({
98
168
  extensions.push(
99
169
  autocomplete({
100
170
  onSearch: (text: string) => {
101
- // TODO query
102
171
  // TODO(burdon): Specify filter (e.g., stack).
103
172
  return query.objects
104
173
  .map<AutocompleteResult | undefined>((object) =>
@@ -110,29 +179,27 @@ export const createBaseExtensions = ({
110
179
  }
111
180
  : undefined,
112
181
  )
113
- .filter(nonNullable);
182
+ .filter(isNotFalsy);
114
183
  },
115
184
  }),
116
185
  );
117
186
  }
118
187
 
119
- extensions.push(
120
- ...[
121
- //
122
- settings?.folding && folding(),
123
- ].filter(isNotFalsy),
124
- );
125
-
126
- if (settings?.debug) {
127
- const items = settings.typewriter ?? '';
128
- extensions.push(...[items ? typewriter({ items: items.split(/[,\n]/) }) : undefined].filter(nonNullable));
188
+ if (settings.debug) {
189
+ const items = settings.typewriter?.split(/[,\n]/) ?? '';
190
+ if (items) {
191
+ extensions.push(typewriter({ items }));
192
+ }
129
193
  }
130
194
 
131
195
  return extensions;
132
196
  };
133
197
 
134
- // TODO(burdon): Factor out style.
135
- const hover = 'rounded-sm text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
198
+ // TODO(burdon): Factor out styles.
199
+ const style = {
200
+ hover: 'rounded-sm text-primary-500 hover:text-primary-600 dark:text-primary-500 hover:dark:text-primary-400',
201
+ icon: 'inline-block leading-none mis-1 cursor-pointer',
202
+ };
136
203
 
137
204
  const onRenderLink = (onSelectObject: (id: string) => void) => (el: Element, url: string) => {
138
205
  // TODO(burdon): Formalize/document internal link format.
@@ -155,25 +222,31 @@ const onRenderLink = (onSelectObject: (id: string) => void) => (el: Element, url
155
222
  target: '_blank',
156
223
  };
157
224
 
158
- const LinkIcon: Icon = isInternal ? ArrowSquareDown : ArrowSquareOut;
159
-
160
- createRoot(el).render(
161
- <StrictMode>
162
- <a {...options} className={hover}>
163
- <LinkIcon weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 cursor-pointer')} />
164
- </a>
165
- </StrictMode>,
225
+ renderRoot(
226
+ el,
227
+ <a {...options} className={style.hover}>
228
+ <Icon
229
+ icon={isInternal ? 'ph--arrow-square-down--bold' : 'ph--arrow-square-out--bold'}
230
+ size={4}
231
+ classNames={style.icon}
232
+ />
233
+ </a>,
166
234
  );
167
235
  };
168
236
 
169
237
  const renderLinkTooltip = (el: Element, url: string) => {
170
238
  const web = new URL(url);
171
- createRoot(el).render(
172
- <StrictMode>
173
- <a href={url} target='_blank' rel='noreferrer' className={hover}>
174
- {web.origin}
175
- <ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 cursor-pointer')} />
176
- </a>
177
- </StrictMode>,
239
+ renderRoot(
240
+ el,
241
+ <a href={url} rel='noreferrer' target='_blank' className={style.hover}>
242
+ {web.origin}
243
+ <Icon icon='ph--arrow-square-out--bold' size={4} classNames={style.icon} />
244
+ </a>,
178
245
  );
179
246
  };
247
+
248
+ // TODO(burdon): Remove react rendering; use DOM directly.
249
+ export const renderRoot = <T extends Element>(root: T, node: ReactNode): T => {
250
+ createRoot(root).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
251
+ return root;
252
+ };