@dxos/react-ui-editor 0.6.5 → 0.6.6-main.e1a6e1f

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 (41) hide show
  1. package/dist/lib/browser/index.mjs +137 -41
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -0
  5. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -1
  7. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +7 -0
  9. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts +2 -1
  11. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +4 -0
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/extensions/debug.d.ts +3 -0
  16. package/dist/types/src/extensions/debug.d.ts.map +1 -0
  17. package/dist/types/src/extensions/index.d.ts +1 -0
  18. package/dist/types/src/extensions/index.d.ts.map +1 -1
  19. package/dist/types/src/extensions/markdown/action.d.ts +1 -1
  20. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/link.d.ts +3 -1
  22. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  23. package/dist/types/src/extensions/modes.d.ts +7 -4
  24. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  25. package/dist/types/src/hooks/useTextEditor.stories.d.ts +4 -0
  26. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  27. package/dist/types/src/translations.d.ts +4 -0
  28. package/dist/types/src/translations.d.ts.map +1 -1
  29. package/package.json +27 -27
  30. package/src/components/TextEditor/TextEditor.stories.tsx +2 -2
  31. package/src/components/TextEditor/TextEditor.tsx +3 -3
  32. package/src/components/Toolbar/Toolbar.stories.tsx +17 -7
  33. package/src/components/Toolbar/Toolbar.tsx +93 -3
  34. package/src/extensions/autocomplete.ts +3 -3
  35. package/src/extensions/comments.ts +1 -0
  36. package/src/extensions/debug.ts +15 -0
  37. package/src/extensions/index.ts +1 -0
  38. package/src/extensions/markdown/action.ts +1 -0
  39. package/src/extensions/modes.ts +9 -6
  40. package/src/hooks/useTextEditor.stories.tsx +13 -14
  41. package/src/translations.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.6.5",
3
+ "version": "0.6.6-main.e1a6e1f",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -21,7 +21,7 @@
21
21
  "src"
22
22
  ],
