@dxos/plugin-markdown 0.6.13-main.ed424a1 → 0.6.13-staging.1e988a3

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 (122) hide show
  1. package/dist/lib/browser/DocumentCard-2P4EICBA.mjs +11 -0
  2. package/dist/lib/browser/DocumentEditor-GPWV3VN3.mjs +11 -0
  3. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs +10 -0
  4. package/dist/lib/browser/MarkdownEditor-EKJJQEFL.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-354DCID5.mjs +117 -0
  6. package/dist/lib/browser/chunk-354DCID5.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-4GGD6YJO.mjs +19 -0
  8. package/dist/lib/browser/chunk-4GGD6YJO.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-7AF2JLK4.mjs +164 -0
  10. package/dist/lib/browser/chunk-7AF2JLK4.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-US5O2P3R.mjs → chunk-CQJL4G4X.mjs} +2 -4
  12. package/dist/lib/browser/chunk-CQJL4G4X.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-RL7QY322.mjs +86 -0
  14. package/dist/lib/browser/chunk-RL7QY322.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-VUN4QKTT.mjs +208 -0
  16. package/dist/lib/browser/chunk-VUN4QKTT.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +117 -78
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/meta.json +1 -1
  20. package/dist/lib/browser/meta.mjs +1 -1
  21. package/dist/lib/browser/types/index.mjs +4 -6
  22. package/dist/lib/node/{chunk-P7YU53RP.cjs → DocumentCard-EHJDDSRY.cjs} +10 -16
  23. package/dist/lib/node/DocumentCard-EHJDDSRY.cjs.map +7 -0
  24. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs +29 -0
  25. package/dist/lib/node/DocumentEditor-I5GCRBKU.cjs.map +7 -0
  26. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs +31 -0
  27. package/dist/lib/node/MarkdownEditor-UE23H75V.cjs.map +7 -0
  28. package/dist/lib/node/chunk-7XIBNEI7.cjs +238 -0
  29. package/dist/lib/node/chunk-7XIBNEI7.cjs.map +7 -0
  30. package/dist/lib/node/chunk-KTYIOXL5.cjs +149 -0
  31. package/dist/lib/node/chunk-KTYIOXL5.cjs.map +7 -0
  32. package/dist/lib/node/chunk-Q4ZSCBQE.cjs +114 -0
  33. package/dist/lib/node/chunk-Q4ZSCBQE.cjs.map +7 -0
  34. package/dist/lib/node/chunk-RVGN72IX.cjs +189 -0
  35. package/dist/lib/node/chunk-RVGN72IX.cjs.map +7 -0
  36. package/dist/lib/node/chunk-TGMR2CKU.cjs +52 -0
  37. package/dist/lib/node/chunk-TGMR2CKU.cjs.map +7 -0
  38. package/dist/lib/node/{chunk-UJMOZCIA.cjs → chunk-VWQH4WC2.cjs} +8 -11
  39. package/dist/lib/node/chunk-VWQH4WC2.cjs.map +7 -0
  40. package/dist/lib/node/index.cjs +147 -112
  41. package/dist/lib/node/index.cjs.map +4 -4
  42. package/dist/lib/node/meta.cjs +3 -3
  43. package/dist/lib/node/meta.cjs.map +1 -1
  44. package/dist/lib/node/meta.json +1 -1
  45. package/dist/lib/node/types/index.cjs +6 -8
  46. package/dist/lib/node/types/index.cjs.map +2 -2
  47. package/dist/types/src/MarkdownPlugin.d.ts.map +1 -1
  48. package/dist/types/src/components/DocumentCard.d.ts +16 -0
  49. package/dist/types/src/components/DocumentCard.d.ts.map +1 -0
  50. package/dist/types/src/components/DocumentEditor.d.ts +14 -0
  51. package/dist/types/src/components/DocumentEditor.d.ts.map +1 -0
  52. package/dist/types/src/components/HeadingMenu.d.ts +13 -0
  53. package/dist/types/src/components/HeadingMenu.d.ts.map +1 -0
  54. package/dist/types/src/components/Layout.d.ts +6 -0
  55. package/dist/types/src/components/Layout.d.ts.map +1 -0
  56. package/dist/types/src/components/MarkdownEditor.d.ts +3 -8
  57. package/dist/types/src/components/MarkdownEditor.d.ts.map +1 -1
  58. package/dist/types/src/components/MarkdownEditor.stories.d.ts +3 -3
  59. package/dist/types/src/components/MarkdownEditor.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/index.d.ts +11 -2
  61. package/dist/types/src/components/index.d.ts.map +1 -1
  62. package/dist/types/src/extensions.d.ts +15 -11
  63. package/dist/types/src/extensions.d.ts.map +1 -1
  64. package/dist/types/src/meta.d.ts +9 -4
  65. package/dist/types/src/meta.d.ts.map +1 -1
  66. package/dist/types/src/types/document.d.ts +1 -10
  67. package/dist/types/src/types/document.d.ts.map +1 -1
  68. package/dist/types/src/types/types.d.ts +9 -8
  69. package/dist/types/src/types/types.d.ts.map +1 -1
  70. package/package.json +36 -41
  71. package/src/MarkdownPlugin.tsx +97 -50
  72. package/src/components/DocumentCard.tsx +107 -0
  73. package/src/components/DocumentEditor.tsx +137 -0
  74. package/src/components/HeadingMenu.tsx +46 -0
  75. package/src/components/Layout.tsx +27 -0
  76. package/src/components/MarkdownEditor.stories.tsx +7 -13
  77. package/src/components/MarkdownEditor.tsx +75 -40
  78. package/src/components/Toolbar.stories.tsx +2 -2
  79. package/src/components/index.ts +14 -2
  80. package/src/extensions.tsx +67 -124
  81. package/src/meta.tsx +19 -0
  82. package/src/types/document.ts +0 -12
  83. package/src/types/types.ts +7 -10
  84. package/src/util.tsx +2 -2
  85. package/dist/lib/browser/MarkdownContainer-AYBJNSXD.mjs +0 -467
  86. package/dist/lib/browser/MarkdownContainer-AYBJNSXD.mjs.map +0 -7
  87. package/dist/lib/browser/chunk-45N5MEOV.mjs +0 -50
  88. package/dist/lib/browser/chunk-45N5MEOV.mjs.map +0 -7
  89. package/dist/lib/browser/chunk-DRJ3FPYF.mjs +0 -15
  90. package/dist/lib/browser/chunk-DRJ3FPYF.mjs.map +0 -7
  91. package/dist/lib/browser/chunk-US5O2P3R.mjs.map +0 -7
  92. package/dist/lib/node/MarkdownContainer-IM3V72FY.cjs +0 -482
  93. package/dist/lib/node/MarkdownContainer-IM3V72FY.cjs.map +0 -7
  94. package/dist/lib/node/chunk-P7YU53RP.cjs.map +0 -7
  95. package/dist/lib/node/chunk-UJMOZCIA.cjs.map +0 -7
  96. package/dist/lib/node/chunk-W2YJVZ3N.cjs +0 -72
  97. package/dist/lib/node/chunk-W2YJVZ3N.cjs.map +0 -7
  98. package/dist/lib/node-esm/MarkdownContainer-56YBSFBE.mjs +0 -468
  99. package/dist/lib/node-esm/MarkdownContainer-56YBSFBE.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs +0 -42
  101. package/dist/lib/node-esm/chunk-MIDCCMIX.mjs.map +0 -7
  102. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs +0 -17
  103. package/dist/lib/node-esm/chunk-NEVN5WR6.mjs.map +0 -7
  104. package/dist/lib/node-esm/chunk-UCNOGIBC.mjs +0 -51
  105. package/dist/lib/node-esm/chunk-UCNOGIBC.mjs.map +0 -7
  106. package/dist/lib/node-esm/index.mjs +0 -494
  107. package/dist/lib/node-esm/index.mjs.map +0 -7
  108. package/dist/lib/node-esm/meta.json +0 -1
  109. package/dist/lib/node-esm/meta.mjs +0 -10
  110. package/dist/lib/node-esm/types/index.mjs +0 -15
  111. package/dist/types/src/components/MarkdownContainer.d.ts +0 -15
  112. package/dist/types/src/components/MarkdownContainer.d.ts.map +0 -1
  113. package/dist/types/src/hooks/index.d.ts +0 -2
  114. package/dist/types/src/hooks/index.d.ts.map +0 -1
  115. package/dist/types/src/hooks/useSelectCurrentThread.d.ts +0 -6
  116. package/dist/types/src/hooks/useSelectCurrentThread.d.ts.map +0 -1
  117. package/src/components/MarkdownContainer.tsx +0 -108
  118. package/src/hooks/index.ts +0 -5
  119. package/src/hooks/useSelectCurrentThread.tsx +0 -46
  120. package/src/meta.ts +0 -15
  121. /package/dist/lib/{node-esm/meta.mjs.map → browser/DocumentCard-2P4EICBA.mjs.map} +0 -0
  122. /package/dist/lib/{node-esm/types/index.mjs.map → browser/DocumentEditor-GPWV3VN3.mjs.map} +0 -0
