@dxos/react-ui-editor 0.4.2 → 0.4.3-main.c3bfede

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 +233 -185
  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/MarkdownEditor.stories.d.ts +20 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +20 -0
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +22 -2
  9. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +20 -0
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts +7 -10
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -6
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/{semaphore.d.ts → sync.d.ts} +6 -5
  18. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -0
  19. package/dist/types/src/index.d.ts +1 -1
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/styles/index.d.ts +1 -0
  22. package/dist/types/src/styles/index.d.ts.map +1 -1
  23. package/dist/types/src/styles/layout.d.ts +2 -0
  24. package/dist/types/src/styles/layout.d.ts.map +1 -0
  25. package/dist/types/src/translations.d.ts +20 -0
  26. package/dist/types/src/translations.d.ts.map +1 -0
  27. package/package.json +24 -24
  28. package/src/components/TextEditor/MarkdownEditor.stories.tsx +13 -16
  29. package/src/components/TextEditor/TextEditor.stories.tsx +11 -14
  30. package/src/components/TextEditor/TextEditor.tsx +3 -3
  31. package/src/components/TextEditor/automerge.stories.tsx +38 -44
  32. package/src/components/TextEditor/hooks.stories.tsx +15 -16
  33. package/src/components/Toolbar/Toolbar.stories.tsx +16 -12
  34. package/src/components/Toolbar/Toolbar.tsx +156 -129
  35. package/src/extensions/automerge/automerge.ts +5 -21
  36. package/src/extensions/automerge/{semaphore.ts → sync.ts} +32 -34
  37. package/src/index.ts +1 -1
  38. package/src/styles/index.ts +1 -0
  39. package/src/styles/layout.ts +6 -0
  40. package/src/translations.ts +25 -0
  41. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +0 -1
@@ -4,7 +4,6 @@
4
4
 