23
23
  "dependencies": {
24
- "@codemirror/autocomplete": "^6.17.0",
24
+ "@codemirror/autocomplete": "^6.18.0",
25
25
  "@codemirror/commands": "^6.6.0",
26
26
  "@codemirror/lang-javascript": "^6.2.2",
27
27
  "@codemirror/lang-markdown": "^6.2.5",
@@ -35,7 +35,7 @@
35
35
  "@fluentui/react-tabster": "^9.19.0",
36
36
  "@lezer/common": "^1.2.1",
37
37
  "@lezer/highlight": "^1.2.0",
38
- "@lezer/markdown": "^1.2.0",
38
+ "@lezer/markdown": "^1.3.0",
39
39
  "@radix-ui/react-context": "^1.0.0",
40
40
  "@replit/codemirror-vim": "^6.0.14",
41
41
  "@replit/codemirror-vscode-keymap": "^6.0.2",
@@ -47,19 +47,19 @@
47
47
  "lodash.sortby": "^4.7.0",
48
48
  "react-dropzone": "^14.2.3",
49
49
  "style-mod": "^4.1.0",
50
- "@dxos/async": "0.6.5",
51
- "@dxos/context": "0.6.5",
52
- "@dxos/debug": "0.6.5",
53
- "@dxos/display-name": "0.6.5",
54
- "@dxos/echo-schema": "0.6.5",
55
- "@dxos/automerge": "0.6.5",
56
- "@dxos/log": "0.6.5",
57
- "@dxos/invariant": "0.6.5",
58
- "@dxos/protocols": "0.6.5",
59
- "@dxos/react-ui-theme": "0.6.5",
60
- "@dxos/react-ui": "0.6.5",
61
- "@dxos/react-async": "0.6.5",
62
- "@dxos/util": "0.6.5"
50
+ "@dxos/async": "0.6.6-main.e1a6e1f",
51
+ "@dxos/automerge": "0.6.6-main.e1a6e1f",
52
+ "@dxos/context": "0.6.6-main.e1a6e1f",
53
+ "@dxos/debug": "0.6.6-main.e1a6e1f",
54
+ "@dxos/invariant": "0.6.6-main.e1a6e1f",
55
+ "@dxos/echo-schema": "0.6.6-main.e1a6e1f",
56
+ "@dxos/log": "0.6.6-main.e1a6e1f",
57
+ "@dxos/display-name": "0.6.6-main.e1a6e1f",
58
+ "@dxos/protocols": "0.6.6-main.e1a6e1f",
59
+ "@dxos/react-async": "0.6.6-main.e1a6e1f",
60
+ "@dxos/react-ui": "0.6.6-main.e1a6e1f",
61
+ "@dxos/react-ui-theme": "0.6.6-main.e1a6e1f",
62
+ "@dxos/util": "0.6.6-main.e1a6e1f"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@phosphor-icons/react": "^2.1.5",
@@ -85,22 +85,22 @@
85
85
  "vite-plugin-top-level-await": "^1.4.1",
86
86
  "vite-plugin-wasm": "^3.3.0",
87
87
  "vitest": "^1.5.0",
88
- "@dxos/automerge": "0.6.5",
89
- "@dxos/config": "0.6.5",
90
- "@dxos/echo-signals": "0.6.5",
91
- "@dxos/keyboard": "0.6.5",
92
- "@dxos/echo-typegen": "0.6.5",
93
- "@dxos/react-ui": "0.6.5",
94
- "@dxos/random": "0.6.5",
95
- "@dxos/react-client": "0.6.5",
96
- "@dxos/storybook-utils": "0.6.5",
97
- "@braneframe/types": "0.6.5"
88
+ "@dxos/automerge": "0.6.6-main.e1a6e1f",
89
+ "@dxos/config": "0.6.6-main.e1a6e1f",
90
+ "@dxos/echo-signals": "0.6.6-main.e1a6e1f",
91
+ "@dxos/keyboard": "0.6.6-main.e1a6e1f",
92
+ "@dxos/echo-typegen": "0.6.6-main.e1a6e1f",
93
+ "@dxos/random": "0.6.6-main.e1a6e1f",
94
+ "@dxos/react-client": "0.6.6-main.e1a6e1f",
95
+ "@dxos/react-ui": "0.6.6-main.e1a6e1f",
96
+ "@dxos/storybook-utils": "0.6.6-main.e1a6e1f",
97
+ "@braneframe/types": "0.6.6-main.e1a6e1f"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@phosphor-icons/react": "^2.1.5",
101
101
  "react": "^18.0.0",
102
102
  "react-dom": "^18.0.0",
103
- "@dxos/react-client": "0.6.5"
103
+ "@dxos/react-client": "0.6.6-main.e1a6e1f"
104
104
  },
105
105
  "publishConfig": {
106
106
  "access": "public"
@@ -23,7 +23,7 @@ import { withTheme } from '@dxos/storybook-utils';
23
23
 
24
24
  import { TextEditor, type TextEditorProps } from './TextEditor';
25
25
  import {
26
- EditorModes,
26
+ InputModeExtensions,
27
27
  annotations,
28
28
  autocomplete,
29
29
  blast,
@@ -511,7 +511,7 @@ export const Vim = {
511
511
  render: () => (
512
512
  <Story
513
513
  text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
514
- extensions={[defaults, EditorModes.vim]}
514
+ extensions={[defaults, InputModeExtensions.vim]}
515
515
  />
516
516
  ),
517
517
  };
@@ -19,7 +19,7 @@ import { log } from '@dxos/log';
19
19
  import { useDefaultValue } from '@dxos/react-ui';
20
20
  import { isNotFalsy } from '@dxos/util';
21
21
 
22
- import { documentId, editorMode, focusEvent } from '../../extensions';
22
+ import { documentId, editorInputMode, focusEvent } from '../../extensions';
23
23
  import { logChanges } from '../../util';
24
24
 
25
25
  export type CursorInfo = {
@@ -143,7 +143,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
143
143
  }
144
144
 
145
145
  // Remove tabster attribute (rely on custom keymap).
146
- if (state.facet(editorMode).noTabster) {
146
+ if (state.facet(editorInputMode).noTabster) {
147
147
  rootRef.current?.removeAttribute('data-tabster');
148
148
  }
149
149
 
@@ -153,7 +153,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
153
153
  log('destroy', { id, instanceId });
154
154
  view?.destroy();
155
155
  };
156
- }, [id, selection, scrollTo, editorMode, extensions]);
156
+ }, [id, selection, scrollTo, extensions]);
157
157
 