@@ -0,0 +1,137 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useMemo } from 'react';
6
+
7
+ import { useResolvePlugin, parseFileManagerPlugin, useIntentDispatcher } from '@dxos/app-framework';
8
+ import { createDocAccessor, fullyQualifiedId, getSpace } from '@dxos/react-client/echo';
9
+ import { useIdentity } from '@dxos/react-client/halo';
10
+ import {
11
+ createDataExtensions,
12
+ listener,
13
+ localStorageStateStoreAdapter,
14
+ state,
15
+ type Extension,
16
+ type EditorSelectionState,
17
+ } from '@dxos/react-ui-editor';
18
+
19
+ import MarkdownEditor, { type MarkdownEditorProps } from './MarkdownEditor';
20
+ import { createBaseExtensions } from '../extensions';
21
+ import { type DocumentType, type MarkdownPluginState, type MarkdownSettingsProps } from '../types';
22
+ import { getFallbackName, setFallbackName } from '../util';
23
+
24
+ type DocumentEditorProps = {
25
+ document: DocumentType;
26
+ settings: MarkdownSettingsProps;
27
+ } & Omit<MarkdownEditorProps, 'id' | 'inputMode' | 'toolbar' | 'extensions'> &
28
+ Pick<MarkdownPluginState, 'extensionProviders'>;
29
+
30
+ /**
31
+ * Editor for a `DocumentType`.
32
+ */
33
+ const DocumentEditor = ({
34
+ document: doc,
35
+ extensionProviders = [],
36
+ viewMode,
37
+ settings,
38
+ ...props
39
+ }: DocumentEditorProps) => {
40
+ const space = getSpace(doc);
41
+ const identity = useIdentity();
42
+ const dispatch = useIntentDispatcher();
43
+
44
+ const baseExtensions = useMemo(() => {
45
+ // TODO(wittjosiah): Autocomplete is not working and this query is causing performance issues.
46
+ // const query = space?.db.query(Filter.schema(DocumentType));
47
+ // query?.subscribe();
48
+ return createBaseExtensions({
49
+ viewMode,
50
+ settings,
51
+ document: doc,
52
+ dispatch,
53
+ // query,
54
+ });
55
+ }, [doc, viewMode, dispatch, settings, settings.folding, settings.numberedHeadings]);
56
+
57
+ const providerExtensions = useMemo(
58
+ () =>
59
+ extensionProviders.reduce((acc: Extension[], provider) => {
60
+ const provided = typeof provider === 'function' ? provider({ document: doc }) : provider;
61
+ acc.push(...provided);
62
+ return acc;
63
+ }, []),
64
+ [extensionProviders],
65
+ );
66
+
67
+ const extensions = useMemo(
68
+ () => [
69
+ // NOTE: Data extensions must be first so that automerge is updated before other extensions compute their state.
70
+ createDataExtensions({
71
+ id: doc.id,
72
+ text: doc.content && createDocAccessor(doc.content, ['content']),
73
+ space,
74
+ identity,
75
+ }),
76
+ state(localStorageStateStoreAdapter),
77
+ listener({
78
+ onChange: (text) => {
79
+ setFallbackName(doc, text);
80
+ },
81
+ }),
82
+ providerExtensions,
83
+ baseExtensions,
84
+ ],
85
+ [doc, doc.content, space, baseExtensions, providerExtensions, identity],
86
+ );
87
+
88
+ const initialValue = useMemo(() => doc.content?.content, [doc.content]);
89
+
90
+ // Migrate gradually to `fallbackName`.
91
+ useEffect(() => {
92
+ if (!doc.fallbackName && doc.content?.content) {
93
+ doc.fallbackName = getFallbackName(doc.content.content);
94
+ }
95
+ }, [doc, doc.content]);
96
+
97
+ // Restore last selection and scroll point.
98
+ const id = fullyQualifiedId(doc);
99
+ const { scrollTo, selection } = useMemo<EditorSelectionState>(
100
+ () => localStorageStateStoreAdapter.getState(id) ?? {},
101
+ [doc],
102
+ );
103
+
104
+ const fileManagerPlugin = useResolvePlugin(parseFileManagerPlugin);
105
+ const handleFileUpload = useMemo(() => {
106
+ if (space === undefined) {
107
+ return undefined;
108
+ }
109
+
110
+ if (fileManagerPlugin?.provides.file.upload === undefined) {
111
+ return undefined;
112
+ }
113
+
114
+ return async (file: File) => {
115
+ return fileManagerPlugin?.provides?.file?.upload?.(file, space);
116
+ };
117
+ }, [fileManagerPlugin, space]);
118
+
119
+ return (
120
+ <MarkdownEditor
121
+ id={id}
122
+ initialValue={initialValue}
123
+ extensions={extensions}
124
+ scrollTo={scrollTo}
125
+ selection={selection}
126
+ onFileUpload={handleFileUpload}
127
+ inputMode={settings.editorInputMode}
128
+ toolbar={settings.toolbar}
129
+ viewMode={viewMode}
130
+ {...props}
131
+ />
132
+ );
133
+ };
134
+
135
+ export default DocumentEditor;
136
+
137
+ export type DocumentEditor = typeof DocumentEditor;
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { DotsThreeVertical } from '@phosphor-icons/react';
6
+ import React, { type PropsWithChildren, type FC } from 'react';
7
+
8
+ import { Surface } from '@dxos/app-framework';
9
+ import { Button, DropdownMenu } from '@dxos/react-ui';
10
+ import { fineButtonDimensions, getSize } from '@dxos/react-ui-theme';
11
+
12
+ import { type DocumentType, type MarkdownProperties } from '../types';
13
+
14
+ // TODO(thure): This needs to be refactored into a graph node action.
15
+ export const DocumentHeadingMenu: FC<{ document: DocumentType }> = ({ document }) => {
16
+ return <HeadingMenu properties={document} content={document.content?.content} />;
17
+ };
18
+
19
+ /**
20
+ * Menu for the layout heading.
21
+ */
22
+ export const HeadingMenu = ({
23
+ content,
24
+ properties,
25
+ }: PropsWithChildren<{
26
+ content: string | undefined;
27
+ properties: MarkdownProperties;
28
+ }>) => {
29
+ return (
30
+ <DropdownMenu.Root modal={false}>
31
+ <DropdownMenu.Trigger asChild>
32
+ <Button variant='ghost' classNames={fineButtonDimensions}>
33
+ <DotsThreeVertical className={getSize(4)} />
34
+ </Button>
35
+ </DropdownMenu.Trigger>
36
+ <DropdownMenu.Portal>
37
+ <DropdownMenu.Content sideOffset={8} classNames='z-10'>
38
+ <DropdownMenu.Viewport>
39
+ <Surface data={{ content, properties }} role='menuitem' />
40
+ </DropdownMenu.Viewport>
41
+ <DropdownMenu.Arrow />
42
+ </DropdownMenu.Content>
43
+ </DropdownMenu.Portal>
44
+ </DropdownMenu.Root>
45
+ );
46
+ };
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import { Main } from '@dxos/react-ui';
8
+ import { editorWithToolbarLayout } from '@dxos/react-ui-editor';
9
+ import { topbarBlockPaddingStart } from '@dxos/react-ui-theme';
10
+
11
+ export const MainLayout = ({ children, toolbar }: PropsWithChildren<{ toolbar?: boolean }>) => {
12
+ return (
13
+ <Main.Content
14
+ bounce
15
+ data-toolbar={toolbar ? 'enabled' : 'disabled'}
16
+ classNames={[topbarBlockPaddingStart, editorWithToolbarLayout]}
17
+ >
18
+ {children}
19
+ </Main.Content>
20
+ );
21
+ };
22
+
23
+ // Used when the editor is embedded in another context (e.g., iframe) and has no topbar/sidebar/etc.
24
+ // TODO(wittjosiah): What's the difference between this and Section/Card?
25
+ export const EmbeddedLayout = ({ children }: PropsWithChildren) => {
26
+ return <Main.Content classNames='min-bs-[100dvh] grid p-0.5'>{children}</Main.Content>;
27
+ };
@@ -3,32 +3,26 @@
3
3
  //
