@dxos/plugin-markdown 0.6.13 → 0.6.14-main.69511f5

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-HRGXWEA4.mjs +465 -0
  2. package/dist/lib/browser/MarkdownContainer-HRGXWEA4.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-DRJ3FPYF.mjs +15 -0
  4. package/dist/lib/browser/chunk-DRJ3FPYF.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-CQJL4G4X.mjs → chunk-US5O2P3R.mjs} +4 -2
  6. package/dist/lib/browser/chunk-US5O2P3R.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-VGIHBUXB.mjs +52 -0
  8. package/dist/lib/browser/chunk-VGIHBUXB.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +85 -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-QZ4YLO7M.cjs +480 -0
  15. package/dist/lib/node/MarkdownContainer-QZ4YLO7M.cjs.map +7 -0
  16. package/dist/lib/node/chunk-6HPTH2F5.cjs +74 -0
  17. package/dist/lib/node/chunk-6HPTH2F5.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 +117 -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-FSWQL76V.mjs +466 -0
  30. package/dist/lib/node-esm/MarkdownContainer-FSWQL76V.mjs.map +7 -0
  31. package/dist/lib/node-esm/chunk-HPOTHJC4.mjs +53 -0
  32. package/dist/lib/node-esm/chunk-HPOTHJC4.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 +12 -6
  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 +12 -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/dist/types/src/util.d.ts.map +1 -1
  66. package/package.json +42 -45
  67. package/src/MarkdownPlugin.tsx +58 -113
  68. package/src/components/MarkdownContainer.tsx +103 -0
  69. package/src/components/MarkdownEditor.stories.tsx +34 -23
  70. package/src/components/MarkdownEditor.tsx +48 -79
  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 +128 -67
  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,64 @@
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,
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;
46
+ role?: string;
58
47
  coordinate?: LayoutCoordinate;
59
48
  inputMode?: EditorInputMode;
60
- role?: string;
61
49
  scrollPastEnd?: boolean;
62
50
  toolbar?: boolean;
63
51
  viewMode?: EditorViewMode;
52
+ editorStateStore?: EditorStateStore;
64
53
  onViewModeChange?: (id: string, mode: EditorViewMode) => void;
65
54
  onFileUpload?: (file: File) => Promise<FileInfo | undefined>;
66
- } & Pick<UseTextEditorProps, 'initialValue' | 'scrollTo' | 'selection' | 'extensions'> &
55
+ } & Pick<UseTextEditorProps, 'initialValue' | 'extensions'> &
67
56
  Partial<Pick<MarkdownPluginState, 'extensionProviders'>>;
68
57
 
58
+ /**
59
+ * Base markdown editor component.
60
+ *
61
+ * This component provides all the features of the markdown editor that do no depend on ECHO.
62
+ * This allows it to be used as a common editor for markdown content on arbitrary backends (e.g. files).
63
+ */
69
64
  export const MarkdownEditor = ({
70
65
  id,
71
66
  role = 'article',
@@ -73,62 +68,39 @@ export const MarkdownEditor = ({
73
68
  extensions,
74
69
  extensionProviders,
75
70
  scrollPastEnd,
76
- scrollTo,
77
- selection,
78
71
  toolbar,
79
72
  viewMode,
73
+ editorStateStore,
80
74
  onFileUpload,
81
75
  onViewModeChange,
82
76
  }: MarkdownEditorProps) => {
83
77
  const { t } = useTranslation(MARKDOWN_PLUGIN);
84
78
  const { themeMode } = useThemeContext();
85
79
  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
80
  const [formattingState, formattingObserver] = useFormattingState();
81
+ const attendableAttributes = useAttendableAttributes(id);
82
+ const { hasAttention } = useAttention(id);
83
+
84
+ // Restore last selection and scroll point.
85
+ const { scrollTo, selection } = useMemo<EditorSelectionState>(() => editorStateStore?.getState(id) ?? {}, [id]);
91
86
 
92
87
  // Extensions from other plugins.
93
- const providerExtensions = useMemo(() => extensionProviders?.map((provider) => provider({})), [extensionProviders]);
88
+ // TODO(burdon): Reconcile with DocumentEditor.useExtensions.
89
+ const providerExtensions = useMemo(
90
+ () => extensionProviders?.flatMap((provider) => provider({})).filter(nonNullable),
91
+ [extensionProviders],
92
+ );
94
93
 
95
94
  // TODO(Zan): Move these into thread plugin as well?
96
95
  const [commentsState, commentObserver] = useCommentState();
97
96
  const onCommentClick = useCallback(() => {
98
- void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
97
+ void dispatch({
98
+ action: LayoutAction.SET_LAYOUT,
99
+ data: { element: 'complementary', state: true },
100
+ });
99
101
  }, [dispatch]);
100
102
  const commentClickObserver = useCommentClickListener(onCommentClick);
101
103
 
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
104
  // Drag files.
133
105
  const handleDrop: DNDOptions['onDrop'] = async (view, { files }) => {
134
106
  const file = files[0];
@@ -161,16 +133,16 @@ export const MarkdownEditor = ({
161
133
  slots: { content: { className: editorContent } },
162
134
  }),
163
135
  editorGutter,
164
- role !== 'section' && onFileUpload ? dropFile({ onDrop: handleDrop }) : [],
136
+ role !== 'section' && onFileUpload && dropFile({ onDrop: handleDrop }),
165
137
  providerExtensions,
166
138
  extensions,
167
- ].filter(nonNullable),
139
+ ].filter(isNotFalsy),
168
140
  ...(role !== 'section' && {
169
141
  id,
170
142
  scrollTo,
171
143
  selection,
172
144
  // TODO(wittjosiah): Autofocus based on layout is racy.
173
- autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
145
+ // autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
174
146
  moveToEndOfLine: true,
175
147
  }),
176
148
  }),