158
158
  // Focus editor on Enter (e.g., when tabbing to this component).
159
159
  const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
@@ -10,13 +10,14 @@ import { TextType } from '@braneframe/types';
10
10
  import { create } from '@dxos/echo-schema';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { faker } from '@dxos/random';
13
- import { createDocAccessor } from '@dxos/react-client/echo';
13
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
14
14
  import { Tooltip, useThemeContext } from '@dxos/react-ui';
15
15
  import { textBlockWidth } from '@dxos/react-ui-theme';
16
16
  import { withTheme } from '@dxos/storybook-utils';
17
17
 
18
18
  import { Toolbar } from './Toolbar';
19
19
  import {
20
+ type Action,
20
21
  type Comment,
21
22
  comments,
22
23
  createBasicExtensions,
@@ -24,6 +25,7 @@ import {
24
25
  createMarkdownExtensions,
25
26
  createThemeExtensions,
26
27
  decorateMarkdown,
28
+ type EditorViewMode,
27
29
  formattingKeymap,
28
30
  image,
29
31
  table,
@@ -37,15 +39,16 @@ faker.seed(101);
37
39
 
38
40
  const Story: FC<{ content: string }> = ({ content }) => {
39
41
  const { themeMode } = useThemeContext();
40
- const [text] = useState(create(TextType, { content }));
42
+ const [text] = useState(createEchoObject(create(TextType, { content })));
41
43
  const [formattingState, formattingObserver] = useFormattingState();
44
+ const [viewMode, setViewMode] = useState<EditorViewMode>('preview');
42
45
  const { parentRef, view } = useTextEditor(() => {
43
46
  return {
44
47
  id: text.id,
45
48
  doc: text.content,
46
49
  extensions: [
47
50
  formattingObserver,
48
- createBasicExtensions(),
51
+ createBasicExtensions({ readonly: viewMode === 'readonly' }),
49
52
  createMarkdownExtensions({ themeMode }),
50
53
  createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
51
54
  createDataExtensions({ id: text.id, text: createDocAccessor(text, ['content']) }),
@@ -56,15 +59,21 @@ const Story: FC<{ content: string }> = ({ content }) => {
56
59
  return id;
57
60
  },
58
61
  }),
59
- decorateMarkdown(),
60
62
  formattingKeymap(),
61
63
  image(),
62
- table(),
64
+ ...(viewMode !== 'source' ? [decorateMarkdown(), table()] : []),
63
65
  ],
64
66
  };
65
- }, [text, formattingObserver, themeMode]);
67
+ }, [text, formattingObserver, viewMode, themeMode]);
66
68
 
67
- const handleAction = useActionHandler(view);
69
+ const handleToolbarAction = useActionHandler(view);
70
+ const handleAction = (action: Action) => {
71
+ if (action.type === 'view-mode') {
72
+ setViewMode(action.data);
73
+ } else {
74
+ handleToolbarAction?.(action);
75
+ }
76
+ };
68
77
 
69
78
  const [_comments, setComments] = useState<Comment[]>([]);
70
79
  useComments(view, text.id, _comments);
@@ -73,6 +82,7 @@ const Story: FC<{ content: string }> = ({ content }) => {
73
82
  <Tooltip.Provider>
74
83
  <div role='none' className='fixed inset-0 flex flex-col'>
75
84
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
85
+ <Toolbar.View mode={viewMode} />
76
86
  <Toolbar.Markdown />
77
87
  <Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
78
88
  <Toolbar.Separator />
@@ -26,6 +26,9 @@ import {
26
26
  TextItalic,
27
27
  CaretDown,
28
28
  Check,
29
+ PencilSimpleSlash,
30
+ MarkdownLogo,
31
+ PencilSimple,
29
32
  } from '@phosphor-icons/react';
30
33
  import { createContext } from '@radix-ui/react-context';
31
34
  import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
@@ -45,7 +48,7 @@ import {
45
48
  } from '@dxos/react-ui';
46
49
  import { getSize } from '@dxos/react-ui-theme';
47
50
 
48
- import { type Action, type ActionType, type Formatting } from '../../extensions';
51
+ import { type EditorViewMode, type Action, type ActionType, type Formatting, EditorViewModes } from '../../extensions';
49
52
  import { translationKey } from '../../translations';
50
53
 
51
54
  const iconStyles = getSize(5);
@@ -60,7 +63,7 @@ const ToolbarSeparator = () => <div role='separator' className='grow' />;
60
63
 
61
64
  export type ToolbarProps = ThemedClassName<
62
65
  PropsWithChildren<{
63
- state: (Formatting & { comment?: boolean; selection?: boolean }) | undefined;
66
+ state: (Formatting & { comment?: boolean; mode?: EditorViewMode; selection?: boolean }) | undefined;
64
67
  onAction?: (action: Action) => void;
65
68
  }>
66
69
  >;
@@ -72,7 +75,10 @@ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) =>
72
75
  <ToolbarContextProvider onAction={onAction} state={state}>