4
4
 
5
5
  import '@dxos-theme';
6
-
7
6
  import React, { useMemo, type FC } from 'react';
8
7
 
9
- import { createDocAccessor, createObject } from '@dxos/react-client/echo';
10
- import { Main } from '@dxos/react-ui';
11
- import { editorWithToolbarLayout, automerge } from '@dxos/react-ui-editor';
12
- import { topbarBlockPaddingStart } from '@dxos/react-ui-theme';
8
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
9
+ import { automerge } from '@dxos/react-ui-editor';
13
10
  import { withLayout, withTheme } from '@dxos/storybook-utils';
14
11
 
12
+ import { MainLayout } from './Layout';
15
13
  import { MarkdownEditor } from './MarkdownEditor';
16
14
 
17
15
  const Story: FC<{
18
16
  content: string;
19
17
  toolbar?: boolean;
20
18
  }> = ({ content = '# Test', toolbar }) => {
21
- const doc = useMemo(() => createObject({ content }), [content]);
19
+ const doc = useMemo(() => createEchoObject({ content }), [content]);
22
20
  const extensions = useMemo(() => [automerge(createDocAccessor(doc, ['content']))], [doc]);
23
21
 
24
22
  return (
25
- <Main.Content
26
- bounce
27
- data-toolbar={toolbar ? 'enabled' : 'disabled'}
28
- classNames={[topbarBlockPaddingStart, editorWithToolbarLayout]}
29
- >
23
+ <MainLayout toolbar={toolbar}>
30
24
  <MarkdownEditor id='test' initialValue={doc.content} extensions={extensions} toolbar={toolbar} />
31
- </Main.Content>
25
+ </MainLayout>
32
26
  );
33
27
  };
