@dxos/plugin-markdown 0.6.8-main.046e6cf

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 (107) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +15 -0
  3. package/dist/lib/browser/DocumentCard-CDWDPILF.mjs +11 -0
  4. package/dist/lib/browser/DocumentCard-CDWDPILF.mjs.map +7 -0
  5. package/dist/lib/browser/DocumentEditor-VMHFHWOQ.mjs +11 -0
  6. package/dist/lib/browser/DocumentEditor-VMHFHWOQ.mjs.map +7 -0
  7. package/dist/lib/browser/MarkdownEditor-KYUQ45PC.mjs +10 -0
  8. package/dist/lib/browser/MarkdownEditor-KYUQ45PC.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-4GGD6YJO.mjs +19 -0
  10. package/dist/lib/browser/chunk-4GGD6YJO.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-6ZL2GJCQ.mjs +177 -0
  12. package/dist/lib/browser/chunk-6ZL2GJCQ.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-DX3K37SM.mjs +86 -0
  14. package/dist/lib/browser/chunk-DX3K37SM.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-MDX3MAMP.mjs +119 -0
  16. package/dist/lib/browser/chunk-MDX3MAMP.mjs.map +7 -0
  17. package/dist/lib/browser/chunk-PZDW7KVZ.mjs +172 -0
  18. package/dist/lib/browser/chunk-PZDW7KVZ.mjs.map +7 -0
  19. package/dist/lib/browser/chunk-RETREORA.mjs +39 -0
  20. package/dist/lib/browser/chunk-RETREORA.mjs.map +7 -0
  21. package/dist/lib/browser/index.mjs +506 -0
  22. package/dist/lib/browser/index.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -0
  24. package/dist/lib/browser/meta.mjs +9 -0
  25. package/dist/lib/browser/meta.mjs.map +7 -0
  26. package/dist/lib/browser/types/index.mjs +12 -0
  27. package/dist/lib/browser/types/index.mjs.map +7 -0
  28. package/dist/lib/node/DocumentCard-SOTFILJY.cjs +32 -0
  29. package/dist/lib/node/DocumentCard-SOTFILJY.cjs.map +7 -0
  30. package/dist/lib/node/DocumentEditor-CUKHGS5R.cjs +29 -0
  31. package/dist/lib/node/DocumentEditor-CUKHGS5R.cjs.map +7 -0
  32. package/dist/lib/node/MarkdownEditor-GGFCD26C.cjs +31 -0
  33. package/dist/lib/node/MarkdownEditor-GGFCD26C.cjs.map +7 -0
  34. package/dist/lib/node/chunk-FW5O2I25.cjs +114 -0
  35. package/dist/lib/node/chunk-FW5O2I25.cjs.map +7 -0
  36. package/dist/lib/node/chunk-IUQ2SKGY.cjs +202 -0
  37. package/dist/lib/node/chunk-IUQ2SKGY.cjs.map +7 -0
  38. package/dist/lib/node/chunk-TGMR2CKU.cjs +52 -0
  39. package/dist/lib/node/chunk-TGMR2CKU.cjs.map +7 -0
  40. package/dist/lib/node/chunk-TO3FCKT7.cjs +202 -0
  41. package/dist/lib/node/chunk-TO3FCKT7.cjs.map +7 -0
  42. package/dist/lib/node/chunk-TZDYK4MV.cjs +151 -0
  43. package/dist/lib/node/chunk-TZDYK4MV.cjs.map +7 -0
  44. package/dist/lib/node/chunk-VYLYUDDI.cjs +58 -0
  45. package/dist/lib/node/chunk-VYLYUDDI.cjs.map +7 -0
  46. package/dist/lib/node/index.cjs +517 -0
  47. package/dist/lib/node/index.cjs.map +7 -0
  48. package/dist/lib/node/meta.cjs +30 -0
  49. package/dist/lib/node/meta.cjs.map +7 -0
  50. package/dist/lib/node/meta.json +1 -0
  51. package/dist/lib/node/types/index.cjs +34 -0
  52. package/dist/lib/node/types/index.cjs.map +7 -0
  53. package/dist/types/src/MarkdownPlugin.d.ts +4 -0
  54. package/dist/types/src/MarkdownPlugin.d.ts.map +1 -0
  55. package/dist/types/src/components/DocumentCard.d.ts +16 -0
  56. package/dist/types/src/components/DocumentCard.d.ts.map +1 -0
  57. package/dist/types/src/components/DocumentEditor.d.ts +14 -0
  58. package/dist/types/src/components/DocumentEditor.d.ts.map +1 -0
  59. package/dist/types/src/components/HeadingMenu.d.ts +13 -0
  60. package/dist/types/src/components/HeadingMenu.d.ts.map +1 -0
  61. package/dist/types/src/components/Layout.d.ts +6 -0
  62. package/dist/types/src/components/Layout.d.ts.map +1 -0
  63. package/dist/types/src/components/MarkdownEditor.d.ts +19 -0
  64. package/dist/types/src/components/MarkdownEditor.d.ts.map +1 -0
  65. package/dist/types/src/components/MarkdownEditor.stories.d.ts +27 -0
  66. package/dist/types/src/components/MarkdownEditor.stories.d.ts.map +1 -0
  67. package/dist/types/src/components/MarkdownSettings.d.ts +6 -0
  68. package/dist/types/src/components/MarkdownSettings.d.ts.map +1 -0
  69. package/dist/types/src/components/Toolbar.stories.d.ts +9 -0
  70. package/dist/types/src/components/Toolbar.stories.d.ts.map +1 -0
  71. package/dist/types/src/components/index.d.ts +13 -0
  72. package/dist/types/src/components/index.d.ts.map +1 -0
  73. package/dist/types/src/extensions.d.ts +20 -0
  74. package/dist/types/src/extensions.d.ts.map +1 -0
  75. package/dist/types/src/index.d.ts +6 -0
  76. package/dist/types/src/index.d.ts.map +1 -0
  77. package/dist/types/src/meta.d.ts +15 -0
  78. package/dist/types/src/meta.d.ts.map +1 -0
  79. package/dist/types/src/translations.d.ts +29 -0
  80. package/dist/types/src/translations.d.ts.map +1 -0
  81. package/dist/types/src/types/document.d.ts +97 -0
  82. package/dist/types/src/types/document.d.ts.map +1 -0
  83. package/dist/types/src/types/index.d.ts +3 -0
  84. package/dist/types/src/types/index.d.ts.map +1 -0
  85. package/dist/types/src/types/types.d.ts +42 -0
  86. package/dist/types/src/types/types.d.ts.map +1 -0
  87. package/dist/types/src/util.d.ts +11 -0
  88. package/dist/types/src/util.d.ts.map +1 -0
  89. package/package.json +94 -0
  90. package/src/MarkdownPlugin.tsx +341 -0
  91. package/src/components/DocumentCard.tsx +107 -0
  92. package/src/components/DocumentEditor.tsx +140 -0
  93. package/src/components/HeadingMenu.tsx +46 -0
  94. package/src/components/Layout.tsx +27 -0
  95. package/src/components/MarkdownEditor.stories.tsx +55 -0
  96. package/src/components/MarkdownEditor.tsx +241 -0
  97. package/src/components/MarkdownSettings.tsx +105 -0
  98. package/src/components/Toolbar.stories.tsx +118 -0
  99. package/src/components/index.ts +21 -0
  100. package/src/extensions.tsx +175 -0
  101. package/src/index.ts +11 -0
  102. package/src/meta.tsx +19 -0
  103. package/src/translations.ts +36 -0
  104. package/src/types/document.ts +17 -0
  105. package/src/types/index.ts +6 -0
  106. package/src/types/types.ts +75 -0
  107. package/src/util.tsx +48 -0