73
76
  <DensityProvider density='fine'>
74
77
  <ElevationProvider elevation='chrome'>
75
- <NaturalToolbar.Root classNames={['is-full shrink-0 overflow-x-auto overflow-y-hidden p-1', classNames]}>
78
+ <NaturalToolbar.Root
79
+ classNames={['p-1 is-full shrink-0 overflow-x-auto overflow-y-hidden', classNames]}
80
+ style={{ contain: 'layout' }}
81
+ >
76
82
  {children}
77
83
  </NaturalToolbar.Root>
78
84
  </ElevationProvider>
@@ -134,6 +140,89 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
134
140
  );
135
141
  };
136
142
 
143
+ //
144
+ // View Mode
145
+ //
146
+
147
+ const ViewModeIcons: Record<EditorViewMode, Icon> = {
148
+ preview: PencilSimple,
149
+ readonly: PencilSimpleSlash,
150
+ source: MarkdownLogo,
151
+ };
152
+
153
+ const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
154
+ const { t } = useTranslation(translationKey);
155
+ const { onAction } = useToolbarContext('ViewMode');
156
+ const ModeIcon = ViewModeIcons[mode ?? 'preview'];
157
+ const suppressNextTooltip = useRef<boolean>(false);
158
+ const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
159
+ const [selectOpen, setSelectOpen] = useState<boolean>(false);
160
+ return (
161
+ <Tooltip.Root
162
+ open={tooltipOpen}
163
+ onOpenChange={(nextOpen) => {
164
+ if (nextOpen && suppressNextTooltip.current) {
165
+ suppressNextTooltip.current = false;
166
+ return setTooltipOpen(false);
167
+ } else {
168
+ return setTooltipOpen(nextOpen);
169
+ }
170
+ }}
171
+ >
172
+ {/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
173
+ heading). Determine the root cause and fix or report to Radix. */}
174
+ <DropdownMenu.Root
175
+ open={selectOpen}
176
+ onOpenChange={(nextOpen: boolean) => {
177
+ if (!nextOpen) {
178
+ suppressNextTooltip.current = true;
179
+ }
180
+ return setSelectOpen(nextOpen);
181
+ }}
182
+ >
183
+ <Tooltip.Trigger asChild>
184
+ <NaturalToolbar.Button asChild>
185
+ <DropdownMenu.Trigger asChild>
186
+ <Button variant='ghost' classNames={buttonStyles}>
187
+ <span className='sr-only'>{t('mode label')}</span>
188
+ <ModeIcon className={iconStyles} />
189
+ <CaretDown />
190
+ </Button>
191
+ </DropdownMenu.Trigger>
192
+ </NaturalToolbar.Button>
193
+ </Tooltip.Trigger>
194
+ <DropdownMenu.Portal>
195
+ <DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
196
+ <DropdownMenu.Viewport>
197
+ {EditorViewModes.map((value) => {
198
+ const Icon = ViewModeIcons[value];
199
+ return (
200
+ <DropdownMenu.CheckboxItem
201
+ key={value}
202
+ checked={value === mode}
203
+ onClick={() => onAction?.({ type: 'view-mode', data: value })}
204
+ >
205
+ <Icon className={iconStyles} />
206
+ <span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
207
+ <Check className={value === mode ? 'visible' : 'invisible'} />
208
+ </DropdownMenu.CheckboxItem>
209
+ );
210
+ })}
211
+ </DropdownMenu.Viewport>
212
+ <DropdownMenu.Arrow />
213
+ </DropdownMenu.Content>
214
+ </DropdownMenu.Portal>
215
+ </DropdownMenu.Root>
216
+ <Tooltip.Portal>
217
+ <Tooltip.Content {...tooltipProps}>
218
+ {t('view mode label')}
219
+ <Tooltip.Arrow />
220
+ </Tooltip.Content>
221
+ </Tooltip.Portal>
222
+ </Tooltip.Root>
223
+ );
224
+ };
225
+
137
226
  //