34
28
 
@@ -36,8 +30,8 @@ export default {
36
30
  title: 'plugin-markdown/EditorMain',
37
31
  component: MarkdownEditor,
38
32
  decorators: [withTheme, withLayout({ tooltips: true })],
39
- parameters: { layout: 'fullscreen' },
40
33
  render: Story,
34
+ parameters: { layout: 'fullscreen' },
41
35
  };
42
36
 
43
37
  const content = Array.from({ length: 100 })
@@ -3,47 +3,61 @@
3
3
  //
4
4
 
5
5
  import { openSearchPanel } from '@codemirror/search';
6
- import { type EditorView } from '@codemirror/view';
6
+ import { EditorView } from '@codemirror/view';
7
7
  import React, { useMemo, useEffect, useCallback } from 'react';
8
8
 
9
- import { type FileInfo, LayoutAction, type LayoutCoordinate, useIntentDispatcher } from '@dxos/app-framework';
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';
10
19
  import { useThemeContext, useTranslation } from '@dxos/react-ui';
11
- import { useAttendableAttributes, useAttention } from '@dxos/react-ui-attention';
12
20
  import {
13
21
  type Action,
14
22
  type DNDOptions,
15
23
  type EditorViewMode,
16
24
  type EditorInputMode,
17
- Toolbar,
18
25
  type UseTextEditorProps,
26
+ Toolbar,
19
27
  createBasicExtensions,
20
28
  createMarkdownExtensions,
21
29
  createThemeExtensions,
22
30
  dropFile,
23
- editorContent,
24
- editorGutter,
25
31
  processAction,
26
32
  useActionHandler,
27
33
  useCommentState,
28
34
  useCommentClickListener,
29
35
  useFormattingState,
30
36
  useTextEditor,
37
+ editorContent,
38
+ editorGutter,
39
+ Cursor,
40
+ setSelection,
31
41
  } from '@dxos/react-ui-editor';
