@dxos/react-ui-editor 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d

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 (280) hide show
  1. package/dist/lib/browser/index.mjs +2979 -2478
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2979 -2478
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Editor/Editor.d.ts +37 -21
  8. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  9. package/dist/types/src/components/Editor/Editor.stories.d.ts +20 -0
  10. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/EditorContent/EditorContent.d.ts +29 -0
  12. package/dist/types/src/components/EditorContent/EditorContent.d.ts.map +1 -0
  13. package/dist/types/src/components/EditorContent/EditorContent.stories.d.ts +26 -0
  14. package/dist/types/src/components/EditorContent/EditorContent.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/EditorContent/controller.d.ts +10 -0
  16. package/dist/types/src/components/EditorContent/controller.d.ts.map +1 -0
  17. package/dist/types/src/components/EditorContent/index.d.ts +3 -0
  18. package/dist/types/src/components/EditorContent/index.d.ts.map +1 -0
  19. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +36 -0
  20. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorMenuProvider/index.d.ts +7 -0
  22. package/dist/types/src/components/EditorMenuProvider/index.d.ts.map +1 -0
  23. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts +4 -0
  24. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -0
  25. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +28 -0
  26. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -0
  27. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +47 -0
  28. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -0
  29. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +34 -0
  30. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -0
  31. package/dist/types/src/components/EditorMenuProvider/util.d.ts +8 -0
  32. package/dist/types/src/components/EditorMenuProvider/util.d.ts.map +1 -0
  33. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +16 -0
  34. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -0
  35. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +2 -0
  36. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -0
  37. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +26 -2
  38. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  39. package/dist/types/src/components/EditorToolbar/actions.d.ts +39 -0
  40. package/dist/types/src/components/EditorToolbar/actions.d.ts.map +1 -0
  41. package/dist/types/src/components/EditorToolbar/blocks.d.ts +3 -3
  42. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  43. package/dist/types/src/components/EditorToolbar/formatting.d.ts +3 -3
  44. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  45. package/dist/types/src/components/EditorToolbar/headings.d.ts +3 -3
  46. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  47. package/dist/types/src/components/EditorToolbar/index.d.ts +2 -1
  48. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  49. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts +11 -0
  50. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts.map +1 -0
  51. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +3 -3
  52. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  53. package/dist/types/src/components/index.d.ts +4 -2
  54. package/dist/types/src/components/index.d.ts.map +1 -1
  55. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts +17 -0
  56. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  57. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  58. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  59. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  60. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  61. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
  62. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  63. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  64. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  65. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  66. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  67. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  68. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  69. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  70. package/dist/types/src/extensions/automerge/sync.d.ts +3 -3
  71. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  72. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  73. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  74. package/dist/types/src/extensions/autoscroll.d.ts +14 -4
  75. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  76. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +4 -4
  77. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  78. package/dist/types/src/extensions/blocks.d.ts +2 -0
  79. package/dist/types/src/extensions/blocks.d.ts.map +1 -0
  80. package/dist/types/src/extensions/bookmarks.d.ts +12 -0
  81. package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
  82. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  83. package/dist/types/src/extensions/factories.d.ts +7 -6
  84. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  85. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  86. package/dist/types/src/extensions/index.d.ts +8 -3
  87. package/dist/types/src/extensions/index.d.ts.map +1 -1
  88. package/dist/types/src/extensions/listener.d.ts +8 -6
  89. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  90. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  91. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -3
  92. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  93. package/dist/types/src/extensions/modal.d.ts +7 -0
  94. package/dist/types/src/extensions/modal.d.ts.map +1 -0
  95. package/dist/types/src/extensions/modes.d.ts +1 -1
  96. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  97. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  98. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  99. package/dist/types/src/extensions/preview/preview.d.ts +6 -3
  100. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  101. package/dist/types/src/extensions/replacer.d.ts +21 -0
  102. package/dist/types/src/extensions/replacer.d.ts.map +1 -0
  103. package/dist/types/src/extensions/replacer.test.d.ts +2 -0
  104. package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
  105. package/dist/types/src/extensions/scrolling.d.ts +78 -0
  106. package/dist/types/src/extensions/scrolling.d.ts.map +1 -0
  107. package/dist/types/src/extensions/state.d.ts +2 -0
  108. package/dist/types/src/extensions/state.d.ts.map +1 -0
  109. package/dist/types/src/extensions/submit.d.ts +10 -0
  110. package/dist/types/src/extensions/submit.d.ts.map +1 -0
  111. package/dist/types/src/extensions/tab.d.ts +4 -0
  112. package/dist/types/src/extensions/tab.d.ts.map +1 -0
  113. package/dist/types/src/extensions/tags/xml-tags.d.ts +41 -16
  114. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  115. package/dist/types/src/hooks/useTextEditor.d.ts +3 -7
  116. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  117. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
  118. package/dist/types/src/stories/Comments.stories.d.ts +3 -3
  119. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  120. package/dist/types/src/stories/Experimental.stories.d.ts +3 -3
  121. package/dist/types/src/stories/Markdown.stories.d.ts +3 -3
  122. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  123. package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -5
  124. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
  125. package/dist/types/src/stories/Preview.stories.d.ts +4 -3
  126. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  127. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  128. package/dist/types/src/stories/TextEditor.stories.d.ts +3 -4
  129. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  130. package/dist/types/src/stories/components/EditorStory.d.ts +11 -8
  131. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  132. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  133. package/dist/types/src/styles/theme.d.ts.map +1 -1
  134. package/dist/types/src/util/debug.d.ts +4 -0
  135. package/dist/types/src/util/debug.d.ts.map +1 -1
  136. package/dist/types/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +44 -46
  138. package/src/components/Editor/Editor.stories.tsx +89 -0
  139. package/src/components/Editor/Editor.tsx +157 -57
  140. package/src/components/EditorContent/EditorContent.stories.tsx +70 -0
  141. package/src/components/EditorContent/EditorContent.tsx +70 -0
  142. package/src/components/EditorContent/controller.ts +50 -0
  143. package/src/components/EditorContent/index.ts +6 -0
  144. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +233 -0
  145. package/src/components/EditorMenuProvider/index.ts +11 -0
  146. package/src/components/EditorMenuProvider/menu-presets.ts +123 -0
  147. package/src/components/EditorMenuProvider/menu.ts +71 -0
  148. package/src/components/EditorMenuProvider/popover.ts +287 -0
  149. package/src/components/EditorMenuProvider/useEditorMenu.ts +175 -0
  150. package/src/components/EditorMenuProvider/util.ts +31 -0
  151. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +82 -0
  152. package/src/components/EditorPreviewProvider/index.ts +5 -0
  153. package/src/components/EditorToolbar/EditorToolbar.tsx +101 -92
  154. package/src/components/EditorToolbar/{lists.ts → actions.ts} +46 -16
  155. package/src/components/EditorToolbar/blocks.ts +2 -1
  156. package/src/components/EditorToolbar/formatting.ts +2 -1
  157. package/src/components/EditorToolbar/headings.ts +8 -5
  158. package/src/components/EditorToolbar/image.ts +1 -1
  159. package/src/components/EditorToolbar/index.ts +3 -7
  160. package/src/components/EditorToolbar/search.ts +1 -1
  161. package/src/components/EditorToolbar/useEditorToolbar.ts +20 -0
  162. package/src/components/EditorToolbar/view-mode.ts +2 -1
  163. package/src/components/index.ts +8 -2
  164. package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +8 -76
  165. package/src/extensions/autocomplete/index.ts +8 -0
  166. package/src/extensions/autocomplete/match.ts +46 -0
  167. package/src/extensions/{command-menu → autocomplete}/placeholder.ts +21 -17
  168. package/src/extensions/{command-dialog → autocomplete}/typeahead.ts +6 -48
  169. package/src/extensions/automerge/automerge.stories.tsx +4 -3
  170. package/src/extensions/automerge/automerge.ts +28 -9
  171. package/src/extensions/automerge/cursor.ts +1 -1
  172. package/src/extensions/automerge/sync.ts +8 -4
  173. package/src/extensions/automerge/update-automerge.ts +1 -1
  174. package/src/extensions/autoscroll.ts +75 -69
  175. package/src/extensions/awareness/awareness-provider.ts +12 -12
  176. package/src/extensions/blocks.ts +131 -0
  177. package/src/extensions/bookmarks.ts +75 -0
  178. package/src/extensions/comments.ts +2 -1
  179. package/src/extensions/factories.ts +22 -24
  180. package/src/extensions/folding.tsx +1 -2
  181. package/src/extensions/index.ts +8 -3
  182. package/src/extensions/listener.ts +14 -20
  183. package/src/extensions/markdown/bundle.ts +14 -2
  184. package/src/extensions/markdown/decorate.ts +8 -8
  185. package/src/extensions/markdown/formatting.ts +8 -10
  186. package/src/extensions/markdown/highlight.ts +1 -1
  187. package/src/extensions/markdown/image.ts +2 -2
  188. package/src/extensions/markdown/table.ts +6 -6
  189. package/src/extensions/modal.ts +24 -0
  190. package/src/extensions/modes.ts +2 -2
  191. package/src/extensions/{floating-menu.ts → outliner/menu.ts} +7 -5
  192. package/src/extensions/outliner/outliner.test.ts +1 -1
  193. package/src/extensions/outliner/outliner.ts +3 -3
  194. package/src/extensions/outliner/tree.test.ts +1 -1
  195. package/src/extensions/preview/index.ts +1 -1
  196. package/src/extensions/preview/preview.ts +14 -11
  197. package/src/extensions/replacer.test.ts +75 -0
  198. package/src/extensions/replacer.ts +93 -0
  199. package/src/extensions/scrolling.ts +189 -0
  200. package/src/extensions/selection.ts +1 -1
  201. package/src/extensions/state.ts +7 -0
  202. package/src/extensions/submit.ts +62 -0
  203. package/src/extensions/tab.ts +29 -0
  204. package/src/extensions/tags/extended-markdown.test.ts +2 -1
  205. package/src/extensions/tags/xml-tags.ts +310 -203
  206. package/src/extensions/typewriter.ts +1 -1
  207. package/src/hooks/useTextEditor.ts +25 -25
  208. package/src/stories/CommandDialog.stories.tsx +13 -19
  209. package/src/stories/Comments.stories.tsx +3 -4
  210. package/src/stories/EditorToolbar.stories.tsx +8 -8
  211. package/src/stories/Experimental.stories.tsx +1 -1
  212. package/src/stories/Markdown.stories.tsx +1 -1
  213. package/src/stories/Outliner.stories.tsx +15 -15
  214. package/src/stories/Popover.stories.tsx +162 -0
  215. package/src/stories/Preview.stories.tsx +41 -37
  216. package/src/stories/Tags.stories.tsx +19 -5
  217. package/src/stories/TextEditor.stories.tsx +4 -30
  218. package/src/stories/components/EditorStory.tsx +31 -20
  219. package/src/stories/components/util.tsx +40 -8
  220. package/src/styles/markdown.ts +1 -1
  221. package/src/styles/theme.ts +2 -1
  222. package/src/util/debug.ts +5 -0
  223. package/dist/lib/browser/testing/index.mjs +0 -76
  224. package/dist/lib/browser/testing/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/testing/index.mjs +0 -78
  226. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  227. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts +0 -38
  228. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +0 -1
  229. package/dist/types/src/components/CommandMenu/index.d.ts +0 -2
  230. package/dist/types/src/components/CommandMenu/index.d.ts.map +0 -1
  231. package/dist/types/src/components/EditorToolbar/lists.d.ts +0 -19
  232. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +0 -1
  233. package/dist/types/src/components/EditorToolbar/util.d.ts +0 -51
  234. package/dist/types/src/components/EditorToolbar/util.d.ts.map +0 -1
  235. package/dist/types/src/extensions/autocomplete.d.ts +0 -26
  236. package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
  237. package/dist/types/src/extensions/command-dialog/action.d.ts +0 -17
  238. package/dist/types/src/extensions/command-dialog/action.d.ts.map +0 -1
  239. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts +0 -6
  240. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts.map +0 -1
  241. package/dist/types/src/extensions/command-dialog/hint.d.ts +0 -19
  242. package/dist/types/src/extensions/command-dialog/hint.d.ts.map +0 -1
  243. package/dist/types/src/extensions/command-dialog/index.d.ts +0 -4
  244. package/dist/types/src/extensions/command-dialog/index.d.ts.map +0 -1
  245. package/dist/types/src/extensions/command-dialog/state.d.ts +0 -16
  246. package/dist/types/src/extensions/command-dialog/state.d.ts.map +0 -1
  247. package/dist/types/src/extensions/command-dialog/typeahead.d.ts +0 -22
  248. package/dist/types/src/extensions/command-dialog/typeahead.d.ts.map +0 -1
  249. package/dist/types/src/extensions/command-menu/command-menu.d.ts +0 -20
  250. package/dist/types/src/extensions/command-menu/command-menu.d.ts.map +0 -1
  251. package/dist/types/src/extensions/command-menu/index.d.ts +0 -3
  252. package/dist/types/src/extensions/command-menu/index.d.ts.map +0 -1
  253. package/dist/types/src/extensions/command-menu/placeholder.d.ts +0 -10
  254. package/dist/types/src/extensions/command-menu/placeholder.d.ts.map +0 -1
  255. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts +0 -24
  256. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts.map +0 -1
  257. package/dist/types/src/extensions/floating-menu.d.ts +0 -7
  258. package/dist/types/src/extensions/floating-menu.d.ts.map +0 -1
  259. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
  260. package/dist/types/src/testing/PreviewPopover.d.ts +0 -20
  261. package/dist/types/src/testing/PreviewPopover.d.ts.map +0 -1
  262. package/dist/types/src/testing/index.d.ts +0 -3
  263. package/dist/types/src/testing/index.d.ts.map +0 -1
  264. package/dist/types/src/testing/util.d.ts +0 -3
  265. package/dist/types/src/testing/util.d.ts.map +0 -1
  266. package/src/components/CommandMenu/CommandMenu.tsx +0 -348
  267. package/src/components/CommandMenu/index.ts +0 -5
  268. package/src/components/EditorToolbar/util.ts +0 -76
  269. package/src/extensions/command-dialog/action.ts +0 -55
  270. package/src/extensions/command-dialog/command-dialog.ts +0 -34
  271. package/src/extensions/command-dialog/hint.ts +0 -103
  272. package/src/extensions/command-dialog/index.ts +0 -7
  273. package/src/extensions/command-dialog/state.ts +0 -90
  274. package/src/extensions/command-menu/command-menu.ts +0 -210
  275. package/src/extensions/command-menu/index.ts +0 -6
  276. package/src/extensions/command-menu/useCommandMenu.ts +0 -134
  277. package/src/stories/CommandMenu.stories.tsx +0 -158
  278. package/src/testing/PreviewPopover.tsx +0 -78
  279. package/src/testing/index.ts +0 -6
  280. package/src/testing/util.ts +0 -7