5
5
  import {
6
6
  type Icon,
7
- CaretRight,
8
7
  ChatText,
9
8
  Code,
10
9
  CodeBlock,
@@ -23,14 +22,25 @@ import {
23
22
  TextHFive,
24
23
  TextHSix,
25
24
  TextItalic,
25
+ Quotes,
26
+ CaretDown,
26
27
  } from '@phosphor-icons/react';
27
28
  import { createContext } from '@radix-ui/react-context';
28
29
  import React, { type PropsWithChildren } from 'react';
29
30
 
30
- import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
31
- import { getSize, mx } from '@dxos/react-ui-theme';
31
+ import {
32
+ DensityProvider,
33
+ ElevationProvider,
34
+ Select,
35
+ type ThemedClassName,
36
+ Toolbar as NaturalToolbar,
37
+ type ToolbarToggleGroupItemProps,
38
+ useTranslation,
39
+ } from '@dxos/react-ui';
40
+ import { getSize } from '@dxos/react-ui-theme';
32
41
 
33
42
  import { type Formatting } from '../../extensions';
43
+ import { translationKey } from '../../translations';
34
44
 
35
45
  const HeadingIcons: { [key: string]: Icon } = {
36
46
  '0': Paragraph,
@@ -66,61 +76,49 @@ export type Action = {
66
76
  data?: any;
67
77
  };
68
78
 
69
- export type ToolbarProps = PropsWithChildren<{
70
- state: Formatting | null;
71
- onAction?: (action: Action) => void;
72
- }>;
79
+ export type ToolbarProps = ThemedClassName<
80
+ PropsWithChildren<{
81
+ state: Formatting | null;
82
+ onAction?: (action: Action) => void;
83
+ }>
84
+ >;
73
85
 
74
86
  const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
75
87
 
76
- const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
88
+ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) => {
77
89
  return (
78
90
  <ToolbarContextProvider onAction={onAction} state={state}>
79
91
  <DensityProvider density='fine'>
80
- <div role='toolbar' className='flex w-full shrink-0 p-1 gap-4 items-center whitespace-nowrap overflow-hidden'>
81
- {children}
82
- </div>
92
+ <ElevationProvider elevation='chrome'>
93
+ <NaturalToolbar.Root classNames={['is-full shrink-0 overflow-x-auto p-1', classNames]}>
94
+ {children}
95
+ </NaturalToolbar.Root>
96
+ </ElevationProvider>
83
97
  </DensityProvider>
84
98
  </ToolbarContextProvider>
85
99
  );
86
100
  };
87
101
 
88
- type ToolbarButtonProps = {
102
+ type ToolbarButtonProps = ToolbarToggleGroupItemProps & {
89
103
  Icon: Icon;
90
- disable?: (state: Formatting) => boolean;
91
- getState?: (state: Formatting) => boolean;
92
- onClick: (state: Formatting | null) => Action | undefined;
93
- } & NonNullable<Pick<ButtonProps, 'title'>>;
94
-
95
- const ToolbarButton = ({ Icon, onClick, title, getState, disable }: ToolbarButtonProps) => {
96
- const { onAction, state } = useToolbarContext('ToolbarButton');
97
- const active = getState && state ? getState(state) : false;
98
- const disabled = disable && state ? disable(state) : false;
104
+ };
99
105
 
100
- const handleClick = (event: React.MouseEvent) => {
101
- const action = onClick(state);
102
- if (action) {
103
- onAction?.(action);
104
- event.preventDefault();
105
- }
106
- };
106
+ const buttonStyles = 'min-bs-0 p-2';
107
+ const iconStyles = getSize(4);
107
108
 
109
+ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
108
110
  return (
109
- <Button
110
- variant='ghost'
111
- classNames={mx('p-2', active && 'ring-[1px]')}
112
- onMouseDown={handleClick}
113
- title={title}
114
- disabled={disabled}
115
- >
116
- <Icon className={getSize(5)} />
117
- </Button>
111
+ <NaturalToolbar.ToggleGroupItem variant='ghost' {...props} classNames={buttonStyles}>
112
+ <Icon className={iconStyles} />
113
+ <span className='sr-only'>{children}</span>
114
+ </NaturalToolbar.ToggleGroupItem>
118
115
  );
119
116
  };
120
117
 
121
- const ToolbarSeparator = () => <div className='grow' />;
118
+ const ToolbarSeparator = () => <div role='separator' className='grow' />;
122
119
 
123
120
  const MarkdownHeading = () => {
121
+ const { t } = useTranslation(translationKey);
124
122
  const { onAction, state } = useToolbarContext('MarkdownFormatting');
125
123
  const blockType = state ? state.blockType : 'paragraph';
126
124
  const header = blockType && /heading(\d)/.exec(blockType);
@@ -132,11 +130,16 @@ const MarkdownHeading = () => {
132
130
  value={value ?? '0'}
133
131
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
134
132
  >
135
- <Select.TriggerButton>
136
- <HeadingIcon className={getSize(5)} />
137
- </Select.TriggerButton>
133
+ <Select.Trigger asChild>
134
+ <NaturalToolbar.Button variant='ghost' classNames={buttonStyles}>
135
+ <span className='sr-only'>{t('heading label')}</span>
136
+ <HeadingIcon className={iconStyles} />
137
+ <CaretDown className={getSize(2)} weight='bold' />
138
+ </NaturalToolbar.Button>
139
+ </Select.Trigger>
138
140
  <Select.Portal>
139
141
  <Select.Content>
142
+ <Select.ScrollUpButton />
140
143
  <Select.Viewport>
141
144
  <Select.Option value='0'>Paragraph</Select.Option>
142
145
  {[1, 2, 3, 4, 5, 6].map((level) => (
@@ -145,99 +148,117 @@ const MarkdownHeading = () => {
145
148
  </Select.Option>
146
149
  ))}
147
150
  </Select.Viewport>
151
+ <Select.ScrollDownButton />
152
+ <Select.Arrow />
148
153
  </Select.Content>
149
154
  </Select.Portal>
150
155
  </Select.Root>
151
156
  );
152
157
  };
153
158
 
154
- const MarkdownStyles = () => (
155
- <div role='none'>
156
- <ToolbarButton
157
- Icon={TextB}
158
- title='String'
159
- disable={(s) => s.blockType === 'codeblock'}
160
- getState={(s) => s.strong}
161
- onClick={(s) => ({ type: 'strong', data: s ? !s.strong : null })}
162
- />
163
- <ToolbarButton
164
- Icon={TextItalic}
165
- title='Emphasis'
166
- disable={(s) => s.blockType === 'codeblock'}
167
- getState={(s) => s.emphasis}
168
- onClick={(s) => ({ type: 'emphasis', data: s ? !s.emphasis : null })}
169
- />
170
- <ToolbarButton
171
- Icon={TextStrikethrough}
172
- title='Strike-through'
173
- disable={(s) => s.blockType === 'codeblock'}
174
- getState={(s) => s.strikethrough}
175
- onClick={(s) => ({ type: 'strikethrough', data: s ? !s.strikethrough : null })}
176
- />
177
- <ToolbarButton
178
- Icon={Code}
179
- title='Inline code'
180
- disable={(s) => s.blockType === 'codeblock'}
181
- getState={(s) => s.code}
182
- onClick={(s) => ({ type: 'code', data: s ? !s.code : null })}
183
- />
184
- </div>
185
- );
159
+ const markdownStyles = [
160
+ { key: 'strong' as const, Icon: TextB },
161
+ { key: 'emphasis' as const, Icon: TextItalic },
162
+ { key: 'strikethrough' as const, Icon: TextStrikethrough },
163
+ { key: 'code' as const, Icon: Code },
164
+ { key: 'link' as const, Icon: Link },
165
+ // <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
166
+ // <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
167
+ ];
186
168
 
187
- const MarkdownLists = () => (
188
- <div role='none'>
189
- <ToolbarButton
190
- Icon={ListBullets}
191
- title='Bullet list'
192
- getState={(s) => s.listStyle === 'bullet'}
193
- onClick={(s) => ({ type: 'list-bullet', data: s ? s.listStyle !== 'bullet' : null })}
194
- />
195
- <ToolbarButton
196
- Icon={ListNumbers}
197
- title='Numbered list'
198
- getState={(s) => s.listStyle === 'ordered'}
199
- onClick={(s) => ({ type: 'list-ordered', data: s ? s.listStyle !== 'ordered' : null })}
200
- />
201
- <ToolbarButton
202
- Icon={ListChecks}
203
- title='Task list'
204
- getState={(s) => s.listStyle === 'task'}
205
- onClick={(s) => ({ type: 'list-tasks', data: s ? s.listStyle !== 'task' : null })}
206
- />
207
- </div>
208
- );
169
+ const MarkdownStyles = () => {
170
+ const { onAction, state } = useToolbarContext('MarkdownStyles');
171
+ const { t } = useTranslation(translationKey);
209
172
 
210
- const MarkdownBlocks = () => (
211
- <div role='none'>
212
- <ToolbarButton
213
- Icon={CaretRight}
214
- title='Block quote'
215
- getState={(s) => s.blockQuote}
216
- onClick={(s) => ({ type: 'blockquote', data: s ? !s.blockQuote : null })}
217
- />
218
- <ToolbarButton
219
- Icon={CodeBlock}
220
- title='Code block'
221
- disable={(s) => !s.blankLine}
222
- getState={(s) => s.blockType === 'codeblock'}
223
- onClick={(s) => ({ type: 'codeblock', data: s ? s.blockType !== 'codeblock' : null })}
224
- />
225
- <ToolbarButton Icon={Table} title='Table' disable={(s) => !s.blankLine} onClick={() => ({ type: 'table' })} />
226
- </div>
227
- );
173
+ return (
174
+ <NaturalToolbar.ToggleGroup
175
+ type='multiple'
176
+ value={markdownStyles.filter(({ key }) => !!state?.[key]).map(({ key }) => key)}
177
+ >
178
+ {markdownStyles.map(({ key, Icon }) => (
179
+ <ToolbarButton
180
+ key={key}
181
+ value={key}
182
+ Icon={Icon}
183
+ disabled={state?.blockType === 'codeblock'}
184
+ onClick={() => onAction?.({ type: key, data: state ? !state[key] : null })}
185
+ >
186
+ {t(`${key} label`)}
187
+ </ToolbarButton>
188
+ ))}
189
+ </NaturalToolbar.ToggleGroup>
190
+ );
191
+ };
228
192
 
229
- const MarkdownLinks = () => (
230
- <div role='none'>
231
- <ToolbarButton
232
- Icon={Link}
233
- title='Link'
234
- getState={(s) => s.link}
235
- onClick={(s) => ({ type: 'link', data: s ? !s.link : null })}
236
- />
237
- {/* <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} /> */}
238
- {/* <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} /> */}
239
- </div>
240
- );
193
+ const markdownLists = [
194
+ { key: 'bullet' as const, Icon: ListBullets, actionType: 'list-bullet' as const },
195
+ { key: 'ordered' as const, Icon: ListNumbers, actionType: 'list-ordered' as const },
196
+ { key: 'task' as const, Icon: ListChecks, actionType: 'list-tasks' as const },
197
+ ];
198
+
199
+ const MarkdownLists = () => {
200
+ const { onAction, state } = useToolbarContext('MarkdownStyles');
201
+ const { t } = useTranslation(translationKey);
202
+ return (
203
+ <NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ?? undefined}>
204
+ {markdownLists.map(({ key, Icon, actionType }) => (
205
+ <ToolbarButton
206
+ key={key}
207
+ value={key}
208
+ Icon={Icon}
209
+ onClick={() => onAction?.({ type: actionType, data: state ? state.listStyle !== key : null })}
210
+ >
211
+ {t(`${key} label`)}
212
+ </ToolbarButton>
213
+ ))}
214
+ </NaturalToolbar.ToggleGroup>
215
+ );
216
+ };
217
+
218
+ const markdownBlocks = [
219
+ {
220
+ key: 'blockquote' as const,
221
+ Icon: Quotes,
222
+ pressed: (state: Formatting | null) => state?.blockQuote,
223
+ action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
224
+ disabled: (state: Formatting | null) => false,
225
+ },
226
+ {
227
+ key: 'codeblock' as const,
228
+ Icon: CodeBlock,
229
+ pressed: (state: Formatting | null) => state?.blockType === 'codeblock',
230
+ action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
231
+ disabled: (state: Formatting | null) => !state?.blankLine,
232
+ },
233
+ ];
234
+
235
+ const MarkdownBlocks = () => {
236
+ const { onAction, state } = useToolbarContext('MarkdownStyles');
237
+ const { t } = useTranslation(translationKey);
238
+ return (
239
+ <>
240
+ <NaturalToolbar.ToggleGroup
241
+ type='multiple'
242
+ value={markdownBlocks.filter(({ pressed }) => pressed(state)).map(({ key }) => key)}
243
+ >
244
+ {markdownBlocks.map(({ key, Icon, action }) => (
245
+ <ToolbarButton key={key} value={key} Icon={Icon} onClick={() => onAction?.(action(state))}>
246
+ {t(`${key} label`)}
247
+ </ToolbarButton>
248
+ ))}
249
+ </NaturalToolbar.ToggleGroup>
250
+ <NaturalToolbar.Button
251
+ variant='ghost'
252
+ disabled={!state?.blankLine}
253
+ onClick={() => onAction?.({ type: 'table' })}
254
+ classNames={buttonStyles}
255
+ >
256
+ <Table className={iconStyles} />
257
+ <span className='sr-only'>{t('table label')}</span>
258
+ </NaturalToolbar.Button>
259
+ </>
260
+ );
261
+ };
241
262
 
242
263
  const MarkdownStandard = () => (
243
264
  <>
@@ -245,13 +266,19 @@ const MarkdownStandard = () => (
245
266
  <MarkdownStyles />
246
267
  <MarkdownLists />
247
268
  <MarkdownBlocks />
248
- <MarkdownLinks />
249
269
  </>
250
270
  );
251
271
 
252
- const MarkdownExtended = () => (
253
- <Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
254
- );
272
+ const MarkdownExtended = () => {
273
+ const { onAction } = useToolbarContext('MarkdownStyles');
274
+ const { t } = useTranslation(translationKey);
275
+ return (
276
+ <NaturalToolbar.Button variant='ghost' onClick={() => onAction?.({ type: 'comment' })} classNames={buttonStyles}>
277
+ <ChatText className={iconStyles} />
278
+ <span className='sr-only'>{t('comment label')}</span>
279
+ </NaturalToolbar.Button>
280
+ );
281
+ };
255
282
 
256
283
  export const Toolbar = {
257
284
  Root: ToolbarRoot,
@@ -4,15 +4,14 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { invertedEffects } from '@codemirror/commands';
8
- import { StateField, type Extension, type StateEffect } from '@codemirror/state';
7
+ import { StateField, type Extension } from '@codemirror/state';
9
8
  import { EditorView, ViewPlugin } from '@codemirror/view';
10
9
 
11
10
  import { next as A, type Prop } from '@dxos/automerge/automerge';
12
11
 
13
12
  import { cursorConverter } from './cursor';
14
13
  import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
15
- import { PatchSemaphore } from './semaphore';
14
+ import { Syncer } from './sync';
16
15
  import { Cursor } from '../cursor';
17
16
 
18
17
  export type AutomergeOptions = {
@@ -55,7 +54,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
55
54
  },
56
55
  });
57
56
 
58
- const semaphore = new PatchSemaphore(handle, syncState);
57
+ const syncer = new Syncer(handle, syncState);
59
58
 
60
59
  return [
61
60
  Cursor.converter.of(cursorConverter(handle, path)),
@@ -73,7 +72,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
73
72
  }
74
73
 
75
74
  _handleChange() {
76
- semaphore.reconcile(this._view);
75
+ syncer.reconcile(this._view, false);
77
76
  }
78
77
  },