@@ -178,6 +150,7 @@ export const MarkdownEditor = ({
178
150
  );
179
151
 
180
152
  useTest(editorView);
153
+ useSelectCurrentThread(editorView, id);
181
154
 
182
155
  // Toolbar handler.
183
156
  const handleToolbarAction = useActionHandler(editorView);
@@ -201,29 +174,27 @@ export const MarkdownEditor = ({
201
174
  return (
202
175
  <div
203
176
  role='none'
204
- // TODO(burdon): Move role logic out of here (see sheet, table, sketch, etc.)
205
177
  {...(role === 'section'
206
178
  ? { className: 'flex flex-col' }
207
179
  : {
208
- className: 'contents group/editor',
209
- ...(isDirectlyAttended && { 'aria-current': 'location' }),
180
+ className: 'contents',
181
+ // TODO(wittjosiah): Factor out to `useAttendableAttributes`?
182
+ ...(hasAttention && { 'aria-current': 'location' }),
183
+ ...attendableAttributes,
210
184
  })}
211
185
  >
212
186
  {toolbar && (
213
- <div role='none' className={mx('flex shrink-0 justify-center overflow-x-auto', attentionFragment)}>
187
+ <div role='none' className='flex shrink-0 justify-center overflow-x-auto attention-surface'>
214
188
  <Toolbar.Root
215
189
  classNames={
216
190
  role === 'section'
217
191
  ? [
218
192
  textBlockWidth,
219
193
  'z-[2] group-focus-within/section:visible',
220
- !isDirectlyAttended && 'invisible',
194
+ !hasAttention && 'invisible',
221
195
  sectionToolbarLayout,
222
196
  ]
223
- : [
224
- textBlockWidth,
225
- 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
226
- ]
197
+ : [textBlockWidth]
227
198
  }
228
199
  state={formattingState && { ...formattingState, ...commentsState }}
229
200
  onAction={handleAction}
@@ -247,8 +218,8 @@ export const MarkdownEditor = ({
247
218
  : mx(
248
219
  'flex is-full bs-full overflow-hidden',
249
220
  focusRing,
250
- attentionFragment,
251
- 'focus-visible:ring-inset',
221
+ 'focus-visible:ring-inset attention-surface',
222
+ 'p-0.5', // TODO(burdon): Handle padding for focusRing consistently.
252
223
  'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
253
224
  )
254
225
  }
@@ -268,5 +239,3 @@ const useTest = (view?: EditorView) => {
268
239
  }
269
240
  }, [view]);
270
241
  };
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 { FormInput } 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
+ <FormInput 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
+ </FormInput>
41
41
 
42
- <SettingsValue label={t('editor input mode label')}>
42
+ <FormInput 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
+ </FormInput>
63
63
 
64
- <SettingsValue label={t('settings toolbar label')}>
64
+ <FormInput label={t('settings toolbar label')}>
65
65
  <Input.Switch checked={settings.toolbar} onCheckedChange={(checked) => (settings.toolbar = !!checked)} />
66
- </SettingsValue>
66
+ </FormInput>
67
67
 
68
- <SettingsValue label={t('settings numbered headings label')}>
68
+ <FormInput 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
+ </FormInput>
74
74
 
75
- <SettingsValue label={t('settings folding label')}>
75
+ <FormInput label={t('settings folding label')}>
76
76
  <Input.Switch checked={settings.folding} onCheckedChange={(checked) => (settings.folding = !!checked)} />
77
- </SettingsValue>
77
+ </FormInput>
78
78
 
79
- <SettingsValue label={t('settings experimental label')}>
79
+ <FormInput 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
+ </FormInput>
85
85
 
86
- <SettingsValue
86
+ <FormInput
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
+ </FormInput>
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,121 @@
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;
32
- document?: DocumentType;
33
- debug?: boolean;
34
- experimental?: boolean;
35
- numberedHeadings?: boolean;
36
- folding?: boolean;
37
- query?: Query<DocumentType>;
35
+ type ExtensionsOptions = {
36
+ document: 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
+ [document, viewMode, dispatch, settings, settings.folding, settings.numberedHeadings],
70
+ );
52
71
 
53
72
  //
54
- // Editor mode.
73
+ // External extensions from other plugins.
55
74
  //
56
- if (settings?.editorInputMode) {
57
- const extension = InputModeExtensions[settings.editorInputMode];
58
- if (extension) {
59
- extensions.push(extension);
60
- }
61
- }
75
+ const pluginExtensions = useMemo<Extension[] | undefined>(
76
+ () =>
77
+ extensionProviders?.reduce((acc: Extension[], provider) => {
78
+ const extension = typeof provider === 'function' ? provider({ document }) : provider;
79
+ if (extension) {
80
+ acc.push(extension);
81
+ }
82
+
83
+ return acc;
84
+ }, []),
85
+ [extensionProviders],
86
+ );
87
+
88
+ //
89
+ // Basic plugins.
90
+ //
91
+ return useMemo<Extension[]>(
92
+ () =>
93
+ [
94
+ // NOTE: Data extensions must be first so that automerge is updated before other extensions compute their state.
95
+ createDataExtensions({
96
+ id: document.id,
97
+ text: document.content && createDocAccessor(document.content, ['content']),
98
+ space,
99
+ identity,
100
+ }),
101
+ selectionState(editorStateStore),
102
+ listener({
103
+ onChange: (text) => setFallbackName(document, text),
104
+ }),
105
+ baseExtensions,
106
+ pluginExtensions,
107
+ ].filter(isNotFalsy),
108
+ [baseExtensions, pluginExtensions, document, document.content, space, identity],
109
+ );
110
+ };
111
+
112
+ /**
113
+ * Create extension instances for editor.
114
+ */
115
+ const createBaseExtensions = ({ document, dispatch, settings, query, viewMode }: ExtensionsOptions): Extension[] => {
116
+ const extensions: Extension[] = [
117
+ settings.editorInputMode && InputModeExtensions[settings.editorInputMode],
118
+ settings.folding && folding(),
119
+ ].filter(isNotFalsy);
62
120
 
63
121
  //
64
122
  // Markdown
@@ -69,7 +127,7 @@ export const createBaseExtensions = ({
69
127
  formattingKeymap(),
70
128
  decorateMarkdown({
71
129
  selectionChangeDelay: 100,
72
- numberedHeadings: settings?.numberedHeadings ? { from: 2 } : undefined,
130
+ numberedHeadings: settings.numberedHeadings ? { from: 2 } : undefined,
73
131
  // TODO(wittjosiah): For internal links, consider ignoring the link text and rendering the label of the object being linked to.
74
132
  renderLinkButton:
75
133
  dispatch && document
@@ -98,7 +156,6 @@ export const createBaseExtensions = ({
98
156
  extensions.push(
99
157
  autocomplete({
100
158
  onSearch: (text: string) => {
101
- // TODO query
102
159
  // TODO(burdon): Specify filter (e.g., stack).
103
160
  return query.objects
104
161
  .map<AutocompleteResult | undefined>((object) =>
@@ -110,29 +167,27 @@ export const createBaseExtensions = ({
110
167
  }
111
168
  : undefined,
112
169
  )
113
- .filter(nonNullable);
170
+ .filter(isNotFalsy);
114
171
  },
115
172
  }),
116
173
  );
117
174
  }
118
175
 
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));
176
+ if (settings.debug) {
177
+ const items = settings.typewriter?.split(/[,\n]/) ?? '';
178
+ if (items) {
179
+ extensions.push(typewriter({ items }));
180
+ }
129
181
  }
130
182
 
131
183
  return extensions;
132
184
  };
133
185
 
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';
186
+ // TODO(burdon): Factor out styles.
187
+ const style = {
188
+ hover: 'rounded-sm text-primary-500 hover:text-primary-600 dark:text-primary-500 hover:dark:text-primary-400',
189
+ icon: 'inline-block leading-none mis-1 cursor-pointer',
190
+ };
136
191
 
137
192
  const onRenderLink = (onSelectObject: (id: string) => void) => (el: Element, url: string) => {
138
193
  // TODO(burdon): Formalize/document internal link format.
@@ -155,25 +210,31 @@ const onRenderLink = (onSelectObject: (id: string) => void) => (el: Element, url
155
210
  target: '_blank',
156
211
  };
157
212
 
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>,
213
+ renderRoot(
214
+ el,
215
+ <a {...options} className={style.hover}>
216
+ <Icon
217
+ icon={isInternal ? 'ph--arrow-square-down--bold' : 'ph--arrow-square-out--bold'}
218
+ size={4}
219
+ classNames={style.icon}
220
+ />
221
+ </a>,
166
222
  );
167
223
  };
168
224
 
169
225
  const renderLinkTooltip = (el: Element, url: string) => {
170
226
  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>,
227
+ renderRoot(
228
+ el,
229
+ <a href={url} rel='noreferrer' target='_blank' className={style.hover}>
230
+ {web.origin}
231
+ <Icon icon='ph--arrow-square-out--bold' size={4} classNames={style.icon} />
232
+ </a>,
178
233
  );
179
234
  };
235
+
236
+ // TODO(burdon): Remove react rendering; use DOM directly.
237
+ export const renderRoot = <T extends Element>(root: T, node: ReactNode): T => {
238
+ createRoot(root).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
239
+ return root;
240
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './useSelectCurrentThread';