@@ -0,0 +1,131 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { RangeSetBuilder } from '@codemirror/state';
6
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
7
+
8
+ import { mx } from '@dxos/react-ui-theme';
9
+
10
+ const paragraphBlockPlugin = ViewPlugin.fromClass(
11
+ class {
12
+ decorations: DecorationSet;
13
+
14
+ constructor(view: EditorView) {
15
+ this.decorations = this.build(view);
16
+ }
17
+
18
+ update(update: ViewUpdate) {
19
+ if (update.docChanged || update.viewportChanged) {
20
+ this.decorations = this.build(update.view);
21
+ }
22
+ }
23
+
24
+ build({ state }: EditorView) {
25
+ const builder = new RangeSetBuilder<Decoration>();
26
+
27
+ // Helper: commit a block from blockStart to endLine (inclusive).
28
+ const pushBlock = (fromLine: number, toLine: number) => {
29
+ // Add line decorations for each line in the block.
30
+ for (let lineNum = fromLine; lineNum <= toLine; lineNum++) {
31
+ const line = state.doc.line(lineNum);
32
+ builder.add(
33
+ line.from,
34
+ line.from,
35
+ Decoration.line({
36
+ class: mx(
37
+ 'block-line',
38
+ fromLine === toLine && 'block-single',
39
+ lineNum === fromLine && 'block-first',
40
+ lineNum > fromLine && lineNum < toLine && 'block-middle',
41
+ lineNum === toLine && 'block-last',
42
+ ),
43
+ }),
44
+ );
45
+ }
46
+ };
47
+
48
+ let blockStart: number | null = null;
49
+ let consecutiveBlankLines = 0;
50
+ const totalLines = state.doc.lines;
51
+ for (let i = 1; i <= totalLines; i++) {
52
+ const line = state.doc.line(i);
53
+ const isBlank = /^\s*$/.test(line.text);
54
+
55
+ if (!isBlank) {
56
+ // Reset blank line counter.
57
+ consecutiveBlankLines = 0;
58
+ // Start a new block if we're not already in one.
59
+ if (blockStart === null) {
60
+ blockStart = i;
61
+ }
62
+ } else {
63
+ // Increment blank line counter.
64
+ consecutiveBlankLines++;
65
+
66
+ // End the current block if we have 2+ consecutive blank lines.
67
+ if (consecutiveBlankLines >= 2 && blockStart !== null) {
68
+ pushBlock(blockStart, i - consecutiveBlankLines);
69
+ blockStart = null;
70
+ }
71
+ }
72
+ }
73
+
74
+ // Handle any remaining block at the end of the document.
75
+ if (blockStart !== null) {
76
+ // Find the last non-blank line for the block end.
77
+ let lastNonBlankLine = totalLines;
78
+ while (lastNonBlankLine >= blockStart) {
79
+ const line = state.doc.line(lastNonBlankLine);
80
+ if (!/^\s*$/.test(line.text)) {
81
+ break;
82
+ }
83
+ lastNonBlankLine--;
84
+ }
85
+ if (lastNonBlankLine >= blockStart) {
86
+ pushBlock(blockStart, lastNonBlankLine);
87
+ }
88
+ }
89
+
90
+ return builder.finish();
91
+ }
92
+ },
93
+ {
94
+ decorations: (v) => v.decorations,
95
+ },
96
+ );
97
+
98
+ export const blocks = () => [
99
+ paragraphBlockPlugin,
100
+ EditorView.baseTheme({
101
+ '.cm-line.block-line': {
102
+ paddingLeft: '0.75rem',
103
+ paddingRight: '0.75rem',
104
+ borderLeft: '1px solid var(--dx-subduedSeparator)',
105
+ borderRight: '1px solid var(--dx-subduedSeparator)',
106
+ },
107
+ '.cm-line.block-single': {
108
+ border: '1px solid var(--dx-subduedSeparator)',
109
+ borderRadius: '6px',
110
+ paddingTop: '0.5rem',
111
+ paddingBottom: '0.5rem',
112
+ marginTop: '0.5rem',
113
+ marginBottom: '0.5rem',
114
+ },
115
+ '.cm-line.block-first': {
116
+ borderTop: '1px solid var(--dx-subduedSeparator)',
117
+ borderTopLeftRadius: '6px',
118
+ borderTopRightRadius: '6px',
119
+ paddingTop: '0.5rem',
120
+ marginTop: '0.5rem',
121
+ },
122
+ '.cm-line.block-middle': {},
123
+ '.cm-line.block-last': {
124
+ borderBottom: '1px solid var(--dx-subduedSeparator)',
125
+ borderBottomLeftRadius: '6px',
126
+ borderBottomRightRadius: '6px',
127
+ paddingBottom: '0.5rem',
128
+ marginBottom: '0.5rem',
129
+ },
130
+ }),
131
+ ];
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Prec, StateEffect, StateField } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+
8
+ type Bookmark = {
9
+ id: string;
10
+ pos: number;
11
+ name: string;
12
+ };
13
+
14
+ export const addBookmark = StateEffect.define<Bookmark>();
15
+ export const removeBookmark = StateEffect.define<string>();
16
+ export const clearBookmarks = StateEffect.define<void>();
17
+
18
+ export const bookmarks = (): Extension => {
19
+ return [
20
+ bookmarksField,
21
+ Prec.highest(
22
+ keymap.of([
23
+ {
24
+ key: 'Mod-ArrowUp',
25
+ run: (view) => {
26
+ const bookmarks = view.state.field(bookmarksField);
27
+ console.log('up', bookmarks);
28
+ return true;
29
+ },
30
+ },
31
+ {
32
+ key: 'Mod-ArrowDown',
33
+ run: (view) => {
34
+ const bookmarks = view.state.field(bookmarksField);
35
+ console.log('down', bookmarks);
36
+ return true;
37
+ },
38
+ },
39
+ ]),
40
+ ),
41
+ ];
42
+ };
43
+
44
+ type BookmarkFieldState = {
45
+ bookmarks: Bookmark[];
46
+ };
47
+
48
+ const bookmarksField = StateField.define<BookmarkFieldState>({
49
+ create: (): BookmarkFieldState => ({
50
+ bookmarks: [],
51
+ }),
52
+
53
+ update: (value: BookmarkFieldState, tr): BookmarkFieldState => {
54
+ // Map bookmark positions through document changes.
55
+ let bookmarks = value.bookmarks.map((bookmark) => ({
56
+ ...bookmark,
57
+ pos: tr.changes.mapPos(bookmark.pos),
58
+ }));
59
+
60
+ // Process effects.
61
+ for (const effect of tr.effects) {
62
+ if (effect.is(addBookmark)) {
63
+ bookmarks = [...bookmarks, effect.value];
64
+ } else if (effect.is(removeBookmark)) {
65
+ bookmarks = bookmarks.filter((b) => b.id !== effect.value);
66
+ } else if (effect.is(clearBookmarks)) {
67
+ bookmarks = [];
68
+ }
69
+ }
70
+
71
+ // Sort bookmarks by position.
72
+ bookmarks.sort(({ pos: a }, { pos: b }) => a - b);
73
+ return { bookmarks };
74
+ },
75
+ });
@@ -104,13 +104,14 @@ export const commentsState = StateField.define<CommentsState>({
104
104
  const styles = EditorView.theme({
105
105
  '.cm-comment, .cm-comment-current': {
106
106
  padding: '3px 0',
107
+ color: 'var(--dx-cmCommentText)',
107
108
  backgroundColor: 'var(--dx-cmCommentSurface)',
108
109
  },
109
110
  '.cm-comment > span, .cm-comment-current > span': {
110
111
  boxDecorationBreak: 'clone',
111
112
  boxShadow: '0 0 1px 3px var(--dx-cmCommentSurface)',
112
113
  backgroundColor: 'var(--dx-cmCommentSurface)',
113
- color: 'var(--dx-cmComment)',
114
+ color: 'var(--dx-cmCommentText)',
114
115
  cursor: 'pointer',
115
116
  },
116
117
  });
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
- import { bracketMatching } from '@codemirror/language';
7
+ import { HighlightStyle, bracketMatching, syntaxHighlighting } from '@codemirror/language';
8
8
  import { searchKeymap } from '@codemirror/search';
9
9
  import { type ChangeSpec, EditorState, type Extension, type TransactionSpec } from '@codemirror/state';
10
10
  import {
@@ -19,14 +19,15 @@ import {
19
19
  placeholder,
20
20
  scrollPastEnd,
21
21
  } from '@codemirror/view';
22
- import { vscodeDarkInit, vscodeDarkStyle, vscodeLightInit, vscodeLightStyle } from '@uiw/codemirror-theme-vscode';
22
+ import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode';
23
23
  import defaultsDeep from 'lodash.defaultsdeep';
24
24
  import merge from 'lodash.merge';
25
25
 
26
26
  import { generateName } from '@dxos/display-name';
27
+ import { type DocAccessor } from '@dxos/echo-db';
27
28
  import { log } from '@dxos/log';
28
- import { type DocAccessor, type Space } from '@dxos/react-client/echo';
29
- import { type Identity } from '@dxos/react-client/halo';
29
+ import { type Messenger } from '@dxos/protocols';
30
+ import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
30
31
  import { type ThemeMode } from '@dxos/react-ui';
31
32
  import { type HuePalette } from '@dxos/react-ui-theme';
32
33
  import { hexToHue, isTruthy } from '@dxos/util';
@@ -44,7 +45,9 @@ import { focus } from './focus';
44
45
 
45
46
  export const filterChars = (chars: RegExp) => {
46
47
  return EditorState.transactionFilter.of((transaction) => {
47
- if (!transaction.docChanged) return transaction;
48
+ if (!transaction.docChanged) {
49
+ return transaction;
50
+ }
48
51
 
49
52
  const changes: ChangeSpec[] = [];
50
53
  transaction.changes.iterChanges((fromA, toA, fromB, toB, text) => {
@@ -62,6 +65,7 @@ export const filterChars = (chars: RegExp) => {
62
65
  if (changes.length) {
63
66
  return [transaction, { changes, sequential: true } as TransactionSpec];
64
67
  }
68
+
65
69
  return transaction;
66
70
  });
67
71
  };
@@ -87,7 +91,6 @@ export type BasicExtensionsOptions = {
87
91
  lineNumbers?: boolean;
88
92
  /** If false then do not set a max-width or side margin on the editor. */
89
93
  lineWrapping?: boolean;
90
- monospace?: boolean;
91
94
  placeholder?: string;
92
95
  /** If true user cannot edit the text, but they can still select and copy it. */
93
96
  readOnly?: boolean;
@@ -136,7 +139,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
136
139
  props.history && history(),
137
140
  props.lineNumbers && [lineNumbers(), editorGutter],
138
141
  props.lineWrapping && EditorView.lineWrapping,
139
- props.monospace && editorMonospace,
140
142
  props.placeholder && placeholder(props.placeholder),
141
143
  props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
142
144
  props.scrollPastEnd && scrollPastEnd(),
@@ -173,6 +175,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
173
175
 
174
176
  export type ThemeExtensionsOptions = {
175
177
  themeMode?: ThemeMode;
178
+ monospace?: boolean;
176
179
  styles?: ThemeStyles;
177
180
  syntaxHighlighting?: boolean;
178
181
  slots?: {
@@ -180,6 +183,7 @@ export type ThemeExtensionsOptions = {
180
183
  className?: string;
181
184
  };
182
185
  scroll?: {
186
+ // NOTE: Do not apply vertical padding to scroll container.
183
187
  className?: string;
184
188
  };
185
189
  content?: {
@@ -190,7 +194,7 @@ export type ThemeExtensionsOptions = {
190
194
 
191
195
  export const grow: ThemeExtensionsOptions['slots'] = {
192
196
  editor: {
193
- className: 'is-full bs-full',
197
+ className: 'bs-full is-full',
194
198
  },
195
199
  } as const;
196
200
 
@@ -202,13 +206,6 @@ export const fullWidth: ThemeExtensionsOptions['slots'] = {
202
206
 
203
207
  export const defaultThemeSlots = grow;
204
208
 
205
- const semanticTokensSettings = {
206
- settings: {
207
- background: 'var(--dx-baseSurface)',
208
- foreground: 'var(--dx-baseText)',
209
- },
210
- };
211
-
212
209
  export const defaultStyles = {
213
210
  dark: vscodeDarkStyle,
214
211
  light: vscodeLightStyle,
@@ -219,17 +216,18 @@ export const defaultStyles = {
219
216
  */
220
217
  export const createThemeExtensions = ({
221
218
  themeMode,
219
+ monospace,
222
220
  styles,
223
- syntaxHighlighting: syntaxHighlightingProps,
221
+ syntaxHighlighting: syntaxHighlightingProp,
224
222
  slots: slotsParam,
225
223
  }: ThemeExtensionsOptions = {}): Extension => {
226
224
  const slots = defaultsDeep({}, slotsParam, defaultThemeSlots);
227
225
  return [
228
226
  EditorView.darkTheme.of(themeMode === 'dark'),
229
227
  EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
230
- syntaxHighlightingProps && [
231
- themeMode === 'dark' ? vscodeDarkInit(semanticTokensSettings) : vscodeLightInit(semanticTokensSettings),
232
- ],
228
+ monospace && editorMonospace,
229
+ syntaxHighlightingProp &&
230
+ syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
233
231
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
234
232
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
235
233
  slots.scroll?.className &&
@@ -250,24 +248,24 @@ export const createThemeExtensions = ({
250
248
  export type DataExtensionsProps<T> = {
251
249
  id: string;
252
250
  text?: DocAccessor<T>;
253
- space?: Space;
251
+ messenger?: Messenger;
254
252
  identity?: Identity | null;
255
253
  };
256
254
 
257
- // TODO(burdon): Move out of react-ui-editor (remove echo deps).
258
- export const createDataExtensions = <T>({ id, text, space, identity }: DataExtensionsProps<T>): Extension[] => {
255
+ // TODO(burdon): Move out of react-ui-editor?
256
+ export const createDataExtensions = <T>({ id, text, messenger, identity }: DataExtensionsProps<T>): Extension[] => {
259
257
  const extensions: Extension[] = [];
260
258
  if (text) {
261
259
  extensions.push(automerge(text));
262
260
  }
263
261
 
264
- if (space && identity) {
262
+ if (messenger && identity) {
265
263
  const peerId = identity?.identityKey.toHex();
266
264
  const hue = (identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0');
267
265
  extensions.push(
268
266
  awareness(
269
267
  new SpaceAwarenessProvider({
270
- space,
268
+ messenger,
271
269
  channel: `awareness.${id}`,
272
270
  peerId: identity.identityKey.toHex(),
273
271
  info: {
@@ -25,8 +25,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
25
25
  foldGutter({
26
26
  markerDOM: (open) => {
27
27
  return renderRoot(
28
- Domino.of('div').classNames('flex h-full items-center').build(),
29
- // TODO(burdon): Use sprite directly.
28
+ Domino.of('div').classNames('flex bs-full items-center').build(),
30
29
  <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
31
30
  );
32
31
  },
@@ -8,13 +8,12 @@ export * from './autoscroll';
8
8
  export * from './automerge';
9
9
  export * from './awareness';
10
10
  export * from './blast';
11
- export * from './command-dialog';
12
- export * from './command-menu';
11
+ export * from './blocks';
12
+ export * from './bookmarks';
13
13
  export * from './comments';
14
14
  export * from './debug';
15
15
  export * from './dnd';
16
16
  export * from './factories';
17
- export * from './floating-menu';
18
17
  export * from './focus';
19
18
  export * from './folding';
20
19
  export * from './hashtag';
@@ -22,9 +21,15 @@ export * from './json';
22
21
  export * from './listener';
23
22
  export * from './markdown';
24
23
  export * from './mention';
24
+ export * from './modal';
25
25
  export * from './modes';
26
26
  export * from './outliner';
27
27
  export * from './preview';
28
+ export * from './replacer';
28
29
  export * from './selection';
30
+ export * from './scrolling';
31
+ export * from './state';
32
+ export * from './submit';
33
+ export * from './tab';
29
34
  export * from './tags';
30
35
  export * from './typewriter';
@@ -5,34 +5,28 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
7
 
8
+ import { isNonNullable } from '@dxos/util';
9
+
8
10
  import { documentId } from './selection';
9
11
 
10
12
  export type ListenerOptions = {
11
- onFocus?: (focusing: boolean) => void;
12
- onChange?: (text: string, id: string) => void;
13
+ onFocus?: (event: { id: string; focusing: boolean }) => void;
14
+ onChange?: (event: { id: string; text: string }) => void;
13
15
  };
14
16
 
15
- /**
16
- * Event listener.
17
- * @deprecated Use EditorView.updateListener and listen for specific update events.
18
- */
19
17
  export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
20
- const extensions: Extension[] = [];
21
-
22
- onFocus &&
23
- extensions.push(
24
- EditorView.focusChangeEffect.of((_, focusing) => {
25
- onFocus(focusing);
18
+ return [
19
+ onFocus &&
20
+ EditorView.focusChangeEffect.of((state, focusing) => {
21
+ onFocus({ id: state.facet(documentId), focusing });
26
22
  return null;
27
23
  }),
28
- );
29
24
 
30
- onChange &&
31
- extensions.push(
32
- EditorView.updateListener.of((update) => {
33
- onChange(update.state.doc.toString(), update.state.facet(documentId));
25
+ onChange &&
26
+ EditorView.updateListener.of(({ state, docChanged }) => {
27
+ if (docChanged) {
28
+ onChange({ id: state.facet(documentId), text: state.doc.toString() });
29
+ }
34
30
  }),
35
- );
36
-
37
- return extensions;
31
+ ].filter(isNonNullable);
38
32
  };
@@ -4,8 +4,10 @@
4
4
 
5
5
  import { completionKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, indentWithTab } from '@codemirror/commands';
7
+ import { jsonLanguage } from '@codemirror/lang-json';
7
8
  import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
8
- import { syntaxHighlighting } from '@codemirror/language';
9
+ import { xml } from '@codemirror/lang-xml';
10
+ import { LanguageDescription, syntaxHighlighting } from '@codemirror/language';
9
11
  import { languages } from '@codemirror/language-data';
10
12
  import { type Extension } from '@codemirror/state';
11
13
  import { keymap } from '@codemirror/view';
@@ -43,6 +45,7 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
43
45
  base: markdownLanguage,
44
46
 
45
47
  // Languages for syntax highlighting fenced code blocks.
48
+ defaultCodeLanguage: jsonLanguage,
46
49
  codeLanguages: languages,
47
50
 
48
51
  // Don't complete HTML tags.
@@ -66,12 +69,21 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
66
69
 
67
70
  // https://codemirror.net/docs/ref/#commands.defaultKeymap
68
71
  ...defaultKeymap,
72
+
73
+ // TODO(burdon): Remove?
69
74
  ...completionKeymap,
70
75
  ].filter(isTruthy),
71
76
  ),
72
77
  ];
73
78
  };
74
79
 
80
+ const xmlLanguageDesc = LanguageDescription.of({
81
+ name: 'xml',
82
+ alias: ['html', 'xhtml'],
83
+ extensions: ['xml', 'xhtml'],
84
+ load: async () => xml(),
85
+ });
86
+
75
87
  /**
76
88
  * Default customizations.
77
89
  * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
@@ -89,5 +101,5 @@ const noSetExtHeading: MarkdownConfig = {
89
101
  * Remove HTML and XML parsing.
90
102
  */
91
103
  const noHtml: MarkdownConfig = {
92
- remove: ['HTMLBlock', 'HTMLTag'],
104
+ // remove: ['HTMLBlock', 'HTMLTag'],
93
105
  };
@@ -52,12 +52,12 @@ class LinkButton extends WidgetType {
52
52
  super();
53
53
  }
54
54
 
55
- override eq(other: this): boolean {
55
+ override eq(other: this) {
56
56
  return this.url === other.url;
57
57
  }
58
58
 
59
59
  // TODO(burdon): Create icon and link directly without react?
60
- override toDOM(view: EditorView): HTMLSpanElement {
60
+ override toDOM(view: EditorView) {
61
61
  const el = document.createElement('span');
62
62
  this.render(el, { url: this.url }, view);
63
63
  return el;
@@ -69,11 +69,15 @@ class CheckboxWidget extends WidgetType {
69
69
  super();
70
70
  }
71
71
 
72
- override eq(other: this): boolean {
72
+ override eq(other: this) {
73
73
  return this._checked === other._checked;
74
74
  }
75
75
 
76
- override toDOM(view: EditorView): HTMLSpanElement {
76
+ override ignoreEvent() {
77
+ return false;
78
+ }
79
+
80
+ override toDOM(view: EditorView) {
77
81
  const input = document.createElement('input');
78
82
  input.className = 'cm-task-checkbox dx-checkbox';
79
83
  input.type = 'checkbox';
@@ -105,10 +109,6 @@ class CheckboxWidget extends WidgetType {
105
109
  span.appendChild(input);
106
110
  return span;
107
111
  }
108
-
109
- override ignoreEvent(): boolean {
110
- return false;
111
- }
112
112
  }
113
113
 
114
114
  class TextWidget extends WidgetType {
@@ -15,12 +15,8 @@ import {
15
15
  } from '@codemirror/state';
16
16
  import { EditorView, type ViewUpdate, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNode, type SyntaxNodeRef } from '@lezer/common';
18
- import { useCallback, useMemo } from 'react';
19
18
 
20
19
  import { debounceAndThrottle } from '@dxos/async';
21
- import { type Live } from '@dxos/live-object';
22
-
23
- import { type EditorToolbarState } from '../../components';
24
20
 
25
21
  // Markdown refs:
26
22
  // https://github.github.com/gfm
@@ -1251,17 +1247,19 @@ export const getFormatting = (state: EditorState): Formatting => {
1251
1247
  /**
1252
1248
  * Hook provides an extension to compute the current formatting state.
1253
1249
  */
1254
- export const useFormattingState = (state: Live<EditorToolbarState>): Extension => {
1255
- const handleUpdate = useCallback(
1250
+ export const formattingListener = (stateProvider: () => Formatting | undefined, delay = 100): Extension => {
1251
+ return EditorView.updateListener.of(
1256
1252
  debounceAndThrottle((update: ViewUpdate) => {
1257
1253
  if (update.docChanged || update.selectionSet) {
1254
+ const state = stateProvider();
1255
+ if (!state) {
1256
+ return;
1257
+ }
1258
+
1258
1259
  Object.entries(getFormatting(update.state)).forEach(([key, active]) => {
1259
1260
  state[key as keyof Formatting] = active as any;
1260
1261
  });
1261
1262
  }
1262
- }, 100),
1263
- [state],
1263
+ }, delay),
1264
1264
  );
1265
-
1266
- return useMemo(() => EditorView.updateListener.of(handleUpdate), [handleUpdate]);
1267
1265
  };
@@ -141,7 +141,7 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
141
141
 
142
142
  // Fonts.
143
143
  {
144
- tag: [tags.monospace],
144
+ tag: [tags.monospace, tags.comment],
145
145
  class: 'font-mono',
146
146
  },
147
147
 
@@ -98,11 +98,11 @@ class ImageWidget extends WidgetType {
98
98
  super();
99
99
  }
100
100
 
101
- override eq(other: this): boolean {
101
+ override eq(other: this) {
102
102
  return this._url === other._url;
103
103
  }
104
104
 
105
- override toDOM(view: EditorView): HTMLImageElement {
105
+ override toDOM(view: EditorView) {
106
106
  const img = document.createElement('img');
107
107
  img.setAttribute('src', this._url);
108
108
  img.setAttribute('class', 'cm-image');
@@ -112,14 +112,18 @@ class TableWidget extends WidgetType {
112
112
  super();
113
113
  }
114
114
 
115
- override eq(other: this): boolean {
115
+ override eq(other: this) {
116
116
  return (
117
117
  this._table.header?.join() === other._table.header?.join() &&
118
118
  this._table.rows?.join() === other._table.rows?.join()
119
119
  );
120
120
  }
121
121
 
122
- override toDOM(view: EditorView): HTMLDivElement {
122
+ override ignoreEvent(e: Event): boolean {
123
+ return !/^mouse/.test(e.type);
124
+ }
125
+
126
+ override toDOM(_view: EditorView) {
123
127
  const div = document.createElement('div');
124
128
  const table = div.appendChild(document.createElement('table'));
125
129
 
@@ -143,8 +147,4 @@ class TableWidget extends WidgetType {
143
147
 
144
148
  return div;
145
149
  }
146
-
147
- override ignoreEvent(e: Event): boolean {
148
- return !/^mouse/.test(e.type);
149
- }
150
150
  }
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { StateEffect, StateField } from '@codemirror/state';
6
+
7
+ export const modalStateEffect = StateEffect.define<boolean>();
8
+
9
+ /**
10
+ * Determines if a modal dialog (e.g., popover) is active.
11
+ */
12
+ export const modalStateField = StateField.define<boolean>({
13
+ create: () => false,
14
+ update: (value, tr) => {
15
+ let newValue = value;
16
+ for (const effect of tr.effects) {
17
+ if (effect.is(modalStateEffect)) {
18
+ newValue = effect.value;
19
+ }
20
+ }
21
+
22
+ return newValue;
23
+ },
24
+ });