32
42
  import { sectionToolbarLayout } from '@dxos/react-ui-stack';
33
43
  import { textBlockWidth, focusRing, mx } from '@dxos/react-ui-theme';
34
- import { isNotFalsy, nonNullable } from '@dxos/util';
44
+ import { nonNullable } from '@dxos/util';
35
45
 
36
- import { useSelectCurrentThread } from '../hooks';
37
46
  import { MARKDOWN_PLUGIN } from '../meta';
38
- import { type MarkdownPluginState } from '../types';
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
+ );
39
53
 
40
54
  const DEFAULT_VIEW_MODE: EditorViewMode = 'preview';
41
55
 
42
56
  export type MarkdownEditorProps = {
43
57
  id: string;
44
- role?: string;
45
58
  coordinate?: LayoutCoordinate;
46
59
  inputMode?: EditorInputMode;
60
+ role?: string;
47
61
  scrollPastEnd?: boolean;
48
62
  toolbar?: boolean;
49
63
  viewMode?: EditorViewMode;
@@ -52,12 +66,6 @@ export type MarkdownEditorProps = {
52
66
  } & Pick<UseTextEditorProps, 'initialValue' | 'scrollTo' | 'selection' | 'extensions'> &
53
67
  Partial<Pick<MarkdownPluginState, 'extensionProviders'>>;
54
68
 
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
- */
61
69
  export const MarkdownEditor = ({
62
70
  id,
63
71
  role = 'article',
@@ -75,27 +83,52 @@ export const MarkdownEditor = ({
75
83
  const { t } = useTranslation(MARKDOWN_PLUGIN);
76
84
  const { themeMode } = useThemeContext();
77
85
  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;
78
90
  const [formattingState, formattingObserver] = useFormattingState();
79
- const attendableAttributes = useAttendableAttributes(id);
80
- const { hasAttention } = useAttention(id);
81
91
 
82
92
  // Extensions from other plugins.
83
- // TODO(burdon): Reconcile with DocumentEditor.useExtensions.
84
- const providerExtensions = useMemo(
85
- () => extensionProviders?.flatMap((provider) => provider({})).filter(nonNullable),
86
- [extensionProviders],
87
- );
93
+ const providerExtensions = useMemo(() => extensionProviders?.map((provider) => provider({})), [extensionProviders]);
88
94
 
89
95
  // TODO(Zan): Move these into thread plugin as well?
90
96
  const [commentsState, commentObserver] = useCommentState();
91
97
  const onCommentClick = useCallback(() => {
92
- void dispatch({
93
- action: LayoutAction.SET_LAYOUT,
94
- data: { element: 'complementary', state: true },
95
- });
98
+ void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
96
99
  }, [dispatch]);
97
100
  const commentClickObserver = useCommentClickListener(onCommentClick);
98
101
 
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
+
99
132
  // Drag files.
100
133
  const handleDrop: DNDOptions['onDrop'] = async (view, { files }) => {
101
134
  const file = files[0];
@@ -128,16 +161,16 @@ export const MarkdownEditor = ({
128
161
  slots: { content: { className: editorContent } },
129
162
  }),
130
163
  editorGutter,
131
- role !== 'section' && onFileUpload && dropFile({ onDrop: handleDrop }),
164
+ role !== 'section' && onFileUpload ? dropFile({ onDrop: handleDrop }) : [],
132
165
  providerExtensions,
133
166
  extensions,
134
- ].filter(isNotFalsy),
167
+ ].filter(nonNullable),
135
168
  ...(role !== 'section' && {
136
169
  id,
137
170
  scrollTo,
138
171
  selection,
139
172
  // TODO(wittjosiah): Autofocus based on layout is racy.
140
- // autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
173
+ autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
141
174
  moveToEndOfLine: true,
142
175
  }),
143
176
  }),
@@ -145,7 +178,6 @@ export const MarkdownEditor = ({
145
178
  );
146
179
 
147
180
  useTest(editorView);
148
- useSelectCurrentThread(editorView, id);
149
181
 
150
182
  // Toolbar handler.
151
183
  const handleToolbarAction = useActionHandler(editorView);
@@ -173,24 +205,25 @@ export const MarkdownEditor = ({
173
205
  {...(role === 'section'
174
206
  ? { className: 'flex flex-col' }
175
207
  : {
176
- className: 'contents',
177
- // TODO(wittjosiah): Factor out to `useAttendableAttributes`?
178
- ...(hasAttention && { 'aria-current': 'location' }),
179
- ...attendableAttributes,
208
+ className: 'contents group/editor',
209
+ ...(isDirectlyAttended && { 'aria-current': 'location' }),
180
210
  })}
181
211
  >
182
212
  {toolbar && (
183
- <div role='none' className='flex shrink-0 justify-center overflow-x-auto attention-surface'>
213
+ <div role='none' className={mx('flex shrink-0 justify-center overflow-x-auto', attentionFragment)}>
184
214
  <Toolbar.Root
185
215
  classNames={
186
216
  role === 'section'
187
217
  ? [
188
218
  textBlockWidth,
189
219
  'z-[2] group-focus-within/section:visible',
190
- !hasAttention && 'invisible',
220
+ !isDirectlyAttended && 'invisible',
191
221
  sectionToolbarLayout,
192
222
  ]
193
- : [textBlockWidth]
223
+ : [
224
+ textBlockWidth,
225
+ 'group-focus-within/editor:border-separator group-[[aria-current]]/editor:border-separator',
226
+ ]
194
227
  }
195
228
  state={formattingState && { ...formattingState, ...commentsState }}
196
229
  onAction={handleAction}
@@ -214,8 +247,8 @@ export const MarkdownEditor = ({
214
247
  : mx(
215
248
  'flex is-full bs-full overflow-hidden',
216
249
  focusRing,
217
- 'focus-visible:ring-inset attention-surface',
218
- 'p-0.5', // TODO(burdon): Handle padding for focusRing consistently.
250
+ attentionFragment,
251
+ 'focus-visible:ring-inset',
219
252
  'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
220
253
  )
221
254
  }
@@ -235,3 +268,5 @@ const useTest = (view?: EditorView) => {
235
268
  }
236
269
  }, [view]);
237
270
  };
271
+
272
+ export default MarkdownEditor;
@@ -9,7 +9,7 @@ import React, { type FC, useState } from 'react';
9
9
  import { create } from '@dxos/echo-schema';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { faker } from '@dxos/random';
12
- import { createDocAccessor, createObject } from '@dxos/react-client/echo';
12
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
13
13
  import { useThemeContext } from '@dxos/react-ui';
14
14
  import {
15
15
  type Action,
@@ -39,7 +39,7 @@ faker.seed(101);
39
39
 
40
40
  const Story: FC<{ content: string }> = ({ content }) => {
41
41
  const { themeMode } = useThemeContext();
42
- const [text] = useState(createObject(create(TextType, { content })));
42
+ const [text] = useState(createEchoObject(create(TextType, { content })));
43
43
  const [formattingState, formattingObserver] = useFormattingState();
44
44
  const [viewMode, setViewMode] = useState<EditorViewMode>('preview');
45
45
  const { parentRef, view } = useTextEditor(() => {
@@ -2,8 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React from 'react';
5
+ import React, { type LazyExoticComponent } 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';
7
16
  export * from './MarkdownSettings';
8
17
 
9
- export const MarkdownContainer = React.lazy(() => import('./MarkdownContainer'));
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'));