@@ -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
+ };
@@ -0,0 +1,55 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+ import React, { useMemo, type FC } from 'react';
7
+
8
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
9
+ import { Tooltip } from '@dxos/react-ui';
10
+ import { automerge } from '@dxos/react-ui-editor';
11
+ import { withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { MainLayout } from './Layout';
14
+ import { MarkdownEditor } from './MarkdownEditor';
15
+
16
+ const Story: FC<{
17
+ content: string;
18
+ toolbar?: boolean;
19
+ }> = ({ content = '# Test', toolbar }) => {
20
+ const doc = useMemo(() => createEchoObject({ content }), [content]);
21
+ const extensions = useMemo(() => [automerge(createDocAccessor(doc, ['content']))], [doc]);
22
+
23
+ return (
24
+ <Tooltip.Provider>
25
+ <MainLayout toolbar={toolbar}>
26
+ <MarkdownEditor id='test' initialValue={doc.content} extensions={extensions} toolbar={toolbar} />
27
+ </MainLayout>
28
+ </Tooltip.Provider>
29
+ );
30
+ };
31
+
32
+ export default {
33
+ title: 'plugin-markdown/EditorMain',
34
+ component: MarkdownEditor,
35
+ decorators: [withTheme],
36
+ render: Story,
37
+ parameters: { layout: 'fullscreen' },
38
+ };
39
+
40
+ const content = Array.from({ length: 100 })
41
+ .map((_, i) => `Line ${i + 1}`)
42
+ .join('\n');
43
+
44
+ export const Default = {
45
+ args: {
46
+ content,
47
+ },
48
+ };
49
+
50
+ export const WithToolbar = {
51
+ args: {
52
+ content,
53
+ toolbar: true,
54
+ },
55
+ };
@@ -0,0 +1,241 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import React, { useMemo, useEffect } from 'react';
7
+
8
+ import {
9
+ LayoutAction,
10
+ useResolvePlugin,
11
+ useIntentResolver,
12
+ parseLayoutPlugin,
13
+ type FileInfo,
14
+ type LayoutCoordinate,
15
+ } from '@dxos/app-framework';
16
+ import { parseAttentionPlugin } from '@dxos/plugin-attention';
17
+ import { useThemeContext, useTranslation } from '@dxos/react-ui';
18
+ import {
19
+ type Action,
20
+ type DNDOptions,
21
+ type EditorViewMode,
22
+ type EditorInputMode,
23
+ type UseTextEditorProps,
24
+ Toolbar,
25
+ createBasicExtensions,
26
+ createMarkdownExtensions,
27
+ createThemeExtensions,
28
+ dropFile,
29
+ processAction,
30
+ scrollThreadIntoView,
31
+ useActionHandler,
32
+ useCommentState,
33
+ useCommentClickListener,
34
+ useFormattingState,
35
+ useTextEditor,
36
+ editorContent,
37
+ editorGutter,
38
+ } from '@dxos/react-ui-editor';
39
+ import { sectionToolbarLayout } from '@dxos/react-ui-stack';
40
+ import { textBlockWidth, focusRing, mx } from '@dxos/react-ui-theme';
41
+ import { nonNullable } from '@dxos/util';
42
+
43
+ import { MARKDOWN_PLUGIN } from '../meta';
44
+ import type { MarkdownPluginState } from '../types';
45
+
46
+ const attentionFragment = mx(
47
+ 'group-focus-within/editor:attention-surface group-[[aria-current]]/editor:attention-surface',
48
+ 'group-focus-within/editor:separator-separator',
49
+ );
50
+
51
+ const DEFAULT_VIEW_MODE: EditorViewMode = 'preview';
52
+
53
+ export type MarkdownEditorProps = {
54
+ id: string;
55
+ coordinate?: LayoutCoordinate;
56
+ inputMode?: EditorInputMode;
57
+ role?: string;
58
+ scrollPastEnd?: boolean;
59
+ toolbar?: boolean;
60
+ viewMode?: EditorViewMode;
61
+ onViewModeChange?: (id: string, mode: EditorViewMode) => void;
62
+ onCommentSelect?: (id: string) => void;
63
+ onFileUpload?: (file: File) => Promise<FileInfo | undefined>;
64
+ } & Pick<UseTextEditorProps, 'initialValue' | 'selection' | 'scrollTo' | 'extensions'> &
65
+ Partial<Pick<MarkdownPluginState, 'extensionProviders'>>;
66
+
67
+ export const MarkdownEditor = ({
68
+ id,
69
+ role = 'article',
70
+ initialValue,
71
+ extensions,
72
+ extensionProviders,
73
+ scrollTo,
74
+ scrollPastEnd,
75
+ selection,
76
+ toolbar,
77
+ viewMode,
78
+ onCommentSelect,
79
+ onFileUpload,
80
+ onViewModeChange,
81
+ }: MarkdownEditorProps) => {
82
+ const { t } = useTranslation(MARKDOWN_PLUGIN);
83
+ const { themeMode } = useThemeContext();
84
+ const attentionPlugin = useResolvePlugin(parseAttentionPlugin);
85
+ const layoutPlugin = useResolvePlugin(parseLayoutPlugin);
86
+ const attended = Array.from(attentionPlugin?.provides.attention?.attended ?? []);
87
+ const isDirectlyAttended = attended.length === 1 && attended[0] === id;
88
+ const [formattingState, formattingObserver] = useFormattingState();
89
+
90
+ // Extensions from other plugins.
91
+ const providerExtensions = useMemo(() => extensionProviders?.map((provider) => provider({})), [extensionProviders]);
92
+
93
+ // TODO(Zan): Move these into thread plugin as well?
94
+ const [commentsState, commentObserver] = useCommentState();
95
+ const commentClickObserver = useCommentClickListener((id) => {
96
+ onCommentSelect?.(id);
97
+ });
98
+
99
+ // Focus comment.
100
+ useIntentResolver(MARKDOWN_PLUGIN, ({ action, data }) => {
101
+ switch (action) {
102
+ case LayoutAction.SCROLL_INTO_VIEW: {
103
+ if (editorView) {
104
+ // TODO(Zan): Try catch this. Fails when thread plugin not present?
105
+ scrollThreadIntoView(editorView, data?.id);
106
+ if (data?.id === id) {
107
+ editorView.scrollDOM
108
+ .closest('[data-attendable-id]')
109
+ ?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'start' });
110
+ }
111
+ return undefined;
112
+ }
113
+ break;
114
+ }
115
+ }
116
+ });
117
+
118
+ const {
119
+ parentRef,
120
+ view: editorView,
121
+ focusAttributes,
122
+ } = useTextEditor(
123
+ () => ({
124
+ initialValue,
125
+ extensions: [
126
+ formattingObserver,
127
+ commentObserver,
128
+ commentClickObserver,
129
+ createBasicExtensions({
130
+ readonly: viewMode === 'readonly',
131
+ placeholder: t('editor placeholder'),
132
+ scrollPastEnd: role === 'section' ? false : scrollPastEnd,
133
+ }),
134
+ createMarkdownExtensions({ themeMode }),
135
+ createThemeExtensions({ themeMode, slots: { content: { className: editorContent } } }),
136
+ editorGutter,
137
+ role !== 'section' && onFileUpload ? dropFile({ onDrop: handleDrop }) : [],
138
+ providerExtensions,
139
+ extensions,
140
+ ].filter(nonNullable),
141
+ ...(role !== 'section' && {
142
+ id,
143
+ selection,
144
+ scrollTo,
145
+ // TODO(wittjosiah): Autofocus based on layout is racey.
146
+ autoFocus: layoutPlugin?.provides.layout ? layoutPlugin?.provides.layout.scrollIntoView === id : true,
147
+ moveToEndOfLine: true,
148
+ }),
149
+ }),
150
+ [id, initialValue, formattingObserver, viewMode, themeMode, extensions, providerExtensions],
151
+ );
152
+
153
+ useTest(editorView);
154
+
155
+ // Toolbar handler.
156
+ const handleToolbarAction = useActionHandler(editorView);
157
+ const handleAction = (action: Action) => {
158
+ if (action.type === 'view-mode') {
159
+ onViewModeChange?.(id, action.data);
160
+ }
161
+
162
+ handleToolbarAction?.(action);
163
+ };
164
+
165
+ // Drag files.
166
+ const handleDrop: DNDOptions['onDrop'] = async (view, { files }) => {
167
+ const file = files[0];
168
+ const info = file && onFileUpload ? await onFileUpload(file) : undefined;
169
+ if (info) {
170
+ processAction(view, { type: 'image', data: info.url });
171
+ }
172
+ };
173
+
174
+ return (
175
+ <div
176
+ role='none'
177
+ // TODO(burdon): Move role logic out of here (see sheet, table, sketch, etc.)
178
+ {...(role === 'section'
179
+ ? { className: 'flex flex-col' }
180
+ : {
181
+ className: 'contents group/editor',
182
+ ...(isDirectlyAttended && { 'aria-current': 'location' }),
183
+ })}
184
+ >
185
+ {toolbar && (
186
+ <div role='none' className={mx('flex shrink-0 justify-center', attentionFragment)}>
187
+ <Toolbar.Root
188
+ classNames={
189
+ role === 'section'
190
+ ? ['z-[2] group-focus-within/section:visible', !attended && 'invisible', sectionToolbarLayout]
191
+ : [
192
+ textBlockWidth,
193
+ 'group-focus-within/editor:separator-separator group-[[aria-current]]/editor:separator-separator',
194
+ ]
195
+ }
196
+ state={formattingState && { ...formattingState, ...commentsState }}
197
+ onAction={handleAction}
198
+ >
199
+ <Toolbar.Markdown />
200
+ {onFileUpload && <Toolbar.Custom onUpload={onFileUpload} />}
201
+ <Toolbar.Separator />
202
+ <Toolbar.View mode={viewMode ?? DEFAULT_VIEW_MODE} />
203
+ <Toolbar.Actions />
204
+ </Toolbar.Root>
205
+ </div>
206
+ )}
207
+ <div
208
+ role='none'
209
+ ref={parentRef}
210
+ data-testid='composer.markdownRoot'
211
+ data-toolbar={toolbar ? 'enabled' : 'disabled'}
212
+ className={
213
+ // TODO(burdon): Factor out margin for focus.
214
+ role === 'section'
215
+ ? mx('flex flex-col flex-1 min-bs-[12rem] mt-[2px]', focusRing)
216
+ : mx(
217
+ 'flex is-full bs-full overflow-hidden',
218
+ focusRing,
219
+ attentionFragment,
220
+ 'focus-visible:ring-inset',
221
+ 'data-[toolbar=disabled]:pbs-2 data-[toolbar=disabled]:row-span-2',
222
+ )
223
+ }
224
+ {...focusAttributes}
225
+ />
226
+ </div>
227
+ );
228
+ };
229
+
230
+ // Expose editor view for playwright tests.
231
+ // TODO(wittjosiah): Find a better way to expose this or find a way to limit it to test runs.
232
+ const useTest = (view?: EditorView) => {
233
+ useEffect(() => {
234
+ const composer = (window as any).composer;
235
+ if (composer) {
236
+ composer.editorView = view;
237
+ }
238
+ }, [view]);
239
+ };
240
+
241
+ export default MarkdownEditor;
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { SettingsValue } from '@dxos/plugin-settings';
8
+ import { Input, Select, useTranslation } from '@dxos/react-ui';
9
+ import { type EditorInputMode, EditorInputModes, type EditorViewMode, EditorViewModes } from '@dxos/react-ui-editor';
10
+
11
+ import { MARKDOWN_PLUGIN } from '../meta';
12
+ import { type MarkdownSettingsProps } from '../types';
13
+
14
+ export const MarkdownSettings = ({ settings }: { settings: MarkdownSettingsProps }) => {
15
+ const { t } = useTranslation(MARKDOWN_PLUGIN);
16
+
17
+ // TODO(wittjosiah): Add skill test confirmation for entering vim mode.
18
+ return (
19
+ <>
20
+ <SettingsValue label={t('default view mode label')}>
21
+ <Select.Root
22
+ value={settings.defaultViewMode}
23
+ onValueChange={(value) => {
24
+ settings.defaultViewMode = value as EditorViewMode;
25
+ }}
26
+ >
27
+ <Select.TriggerButton />
28
+ <Select.Portal>
29
+ <Select.Content>
30
+ <Select.Viewport>
31
+ {EditorViewModes.map((mode) => (
32
+ <Select.Option key={mode} value={mode}>
33
+ {t(`${mode} mode label`, { ns: 'react-ui-editor' })}
34
+ </Select.Option>
35
+ ))}
36
+ </Select.Viewport>
37
+ </Select.Content>
38
+ </Select.Portal>
39
+ </Select.Root>
40
+ </SettingsValue>
41
+
42
+ <SettingsValue label={t('editor input mode label')}>
43
+ <Select.Root
44
+ value={settings.editorInputMode ?? 'default'}
45
+ onValueChange={(value) => {
46
+ settings.editorInputMode = value as EditorInputMode;
47
+ }}
48
+ >
49
+ <Select.TriggerButton placeholder={t('select editor input mode placeholder')} />
50
+ <Select.Portal>
51
+ <Select.Content>
52
+ <Select.Viewport>
53
+ {EditorInputModes.map((mode) => (
54
+ <Select.Option key={mode} value={mode}>
55
+ {t(`settings editor input mode ${mode} label`)}
56
+ </Select.Option>
57
+ ))}
58
+ </Select.Viewport>
59
+ </Select.Content>
60
+ </Select.Portal>
61
+ </Select.Root>
62
+ </SettingsValue>
63
+
64
+ <SettingsValue label={t('settings toolbar label')}>
65
+ <Input.Switch checked={settings.toolbar} onCheckedChange={(checked) => (settings.toolbar = !!checked)} />
66
+ </SettingsValue>
67
+
68
+ <SettingsValue label={t('settings numbered headings label')}>
69
+ <Input.Switch
70
+ checked={settings.numberedHeadings}
71
+ onCheckedChange={(checked) => (settings.numberedHeadings = !!checked)}
72
+ />
73
+ </SettingsValue>
74
+
75
+ <SettingsValue label={t('settings folding label')}>
76
+ <Input.Switch checked={settings.folding} onCheckedChange={(checked) => (settings.folding = !!checked)} />
77
+ </SettingsValue>
78
+
79
+ <SettingsValue label={t('settings experimental label')}>
80
+ <Input.Switch
81
+ checked={settings.experimental}
82
+ onCheckedChange={(checked) => (settings.experimental = !!checked)}
83
+ />
84
+ </SettingsValue>
85
+
86
+ <SettingsValue
87
+ label={t('settings debug label')}
88
+ secondary={
89
+ settings.debug ? (
90
+ <Input.Root>
91
+ <Input.TextArea
92
+ rows={5}
93
+ value={settings.typewriter}
94
+ onChange={({ target: { value } }) => (settings.typewriter = value)}
95
+ placeholder={t('settings debug placeholder')}
96
+ />
97
+ </Input.Root>
98
+ ) : undefined
99
+ }
100
+ >
101
+ <Input.Switch checked={settings.debug} onCheckedChange={(checked) => (settings.debug = !!checked)} />
102
+ </SettingsValue>
103
+ </>
104
+ );
105
+ };
@@ -0,0 +1,118 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React, { type FC, useState } from 'react';
8
+
9
+ import { create } from '@dxos/echo-schema';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { faker } from '@dxos/random';
12
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
13
+ import { Tooltip, useThemeContext } from '@dxos/react-ui';
14
+ import {
15
+ type Action,
16
+ type Comment,
17
+ type EditorViewMode,
18
+ comments,
19
+ createBasicExtensions,
20
+ createDataExtensions,
21
+ createMarkdownExtensions,
22
+ createThemeExtensions,
23
+ decorateMarkdown,
24
+ editorContent,
25
+ formattingKeymap,
26
+ Toolbar,
27
+ translations,
28
+ useActionHandler,
29
+ useComments,
30
+ useFormattingState,
31
+ useTextEditor,
32
+ } from '@dxos/react-ui-editor';
33
+ import { textBlockWidth } from '@dxos/react-ui-theme';
34
+ import { withTheme } from '@dxos/storybook-utils';
35
+
36
+ import { TextType } from '../types';
37
+
38
+ faker.seed(101);
39
+
40
+ const Story: FC<{ content: string }> = ({ content }) => {
41
+ const { themeMode } = useThemeContext();
42
+ const [text] = useState(createEchoObject(create(TextType, { content })));
43
+ const [formattingState, formattingObserver] = useFormattingState();
44
+ const [viewMode, setViewMode] = useState<EditorViewMode>('preview');
45
+ const { parentRef, view } = useTextEditor(() => {
46
+ return {
47
+ id: text.id,
48
+ initialValue: text.content,
49
+ extensions: [
50
+ formattingObserver,
51
+ createBasicExtensions({ readonly: viewMode === 'readonly' }),
52
+ createMarkdownExtensions({ themeMode }),
53
+ createThemeExtensions({ themeMode, slots: { editor: { className: editorContent } } }),
54
+ createDataExtensions({ id: text.id, text: createDocAccessor(text, ['content']) }),
55
+ comments({
56
+ onCreate: ({ cursor }) => {
57
+ const id = PublicKey.random().toHex();
58
+ setComments((comments) => [...comments, { id, cursor }]);
59
+ return id;
60
+ },
61
+ }),
62
+ formattingKeymap(),
63
+ ...(viewMode !== 'source' ? [decorateMarkdown()] : []),
64
+ ],
65
+ };
66
+ }, [text, formattingObserver, viewMode, themeMode]);
67
+
68
+ const handleToolbarAction = useActionHandler(view);
69
+ const handleAction = (action: Action) => {
70
+ if (action.type === 'view-mode') {
71
+ setViewMode(action.data);
72
+ } else {
73
+ handleToolbarAction?.(action);
74
+ }
75
+ };
76
+
77
+ const [_comments, setComments] = useState<Comment[]>([]);
78
+ useComments(view, text.id, _comments);
79
+
80
+ return (
81
+ <Tooltip.Provider>
82
+ <div role='none' className='fixed inset-0 flex flex-col'>
83
+ <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
84
+ <Toolbar.View mode={viewMode} />
85
+ <Toolbar.Markdown />
86
+ <Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
87
+ <Toolbar.Separator />
88
+ <Toolbar.Actions />
89
+ </Toolbar.Root>
90
+ <div ref={parentRef} />
91
+ </div>
92
+ </Tooltip.Provider>
93
+ );
94
+ };
95
+
96
+ export default {
97
+ title: 'react-ui-editor/Toolbar',
98
+ component: Toolbar,
99
+ decorators: [withTheme],
100
+ parameters: { translations, layout: 'fullscreen' },
101
+ render: (args: any) => <Story {...args} />,
102
+ } as any;
103
+
104
+ const content = [
105
+ '# Demo',
106
+ '',
107
+ 'The editor supports **Markdown** styles.',
108
+ '',
109
+ faker.lorem.paragraph({ min: 5, max: 8 }),
110
+ '',
111
+ '',
112
+ ].join('\n');
113
+
114
+ export const Default = {
115
+ args: {
116
+ content,
117
+ },
118
+ };
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { type LazyExoticComponent } from 'react';
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
+ export * from './MarkdownSettings';
17
+
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'));