79
78
  ),
@@ -81,23 +80,8 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
81
80
  // Reconcile local updates.
82
81
  EditorView.updateListener.of(({ view, changes }) => {
83
82
  if (!changes.empty) {
84
- semaphore.reconcile(view);
83
+ syncer.reconcile(view, true);
85
84
  }
86
85
  }),
87
-
88
- // TODO(burdon): Record undo transactions.
89
- // See https://github.com/yjs/y-codemirror.next/tree/main
90
- // https://codemirror.net/examples/inverted-effect
91
- invertedEffects.of((tr) => {
92
- // Each change results it three transactions: insert, delete, insert.
93
- const effects: StateEffect<any>[] = [];
94
- if (!tr.changes.empty) {
95
- tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
96
- // effects.push(effectType.of({});
97
- });
98
- }
99
-
100
- return effects;
101
- }),
102
86
  ];
103
87
  };
@@ -12,11 +12,11 @@ import { next as A } from '@dxos/automerge/automerge';
12
12
  import {
13
13
  getLastHeads,
14
14
  getPath,
15
- isReconcile,
16
- reconcileAnnotation,
17
- updateHeads,
18
15
  type IDocHandle,
19
16
  type State,
17
+ reconcileAnnotation,
18
+ updateHeads,
19
+ isReconcile,
20
20
  } from './defs';