138
227
  // Heading
139
228
  //
@@ -420,6 +509,7 @@ export const Toolbar = {
420
509
  Root: ToolbarRoot,
421
510
  Button: ToolbarToggleButton,
422
511
  Separator: ToolbarSeparator,
512
+ View: MarkdownView,
423
513
  Markdown: MarkdownStandard,
424
514
  Custom: MarkdownCustom,
425
515
  Actions: MarkdownActions,
@@ -19,13 +19,14 @@ import { keymap } from '@codemirror/view';
19
19
  export type AutocompleteResult = Completion;
20
20
 
21
21
  export type AutocompleteOptions = {
22
+ activateOnTyping?: boolean;
22
23
  onSearch: (text: string) => Completion[];
23
24
  };
24
25
 
25
26
  /**
26
27
  * Autocomplete extension.
27
28
  */
28
- export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
29
+ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions) => {
29
30
  return [
30
31
  // https://codemirror.net/docs/ref/#view.keymap
31
32
  // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
@@ -35,7 +36,7 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
35
36
  // https://codemirror.net/examples/autocompletion
36
37
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
37
38
  autocompletion({
38
- activateOnTyping: false,
39
+ activateOnTyping,
39
40
 
40
41
  // closeOnBlur: false,
41
42
  // defaultKeymap: false,
@@ -44,7 +45,6 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
44
45
  tooltipClass: () => 'shadow rounded',
45
46
  }),
46
47
 
47
- // TODO(burdon): Option to create new page?
48
48
  // TODO(burdon): Optional decoration via addToOptions
49
49
  markdownLanguage.data.of({
50
50
  autocomplete: (context: CompletionContext): CompletionResult | null => {
@@ -40,6 +40,7 @@ import { callbackWrapper } from '../util';
40
40
  // State management.
41
41
  //
42
42
 
43
+ // TODO(wittjosiah): Factor out, not comments-specific.
43
44
  const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
44
45
 
45
46
  type CommentState = {
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
7
+
8
+ // eslint-disable-next-line no-console
9
+ export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
10
+ const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
11
+ return StateField.define<any>({
12
+ create: (state) => logTokens(state),
13
+ update: (_: RangeSet<any>, tr: Transaction) => logTokens(tr.state),
14
+ });
15
+ };
@@ -10,6 +10,7 @@ export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
+ export * from './debug';
13
14
  export * from './doc';
14
15
  export * from './dnd';
15
16
  export * from './factories';
@@ -25,6 +25,7 @@ import {
25
25
  import { createComment } from '../comments';
26
26
 
27
27
  export type ActionType =
28
+ | 'view-mode'
28
29
  | 'blockquote'
29
30
  | 'strong'
30
31
  | 'codeblock'
@@ -9,28 +9,31 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
9
9
 
10
10
  export const focusEvent = 'focus.container';
11
11
 
12
- export type EditorMode = 'default' | 'vim' | 'vscode' | undefined;
12
+ export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
13
+ export type EditorViewMode = (typeof EditorViewModes)[number];
14
+ export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
15
+ export type EditorInputMode = (typeof EditorInputModes)[number];
13
16
 
14
- export type EditorConfig = {
17
+ export type EditorInputConfig = {
15
18
  type: string;
16
19
  noTabster?: boolean;
17
20
  };
18
21
 
19
- export const editorMode = Facet.define<EditorConfig, EditorConfig>({
22
+ export const editorInputMode = Facet.define<EditorInputConfig, EditorInputConfig>({
20
23
  combine: (modes) => modes[0] ?? {},
21
24
  });
22
25
 
23
- export const EditorModes: { [mode: string]: Extension } = {
26
+ export const InputModeExtensions: { [mode: string]: Extension } = {
24
27
  default: [],
25
28
  vscode: [
26
29
  // https://github.com/replit/codemirror-vscode-keymap
27
- editorMode.of({ type: 'vscode' }),
30
+ editorInputMode.of({ type: 'vscode' }),
28
31
  keymap.of(vscodeKeymap),
29
32
  ],
30
33
  vim: [
31
34
  // https://github.com/replit/codemirror-vim
32
35
  vim(),
33
- editorMode.of({ type: 'vim', noTabster: true }),
36
+ editorInputMode.of({ type: 'vim', noTabster: true }),
34
37
  keymap.of([
35
38
  {
36
39
  key: 'Alt-Escape',
@@ -13,10 +13,9 @@ import { withTheme } from '@dxos/storybook-utils';
13
13
  import { useActionHandler } from './useActionHandler';
14
14
  import { useTextEditor } from './useTextEditor';
15
15
  import { Toolbar } from '../components';
16
- import { createBasicExtensions, createThemeExtensions } from '../extensions';
16
+ import { createBasicExtensions, createThemeExtensions, InputModeExtensions } from '../extensions';
17
17
  import {
18
- type EditorMode,
19
- EditorModes,
18
+ type EditorInputMode,
20
19
  decorateMarkdown,
21
20
  createMarkdownExtensions,
22
21
  formattingKeymap,
@@ -36,13 +35,13 @@ type StoryProps = {
36
35
  const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
37
36
  const { themeMode } = useThemeContext();
38
37
  const [formattingState, trackFormatting] = useFormattingState();
39
- const [editorMode, setEditorMode] = useState<EditorMode>('default');
38
+ const [editorInputMode, setEditorInputMode] = useState<EditorInputMode>('default');
40
39
  const { parentRef, view } = useTextEditor(
41
40
  () => ({
42
41
  autoFocus,
43
42
  doc,
44
43
  extensions: [
45
- editorMode ? EditorModes[editorMode] : [],
44
+ editorInputMode ? InputModeExtensions[editorInputMode] : [],
46
45
  createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
47
46
  createMarkdownExtensions({ themeMode }),
48
47
  createThemeExtensions({ themeMode }),
@@ -53,7 +52,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
53
52
  trackFormatting,
54
53
  ],
55
54
  }),
56
- [editorMode, themeMode, placeholder, readonly],
55
+ [editorInputMode, themeMode, placeholder, readonly],
57
56
  );
58
57
 
59
58
  const handleAction = useActionHandler(view);
@@ -64,7 +63,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
64
63
  <div role='none' className={mx('fixed inset-0 flex flex-col')}>
65
64
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
66
65
  <Toolbar.Markdown />
67
- <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
66
+ <EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
68
67
  </Toolbar.Root>
69
68
  <div role='none' className='grow overflow-hidden'>
70
69
  <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
@@ -73,17 +72,17 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
73
72
  );
74
73
  };
75
74
 
76
- const EditorModeToolbar = ({
77
- editorMode,
78
- setEditorMode,
75
+ const EditorInputModeToolbar = ({
76
+ editorInputMode,
77
+ setEditorInputMode,
79
78
  }: {
80
- editorMode: EditorMode;
81
- setEditorMode: (mode: EditorMode) => void;
79
+ editorInputMode: EditorInputMode;
80
+ setEditorInputMode: (mode: EditorInputMode) => void;
82
81
  }) => {
83
82
  return (
84
- <Select.Root value={editorMode} onValueChange={(value) => setEditorMode(value as EditorMode)}>
83
+ <Select.Root value={editorInputMode} onValueChange={(value) => setEditorInputMode(value as EditorInputMode)}>
85
84
  <NaturalToolbar.Button asChild>
86
- <Select.TriggerButton variant='ghost'>{editorMode}</Select.TriggerButton>
85
+ <Select.TriggerButton variant='ghost'>{editorInputMode}</Select.TriggerButton>
87
86
  </NaturalToolbar.Button>
88
87
  <Select.Portal>
89
88
  <Select.Content>
@@ -25,6 +25,10 @@ export default [
25
25
  'heading level label_zero': 'Paragraph',
26
26
  'heading level label_one': 'Heading level {{count}}',
27
27
  'heading level label_other': 'Heading level {{count}}',
28
+ 'view mode label': 'Editor view',
29
+ 'preview mode label': 'Live preview',
30
+ 'readonly mode label': 'Read only',
31
+ 'source mode label': 'Source',
28
32
  },
29
33
  },
30
34
  },