21
21
  import { updateAutomerge } from './update-automerge';
22
22
  import { updateCodeMirror } from './update-codemirror';
@@ -24,8 +24,8 @@ import { updateCodeMirror } from './update-codemirror';
24
24
  /**
25
25
  * Implements three-way merge (on each mutation).
26
26
  */
27
- export class PatchSemaphore {
28
- private _inReconcile = false;
27
+ export class Syncer {
28
+ private _pending = false;
29
29
 
30
30
  // prettier-ignore
31
31
  constructor(
@@ -33,46 +33,44 @@ export class PatchSemaphore {
33
33
  private readonly _state: StateField<State>
34
34
  ) {}
35
35
 
36
- reconcile(view: EditorView) {
37
- if (!this._inReconcile) {
38
- this._inReconcile = true;
39
- this.doReconcile(view);
40
- this._inReconcile = false;
36
+ reconcile(view: EditorView, editor: boolean) {
37
+ // TODO(burdon): Better way to do mutex?
38
+ if (this._pending) {
39
+ return;
41
40
  }
42
- }
43
-
44
- private doReconcile(view: EditorView) {
45
- const path = getPath(view.state, this._state);
46
41
 
47
- // Get the heads before the unreconciled transactions are applied.
48
- const oldHeads = getLastHeads(view.state, this._state);
49
- let selection = view.state.selection;
42
+ this._pending = true;
43
+ if (editor) {
44
+ this.onEditorChange(view);
45
+ } else {
46
+ this.onAutomergeChange(view);
47
+ }
48
+ this._pending = false;
49
+ }
50
50
 
51
- // First undo all the unreconciled transactions.
51
+ onEditorChange(view: EditorView) {
52
+ // Apply the unreconciled transactions to the document.
52
53
  const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
53
- const toInvert = transactions.slice().reverse();
54
- for (const tr of toInvert) {
55
- const inverted = tr.changes.invert(tr.startState.doc);
56
- selection = selection.map(inverted);
54
+ const newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
55
+
56
+ if (newHeads) {
57
57
  view.dispatch({
58
- changes: inverted,
59
- annotations: reconcileAnnotation.of(true),
58
+ effects: updateHeads(newHeads),
59
+ annotations: reconcileAnnotation.of(false),
60
60
  });
61
61
  }
62
+ }
62
63
 
63
- // Apply the unreconciled transactions to the document.
64
- // NOTE: null and undefined each come from automerge and repo respectively.
65
- let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
66
- if (newHeads === null || newHeads === undefined) {
67
- // TODO(alexjg): this is the call that's resetting the editor state on click.
68
- newHeads = A.getHeads(this._handle.docSync()!);
69
- }
70
-
71
- // Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
64
+ onAutomergeChange(view: EditorView) {
65
+ // Get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
66
+ const oldHeads = getLastHeads(view.state, this._state);
67
+ const newHeads = A.getHeads(this._handle.docSync()!);
72
68
  const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
69
+
70
+ const selection = view.state.selection;
71
+ const path = getPath(view.state, this._state);
73
72
  updateCodeMirror(view, selection, path, diff);
74
73
 
75
- // Update automerge state.
76
74
  view.dispatch({
77
75
  effects: updateHeads(newHeads),
78
76
  annotations: reconcileAnnotation.of(false),
package/src/index.ts CHANGED
@@ -10,6 +10,6 @@ export { type EditorView, keymap } from '@codemirror/view';
10
10
  export * from './components';
11
11
  export * from './extensions';
12
12
  export * from './hooks';
13
- export { getToken } from './styles';
13
+ export { getToken, editorWithToolbarLayout } from './styles';
14
14
  export * from './themes';
15
15
  export * from './util';
@@ -5,3 +5,4 @@
5
5
  export * from './cursors';
6
6
  export * from './markdown';
7
7
  export * from './tokens';
8
+ export * from './layout';
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export const editorWithToolbarLayout =
6
+ 'fixed inset-0 grid grid-cols-1 grid-rows-[min-content_1fr] justify-center content-start overflow-hidden';
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export const translationKey = 'react-ui-editor';
6
+
7
+ export default [
8
+ {
9
+ 'en-US': {
10
+ [translationKey]: {
11
+ 'strong label': 'Strong',
12
+ 'emphasis label': 'Emphasis',
13
+ 'strikethrough label': 'Strike-through',
14
+ 'code label': 'Code',
15
+ 'bullet label': 'Bullet list',
16
+ 'ordered label': 'Numbered list',
17
+ 'task label': 'Task list',
18
+ 'blockquote label': 'Block quote',
19
+ 'codeblock label': 'Code block',
20
+ 'comment label': 'Create comment',
21
+ 'heading label': 'Heading level',
22
+ },
23
+ },
24
+ },
25
+ ];
@@ -1 +0,0 @@
1
- {"version":3,"file":"semaphore.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/automerge/semaphore.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAML,KAAK,UAAU,EACf,KAAK,KAAK,EACX,MAAM,QAAQ,CAAC;AAIhB;;GAEG;AACH,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,YAAY,CAAS;gBAIV,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC;IAG5C,SAAS,CAAC,IAAI,EAAE,UAAU;IAQ1B,OAAO,CAAC,WAAW;CAqCpB"}