@dxos/react-ui-editor 0.10.0 → 0.11.0

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 (113) hide show
  1. package/dist/lib/index.mjs +1477 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +32 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/types/src/components/Editor/Editor.d.ts +22 -17
  6. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  7. package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
  8. package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
  9. package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
  10. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
  11. package/dist/types/src/components/Editor/index.d.ts +2 -0
  12. package/dist/types/src/components/Editor/index.d.ts.map +1 -1
  13. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
  14. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  15. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
  16. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
  18. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
  20. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
  22. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
  24. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  27. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
  28. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  29. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
  31. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  32. package/dist/types/src/components/index.d.ts +1 -0
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +0 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
  37. package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
  38. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
  40. package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
  41. package/dist/types/src/stories/Comments.stories.d.ts +53 -14
  42. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  43. package/dist/types/src/stories/Diff.stories.d.ts +32 -0
  44. package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
  45. package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
  46. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  47. package/dist/types/src/stories/Folding.stories.d.ts +18 -0
  48. package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
  49. package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
  50. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  51. package/dist/types/src/stories/Popover.stories.d.ts +3 -3
  52. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  53. package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
  54. package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
  55. package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
  56. package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
  57. package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
  58. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
  59. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  60. package/dist/types/src/stories/components/util.d.ts +16 -0
  61. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +40 -83
  64. package/src/components/Editor/Editor.tsx +142 -35
  65. package/src/components/Editor/EditorContext.ts +34 -0
  66. package/src/components/Editor/EditorView.tsx +9 -6
  67. package/src/components/Editor/index.ts +3 -0
  68. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
  69. package/src/components/EditorMenuProvider/menu.ts +4 -2
  70. package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
  71. package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
  72. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
  73. package/src/components/EditorPreviewProvider/index.ts +2 -0
  74. package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
  75. package/src/components/EditorToolbar/index.ts +1 -0
  76. package/src/components/EditorToolbar/lists.ts +2 -6
  77. package/src/components/EditorToolbar/view-mode.ts +41 -12
  78. package/src/components/index.ts +1 -0
  79. package/src/index.ts +0 -1
  80. package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
  81. package/src/stories/Automerge.stories.tsx +99 -78
  82. package/src/stories/Blocks.stories.tsx +83 -0
  83. package/src/stories/Comments.stories.tsx +273 -74
  84. package/src/stories/Diff.stories.tsx +48 -0
  85. package/src/stories/EditorToolbar.stories.tsx +2 -2
  86. package/src/stories/Experimental.stories.tsx +13 -2
  87. package/src/stories/Folding.stories.tsx +120 -0
  88. package/src/stories/Markdown.stories.tsx +1 -1
  89. package/src/stories/Outliner.stories.tsx +18 -7
  90. package/src/stories/Popover.stories.tsx +27 -5
  91. package/src/stories/Suggest.stories.tsx +199 -0
  92. package/src/stories/TrackChanges.stories.tsx +226 -0
  93. package/src/stories/Widgets.stories.tsx +162 -10
  94. package/src/stories/components/EditorStory.tsx +15 -6
  95. package/src/stories/components/util.tsx +40 -2
  96. package/dist/lib/browser/index.mjs +0 -1676
  97. package/dist/lib/browser/index.mjs.map +0 -7
  98. package/dist/lib/browser/meta.json +0 -1
  99. package/dist/lib/browser/translations.mjs +0 -39
  100. package/dist/lib/browser/translations.mjs.map +0 -7
  101. package/dist/lib/node-esm/index.mjs +0 -1678
  102. package/dist/lib/node-esm/index.mjs.map +0 -7
  103. package/dist/lib/node-esm/meta.json +0 -1
  104. package/dist/lib/node-esm/translations.mjs +0 -41
  105. package/dist/lib/node-esm/translations.mjs.map +0 -7
  106. package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
  107. package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
  108. package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
  109. package/dist/types/src/extensions/index.d.ts +0 -2
  110. package/dist/types/src/extensions/index.d.ts.map +0 -1
  111. package/src/extensions/assistant-extension.tsx +0 -223
  112. package/src/extensions/index.ts +0 -5
  113. /package/dist/types/src/{extensions → stories}/Assistant.stories.d.ts +0 -0
@@ -0,0 +1,1477 @@
1
+ import { createContext } from "@radix-ui/react-context";
2
+ import { Atom } from "@effect-atom/atom";
3
+ import { Fragment, forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { invariant } from "@dxos/invariant";
6
+ import { mx } from "@dxos/ui-theme";
7
+ import { getProviderValue, isNonNullable, isTruthy } from "@dxos/util";
8
+ import { Inline, List, addBlockquote, addCodeblock, addLink, createBasicExtensions, createEditorStateTransaction, createMarkdownExtensions, createThemeExtensions, debugDispatcher, decorateMarkdown, documentId, initialSync, insertAtCursor, insertAtLineStart, insertTable, modalStateEffect, modalStateField, placeholder, removeBlockquote, removeCodeblock, removeLink, setHeading, setStyle, toggleList } from "@dxos/ui-editor";
9
+ import { EditorState, Prec, RangeSetBuilder, StateEffect, StateField, Transaction } from "@codemirror/state";
10
+ import { Decoration, EditorView, ViewPlugin, keymap } from "@codemirror/view";
11
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
12
+ import { addEventListener } from "@dxos/async";
13
+ import { DX_ANCHOR_ACTIVATE, ElevationProvider, Icon, Input, Popover, ScrollArea, toLocalizedString, useDynamicRef, useThemeContext, useTranslation } from "@dxos/react-ui";
14
+ import { translationKey } from "#translations";
15
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
16
+ import { Menu, MenuBuilder, useMenuActions } from "@dxos/react-ui-menu";
17
+ import { openSearchPanel } from "@codemirror/search";
18
+ import { log } from "@dxos/log";
19
+ //#region src/components/Editor/controller.ts
20
+ var withHiddenView = (controller) => {
21
+ Object.defineProperty(controller, "view", { enumerable: false });
22
+ return controller;
23
+ };
24
+ var noopController = withHiddenView({
25
+ get view() {
26
+ return null;
27
+ },
28
+ getText: () => "",
29
+ setText: () => {},
30
+ focus: () => {}
31
+ });
32
+ var createEditorController = (view) => {
33
+ return withHiddenView({
34
+ get view() {
35
+ return view;
36
+ },
37
+ getText: () => {
38
+ return view?.state.doc.toString() ?? "";
39
+ },
40
+ setText: (text, focus) => {
41
+ view?.dispatch({
42
+ changes: {
43
+ from: 0,
44
+ to: view?.state.doc.length ?? 0,
45
+ insert: text
46
+ },
47
+ selection: {
48
+ anchor: text.length,
49
+ head: text.length
50
+ }
51
+ });
52
+ if (focus) view?.focus();
53
+ },
54
+ focus: () => view?.focus()
55
+ });
56
+ };
57
+ //#endregion
58
+ //#region src/components/Editor/EditorContext.ts
59
+ var [EditorContextProvider, useEditorContext] = createContext("Editor");
60
+ /**
61
+ * Access the editor context. Must be used within `Editor.Root`.
62
+ */
63
+ //#endregion
64
+ //#region src/components/EditorMenuProvider/menu.ts
65
+ var getMenuItem = (groups, id) => {
66
+ return groups.flatMap((group) => group.items).find((item) => item.id === id);
67
+ };
68
+ /** Undefined when the menu is empty; at either end the current item is returned unchanged. */
69
+ var getNextMenuItem = (groups, id) => {
70
+ const items = groups.flatMap((group) => group.items);
71
+ const index = items.findIndex((item) => item.id === id);
72
+ return index < items.length - 1 ? items[index + 1] : items[index];
73
+ };
74
+ /** Undefined when the menu is empty; at either end the current item is returned unchanged. */
75
+ var getPreviousMenuItem = (groups, id) => {
76
+ const items = groups.flatMap((group) => group.items);
77
+ const index = items.findIndex((item) => item.id === id);
78
+ return index > 0 ? items[index - 1] : items[index];
79
+ };
80
+ var createMenuGroup = ({ id = "menu", label, filter, items }) => ({
81
+ id,
82
+ label,
83
+ items: items.filter((item) => !filter || item.toLowerCase().includes(filter.toLowerCase())).map((item, i) => ({
84
+ id: `${id}-${i}`,
85
+ label: item,
86
+ onSelect: ({ view, head }) => insertAtCursor(view, head, item)
87
+ }))
88
+ });
89
+ var filterMenuGroups = (groups, filter) => {
90
+ return groups.map((group) => ({
91
+ ...group,
92
+ items: group.items.filter(filter)
93
+ }));
94
+ };
95
+ //#endregion
96
+ //#region src/components/EditorMenuProvider/popover.ts
97
+ var DELIMITERS = [" ", ":"];
98
+ /**
99
+ * Creates a popover that appears when the trigger character is inserted.
100
+ * This can be used for context menus or autocompletion.
101
+ */
102
+ var popover = (options = {}) => {
103
+ return [
104
+ Prec.highest(popoverKeymap(options)),
105
+ popoverStateField,
106
+ popoverTriggerListener(options),
107
+ options.activateOnTyping && popoverAutoActivate(options),
108
+ popoverAnchorDecoration(options),
109
+ modalStateField,
110
+ options.trigger && placeholder({
111
+ content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
112
+ focusOnly: true,
113
+ ...options.placeholder
114
+ })
115
+ ].filter(isTruthy);
116
+ };
117
+ /**
118
+ * Listen for selection and document changes.
119
+ */
120
+ var popoverTriggerListener = (options) => EditorView.updateListener.of(({ view, docChanged }) => {
121
+ const { range: activeRange, trigger } = view.state.field(popoverStateField) ?? {};
122
+ if (!activeRange) return;
123
+ const text = view.state.doc.sliceString(activeRange.from, activeRange.to);
124
+ const selection = view.state.selection.main;
125
+ const shouldClose = (trigger ? trigger !== text[0] : false) || /\s/.test(trigger ? text.slice(1) : text) || selection.head < activeRange.from || selection.head > activeRange.to + 1;
126
+ const nextRange = shouldClose ? null : docChanged ? {
127
+ from: activeRange.from,
128
+ to: selection.head
129
+ } : activeRange;
130
+ if (nextRange !== activeRange) view.dispatch({ effects: popoverRangeEffect.of(nextRange ? {
131
+ range: nextRange,
132
+ trigger
133
+ } : null) });
134
+ if (shouldClose) options.onClose?.({ view });
135
+ });
136
+ /**
137
+ * Open the popover while typing a word, without requiring a trigger character. Once a range is
138
+ * active, {@link popoverTriggerListener} keeps it in sync and closes it (whitespace, cursor moves).
139
+ */
140
+ var popoverAutoActivate = (options) => EditorView.updateListener.of(({ view, docChanged, transactions }) => {
141
+ if (!docChanged || !transactions.some((tr) => tr.isUserEvent("input")) || view.state.field(popoverStateField)) return;
142
+ const selection = view.state.selection.main;
143
+ if (!selection.empty) return;
144
+ const line = view.state.doc.lineAt(selection.head);
145
+ const before = line.text.slice(0, selection.head - line.from);
146
+ const idx = getLastIndexOf(before, options.delimiters ?? DELIMITERS);
147
+ if (before.slice(idx + 1).length === 0) return;
148
+ view.dispatch({ effects: popoverRangeEffect.of({ range: {
149
+ from: line.from + idx + 1,
150
+ to: selection.head
151
+ } }) });
152
+ });
153
+ /**
154
+ * Popover navigation.
155
+ */
156
+ var popoverKeymap = (options) => {
157
+ const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
158
+ return keymap.of([
159
+ ...triggers.filter(isNonNullable).map((trigger) => ({
160
+ key: trigger,
161
+ run: (view) => {
162
+ const selection = view.state.selection.main;
163
+ const line = view.state.doc.lineAt(selection.head);
164
+ if (line.text.trim() === "" || selection.head === line.from || selection.head > line.from && line.text[selection.head - line.from - 1] === " ") {
165
+ view.dispatch({
166
+ changes: {
167
+ from: selection.head,
168
+ insert: trigger
169
+ },
170
+ selection: {
171
+ anchor: selection.head + 1,
172
+ head: selection.head + 1
173
+ },
174
+ effects: popoverRangeEffect.of({
175
+ trigger,
176
+ range: {
177
+ from: selection.head,
178
+ to: selection.head + 1
179
+ }
180
+ })
181
+ });
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ })),
187
+ options.triggerKey && {
188
+ key: options.triggerKey,
189
+ run: (view) => {
190
+ const selection = view.state.selection.main;
191
+ const line = view.state.doc.lineAt(selection.head);
192
+ let str = line.text.slice(0, selection.head - line.from);
193
+ const idx = getLastIndexOf(str, options.delimiters ?? DELIMITERS);
194
+ if (idx !== -1) str = str.slice(idx + 1);
195
+ const from = line.from + idx;
196
+ view.dispatch({
197
+ effects: popoverRangeEffect.of({ range: {
198
+ from: from + 1,
199
+ to: selection.head
200
+ } }),
201
+ changes: selection.head === view.state.doc.length ? {
202
+ from: from + 1,
203
+ to: selection.head,
204
+ insert: " "
205
+ } : void 0
206
+ });
207
+ return true;
208
+ }
209
+ },
210
+ {
211
+ key: "ArrowUp",
212
+ run: (view) => {
213
+ if (view.state.field(popoverStateField)?.range) {
214
+ options.onArrowUp?.({ view });
215
+ return true;
216
+ }
217
+ return false;
218
+ }
219
+ },
220
+ {
221
+ key: "ArrowDown",
222
+ run: (view) => {
223
+ if (view.state.field(popoverStateField)?.range) {
224
+ options.onArrowDown?.({ view });
225
+ return true;
226
+ }
227
+ return false;
228
+ }
229
+ },
230
+ {
231
+ key: "Enter",
232
+ run: (view) => {
233
+ if (view.state.field(popoverStateField)?.range) return options.onEnter?.({ view }) ?? false;
234
+ return false;
235
+ }
236
+ }
237
+ ].filter(isTruthy));
238
+ };
239
+ /**
240
+ * Creates a <dx-anchor> tag, which is used to anchor the Popver.
241
+ */
242
+ var popoverAnchorDecoration = (options) => {
243
+ return ViewPlugin.fromClass(class {
244
+ view;
245
+ _decorations = Decoration.none;
246
+ constructor(view) {
247
+ this.view = view;
248
+ }
249
+ update({ view, transactions }) {
250
+ const builder = new RangeSetBuilder();
251
+ const { range, trigger } = view.state.field(popoverStateField) ?? {};
252
+ if (range) {
253
+ const selection = view.state.selection.main;
254
+ if (selection.head >= range.from && selection.head <= range.to) builder.add(range.from, range.to + 1, Decoration.mark({
255
+ tagName: "dx-anchor",
256
+ class: "cm-popover-trigger",
257
+ attributes: {
258
+ "data-visible-focus": "false",
259
+ "data-auto-trigger": "true",
260
+ "data-trigger": trigger ?? options.triggerKey ?? ""
261
+ }
262
+ }));
263
+ if (transactions.some((tr) => tr.effects.some((effect) => effect.is(popoverRangeEffect)))) {
264
+ const content = view.state.sliceDoc(range.from + (trigger ? trigger.length : 0), range.to);
265
+ options.onTextChange?.({
266
+ view,
267
+ pos: selection.head,
268
+ text: content,
269
+ trigger
270
+ });
271
+ }
272
+ }
273
+ this._decorations = builder.finish();
274
+ }
275
+ }, { decorations: (v) => v._decorations });
276
+ };
277
+ var popoverRangeEffect = StateEffect.define();
278
+ var popoverStateField = StateField.define({
279
+ create: () => null,
280
+ update: (value, tr) => {
281
+ let newValue = value;
282
+ for (const effect of tr.effects) if (effect.is(popoverRangeEffect)) newValue = effect.value;
283
+ return newValue;
284
+ }
285
+ });
286
+ var getLastIndexOf = (str, delimiters) => Math.max(...delimiters.map((delim) => str.lastIndexOf(delim)));
287
+ //#endregion
288
+ //#region src/components/EditorMenuProvider/menu-presets.ts
289
+ /**
290
+ * Re-open the popover in link-query ("@") mode at the cursor, seeding the same query shape the user
291
+ * would get by typing the trigger manually: a single "@" for an inline link, "@@" for a block embed.
292
+ * The consumer's `@` handler (e.g. plugin-markdown's link query) renders the object picker and, on
293
+ * selection, replaces this range with the corresponding markdown link.
294
+ *
295
+ * Deferred to the next frame: selecting a slash-menu item deletes the "/" range, which closes the
296
+ * popover; opening synchronously races that close (`onActivate` reads a stale open state) and the
297
+ * picker never appears.
298
+ */
299
+ var openLinkQuery = (view, insert) => {
300
+ requestAnimationFrame(() => {
301
+ const { head } = view.state.selection.main;
302
+ const to = head + insert.length;
303
+ view.dispatch({
304
+ changes: {
305
+ from: head,
306
+ insert
307
+ },
308
+ selection: {
309
+ anchor: to,
310
+ head: to
311
+ },
312
+ effects: popoverRangeEffect.of({
313
+ trigger: "@",
314
+ range: {
315
+ from: head,
316
+ to
317
+ }
318
+ })
319
+ });
320
+ });
321
+ };
322
+ var formattingCommands = {
323
+ id: "markdown",
324
+ label: "Markdown",
325
+ items: [
326
+ {
327
+ id: "heading-1",
328
+ label: "Heading 1",
329
+ icon: "ph--text-h-one--regular",
330
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "# ")
331
+ },
332
+ {
333
+ id: "heading-2",
334
+ label: "Heading 2",
335
+ icon: "ph--text-h-two--regular",
336
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "## ")
337
+ },
338
+ {
339
+ id: "heading-3",
340
+ label: "Heading 3",
341
+ icon: "ph--text-h-three--regular",
342
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "### ")
343
+ },
344
+ {
345
+ id: "heading-4",
346
+ label: "Heading 4",
347
+ icon: "ph--text-h-four--regular",
348
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "#### ")
349
+ },
350
+ {
351
+ id: "heading-5",
352
+ label: "Heading 5",
353
+ icon: "ph--text-h-five--regular",
354
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "##### ")
355
+ },
356
+ {
357
+ id: "heading-6",
358
+ label: "Heading 6",
359
+ icon: "ph--text-h-six--regular",
360
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "###### ")
361
+ },
362
+ {
363
+ id: "bullet-list",
364
+ label: "Bullet List",
365
+ icon: "ph--list-bullets--regular",
366
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "- ")
367
+ },
368
+ {
369
+ id: "numbered-list",
370
+ label: "Numbered List",
371
+ icon: "ph--list-numbers--regular",
372
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "1. ")
373
+ },
374
+ {
375
+ id: "task-list",
376
+ label: "Task List",
377
+ icon: "ph--list-checks--regular",
378
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "- [ ] ")
379
+ },
380
+ {
381
+ id: "quote",
382
+ label: "Quote",
383
+ icon: "ph--quotes--regular",
384
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "> ")
385
+ },
386
+ {
387
+ id: "code-block",
388
+ label: "Code Block",
389
+ icon: "ph--code-block--regular",
390
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "```\n\n```")
391
+ },
392
+ {
393
+ id: "table",
394
+ label: "Table",
395
+ icon: "ph--table--regular",
396
+ onSelect: ({ view, head }) => insertAtLineStart(view, head, "| | | |\n|---|---|---|\n| | | |")
397
+ }
398
+ ]
399
+ };
400
+ var linkSlashCommands = {
401
+ id: "link",
402
+ label: "Link",
403
+ items: [{
404
+ id: "inline-link",
405
+ label: "Inline link",
406
+ icon: "ph--link--regular",
407
+ onSelect: ({ view }) => openLinkQuery(view, "@")
408
+ }, {
409
+ id: "inline-object",
410
+ label: "Inline object",
411
+ icon: "ph--lego--regular",
412
+ onSelect: ({ view }) => openLinkQuery(view, "@@")
413
+ }]
414
+ };
415
+ //#endregion
416
+ //#region src/components/EditorMenuProvider/EditorMenuProvider.tsx
417
+ var __dxlog_file$3 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/EditorMenuProvider/EditorMenuProvider.tsx";
418
+ /**
419
+ * Implements the Popover and listens for the `dx-anchor-activate` event from the `popover` extension's decoration.
420
+ * NOTE: We don't use DropdownMenu because the command menu needs to manage focus explicitly.
421
+ * I.e., focus must remain in the editor while displaying the menu (for type-ahead).
422
+ */
423
+ var EditorMenuProvider = ({ children, getView, groups, currentItem, open: openProp, defaultOpen, numItems = 8, search, query = "", searchPlaceholder, onOpenChange, onActivate, onSelect, onCancel, onQueryChange, onNavigate }) => {
424
+ const { t } = useTranslation(translationKey);
425
+ const triggerRef = useRef(null);
426
+ const getViewRef = useDynamicRef(getView);
427
+ const [open, setOpen] = useControllableState({
428
+ prop: openProp,
429
+ defaultProp: defaultOpen,
430
+ onChange: (open) => {
431
+ const view = getViewRef.current?.();
432
+ invariant(view, void 0, {
433
+ "~LogMeta": "~LogMeta",
434
+ F: __dxlog_file$3,
435
+ L: 90,
436
+ S: void 0,
437
+ A: ["view", ""]
438
+ });
439
+ onOpenChange?.({
440
+ view,
441
+ open
442
+ });
443
+ }
444
+ });
445
+ const [root, setRoot] = useState(null);
446
+ useEffect(() => {
447
+ if (!root) return;
448
+ return addEventListener(root, DX_ANCHOR_ACTIVATE, (event) => {
449
+ const { trigger, dxn } = event;
450
+ if (!dxn) {
451
+ triggerRef.current = trigger;
452
+ if (onActivate) {
453
+ const view = getViewRef.current?.();
454
+ if (view) onActivate({
455
+ view,
456
+ trigger: trigger.getAttribute("data-trigger") ?? void 0
457
+ });
458
+ } else requestAnimationFrame(() => setOpen(true));
459
+ }
460
+ }, {
461
+ capture: true,
462
+ passive: false
463
+ });
464
+ }, [root, onActivate]);
465
+ const handleSelect = useCallback((item) => {
466
+ const view = getViewRef.current?.();
467
+ invariant(view, void 0, {
468
+ "~LogMeta": "~LogMeta",
469
+ F: __dxlog_file$3,
470
+ L: 129,
471
+ S: void 0,
472
+ A: ["view", ""]
473
+ });
474
+ onSelect?.({
475
+ view,
476
+ item
477
+ });
478
+ setOpen(false);
479
+ view.focus();
480
+ }, [
481
+ getViewRef,
482
+ onSelect,
483
+ setOpen
484
+ ]);
485
+ const menuGroups = groups?.filter((group) => group.items.length > 0) ?? [];
486
+ const searchInputRef = useRef(null);
487
+ useEffect(() => {
488
+ if (!search || !open) return;
489
+ let inner;
490
+ const outer = requestAnimationFrame(() => {
491
+ inner = requestAnimationFrame(() => searchInputRef.current?.focus());
492
+ });
493
+ return () => {
494
+ cancelAnimationFrame(outer);
495
+ cancelAnimationFrame(inner);
496
+ };
497
+ }, [search, open]);
498
+ const handleSearchKeyDown = useCallback((event) => {
499
+ switch (event.key) {
500
+ case "ArrowUp":
501
+ case "ArrowDown":
502
+ event.preventDefault();
503
+ onNavigate?.(event.key === "ArrowUp" ? "up" : "down");
504
+ break;
505
+ case "Enter": {
506
+ const item = getMenuItem(menuGroups, currentItem);
507
+ if (item) {
508
+ event.preventDefault();
509
+ handleSelect(item);
510
+ }
511
+ break;
512
+ }
513
+ }
514
+ }, [
515
+ menuGroups,
516
+ currentItem,
517
+ handleSelect,
518
+ onNavigate
519
+ ]);
520
+ return /* @__PURE__ */ jsxs(Popover.Root, {
521
+ modal: false,
522
+ open,
523
+ onOpenChange: setOpen,
524
+ children: [
525
+ /* @__PURE__ */ jsx(Popover.VirtualTrigger, { virtualRef: triggerRef }),
526
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
527
+ align: "start",
528
+ classNames: ["flex flex-col", !search && !menuGroups.length && "hidden"],
529
+ style: { maxBlockSize: 36 * numItems + 10 + (search ? 36 : 0) },
530
+ onEscapeKeyDown: () => {
531
+ const currentView = getViewRef.current?.();
532
+ if (currentView) onCancel?.({ view: currentView });
533
+ },
534
+ onOpenAutoFocus: search ? void 0 : (event) => event.preventDefault(),
535
+ children: [
536
+ search && /* @__PURE__ */ jsx(Input.Root, { children: /* @__PURE__ */ jsx(Input.TextInput, {
537
+ ref: searchInputRef,
538
+ density: "sm",
539
+ variant: "subdued",
540
+ classNames: "shrink-0 mbe-1",
541
+ value: query,
542
+ placeholder: searchPlaceholder,
543
+ "aria-label": searchPlaceholder ?? t("search.label"),
544
+ onChange: (event) => onQueryChange?.(event.target.value),
545
+ onKeyDown: handleSearchKeyDown
546
+ }) }),
547
+ /* @__PURE__ */ jsx(Popover.Viewport, {
548
+ asChild: true,
549
+ classNames: "dx-container",
550
+ children: /* @__PURE__ */ jsx(ScrollArea.Root, {
551
+ thin: true,
552
+ children: /* @__PURE__ */ jsx(ScrollArea.Viewport, { children: /* @__PURE__ */ jsx(Menu$1, {
553
+ groups: menuGroups,
554
+ currentItem,
555
+ onSelect: handleSelect
556
+ }) })
557
+ })
558
+ }),
559
+ /* @__PURE__ */ jsx(Popover.Arrow, {})
560
+ ]
561
+ }) }),
562
+ /* @__PURE__ */ jsx("div", {
563
+ className: "contents",
564
+ ref: setRoot,
565
+ children
566
+ })
567
+ ]
568
+ });
569
+ };
570
+ var Menu$1 = ({ groups, currentItem, onSelect }) => {
571
+ const { tx } = useThemeContext();
572
+ return /* @__PURE__ */ jsx("ul", { children: groups.map((group, index) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuGroup, {
573
+ group,
574
+ currentItem,
575
+ onSelect
576
+ }), index < groups.length - 1 && /* @__PURE__ */ jsx("div", { className: tx("menu.separator", {}) })] }, group.id)) });
577
+ };
578
+ var MenuGroup = ({ group, currentItem, onSelect }) => {
579
+ const { tx } = useThemeContext();
580
+ const { t } = useTranslation();
581
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [group.label && /* @__PURE__ */ jsx("div", {
582
+ className: tx("menu.groupLabel", {}),
583
+ children: /* @__PURE__ */ jsx("span", { children: toLocalizedString(group.label, t) })
584
+ }), group.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
585
+ item,
586
+ current: currentItem === item.id,
587
+ onSelect
588
+ }, item.id))] });
589
+ };
590
+ var MenuItem = ({ item, current, onSelect }) => {
591
+ const { tx } = useThemeContext();
592
+ const { t } = useTranslation();
593
+ const listRef = useRef(null);
594
+ useEffect(() => {
595
+ if (current && listRef.current) listRef.current.scrollIntoView({
596
+ behavior: "instant",
597
+ block: "nearest"
598
+ });
599
+ }, [current]);
600
+ const handleSelect = useCallback(() => onSelect?.(item), [item, onSelect]);
601
+ return /* @__PURE__ */ jsxs("li", {
602
+ ref: listRef,
603
+ className: tx("menu.item", {}, [current && "bg-hover-surface"]),
604
+ onClick: handleSelect,
605
+ children: [item.icon && /* @__PURE__ */ jsx(Icon, { icon: item.icon }), /* @__PURE__ */ jsx("span", {
606
+ className: "grow truncate",
607
+ children: toLocalizedString(item.label, t)
608
+ })]
609
+ });
610
+ };
611
+ //#endregion
612
+ //#region src/components/EditorMenuProvider/useEditorMenu.ts
613
+ var __dxlog_file$2 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/EditorMenuProvider/useEditorMenu.ts";
614
+ /**
615
+ * ```tsx
616
+ * const { groupsRef, extension, ...menuProps } = useEditorMenu();
617
+ * const { parentRef, viewRef } = useTextEditor({ extensions: [extension] });
618
+ * return (
619
+ * <EditorMenuProvider getView={() => viewRef.current} groups={groupsRef.current} {...menuProps}>
620
+ * <div ref={parentRef} />
621
+ * </EditorMenuProvider>
622
+ * );
623
+ * ```
624
+ */
625
+ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, filter = true, searchTriggers, getMenu }) => {
626
+ const groupsRef = useRef([]);
627
+ const currentRef = useRef(null);
628
+ const [currentItem, setCurrentItem] = useState();
629
+ const [open, setOpen] = useState(false);
630
+ const [search, setSearch] = useState(false);
631
+ const [query, setQuery] = useState("");
632
+ const [_, refresh] = useState({});
633
+ const contextRef = useRef(null);
634
+ const requestRef = useRef(0);
635
+ /**
636
+ * Get filtered options.
637
+ */
638
+ const getMenuOptions = useCallback(async ({ text, trigger, ...props }) => {
639
+ const groups = await getMenu?.({
640
+ text,
641
+ trigger,
642
+ ...props
643
+ }) ?? [];
644
+ return filter && trigger !== "@" ? filterMenuGroups(groups, (item) => text ? item.label.toLowerCase().startsWith(text.toLowerCase()) : true) : groups;
645
+ }, [getMenu, filter]);
646
+ /**
647
+ * Recompute the menu and preselect the first item.
648
+ */
649
+ const updateGroups = useCallback(async ({ view, pos, text, trigger }) => {
650
+ const request = ++requestRef.current;
651
+ const groups = await getMenuOptions({
652
+ state: view.state,
653
+ pos,
654
+ text,
655
+ trigger
656
+ }) ?? [];
657
+ if (request !== requestRef.current) return;
658
+ groupsRef.current = groups;
659
+ const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
660
+ if (firstItem) {
661
+ setCurrentItem(firstItem.id);
662
+ currentRef.current = firstItem;
663
+ } else {
664
+ setCurrentItem(void 0);
665
+ currentRef.current = null;
666
+ }
667
+ refresh({});
668
+ }, [getMenuOptions]);
669
+ const handleOpenChange = useCallback(async ({ view, open }) => {
670
+ invariant(view, void 0, {
671
+ "~LogMeta": "~LogMeta",
672
+ F: __dxlog_file$2,
673
+ L: 136,
674
+ S: void 0,
675
+ A: ["view", ""]
676
+ });
677
+ setOpen(open);
678
+ if (!open) {
679
+ setCurrentItem(void 0);
680
+ setSearch(false);
681
+ setQuery("");
682
+ contextRef.current = null;
683
+ requestRef.current++;
684
+ groupsRef.current = [];
685
+ currentRef.current = null;
686
+ view.dispatch({ effects: [popoverRangeEffect.of(null)] });
687
+ }
688
+ requestAnimationFrame(() => {
689
+ view.dispatch({ effects: [modalStateEffect.of(open)] });
690
+ });
691
+ }, [getMenuOptions]);
692
+ const handleActivate = useCallback(async ({ view, trigger }) => {
693
+ const item = getMenuItem(groupsRef.current, currentItem);
694
+ if (item) currentRef.current = item;
695
+ if (!open) handleOpenChange({
696
+ view,
697
+ open: true,
698
+ trigger
699
+ });
700
+ }, [open, handleOpenChange]);
701
+ const handleSelect = useCallback(({ view, item }) => {
702
+ const { range } = view.state.field(popoverStateField) ?? {};
703
+ if (range) view.dispatch({ changes: {
704
+ from: range.from,
705
+ to: range.to,
706
+ insert: ""
707
+ } });
708
+ item.onSelect?.({
709
+ view,
710
+ head: view.state.selection.main.head
711
+ });
712
+ view.focus();
713
+ }, []);
714
+ const handleCancel = useCallback(({ view }) => {
715
+ const { range, trigger } = view.state.field(popoverStateField) ?? {};
716
+ if (range && trigger) view.dispatch({ changes: {
717
+ ...range,
718
+ insert: ""
719
+ } });
720
+ }, []);
721
+ const handleQueryChange = useCallback((query) => {
722
+ setQuery(query);
723
+ const context = contextRef.current;
724
+ if (context) updateGroups({
725
+ ...context,
726
+ text: context.text + query
727
+ });
728
+ }, [updateGroups]);
729
+ const handleNavigate = useCallback((direction) => {
730
+ setCurrentItem((currentItem) => {
731
+ const next = direction === "up" ? getPreviousMenuItem(groupsRef.current, currentItem) : getNextMenuItem(groupsRef.current, currentItem);
732
+ if (!next) return currentItem;
733
+ currentRef.current = next;
734
+ return next.id;
735
+ });
736
+ }, []);
737
+ const serializedTrigger = Array.isArray(trigger) ? trigger.join(",") : trigger;
738
+ const serializedSearchTriggers = searchTriggers?.join(",");
739
+ return {
740
+ groupsRef,
741
+ extension: useMemo(() => {
742
+ return popover({
743
+ trigger,
744
+ triggerKey,
745
+ placeholder,
746
+ activateOnTyping,
747
+ onClose: ({ view }) => handleOpenChange({
748
+ view,
749
+ open: false
750
+ }),
751
+ onEnter: ({ view }) => {
752
+ if (currentRef.current) {
753
+ handleSelect({
754
+ view,
755
+ item: currentRef.current
756
+ });
757
+ return true;
758
+ }
759
+ return false;
760
+ },
761
+ onArrowUp: () => handleNavigate("up"),
762
+ onArrowDown: () => handleNavigate("down"),
763
+ onTextChange: async ({ view, pos, text, trigger }) => {
764
+ contextRef.current = {
765
+ view,
766
+ pos,
767
+ text,
768
+ trigger
769
+ };
770
+ setSearch(!!trigger && !!searchTriggers?.includes(trigger));
771
+ setQuery("");
772
+ await updateGroups({
773
+ view,
774
+ pos,
775
+ text,
776
+ trigger
777
+ });
778
+ }
779
+ });
780
+ }, [
781
+ handleOpenChange,
782
+ handleNavigate,
783
+ updateGroups,
784
+ serializedTrigger,
785
+ serializedSearchTriggers,
786
+ placeholder,
787
+ activateOnTyping
788
+ ]),
789
+ currentItem,
790
+ open,
791
+ search,
792
+ query,
793
+ onOpenChange: handleOpenChange,
794
+ onActivate: handleActivate,
795
+ onSelect: handleSelect,
796
+ onCancel: handleCancel,
797
+ onQueryChange: handleQueryChange,
798
+ onNavigate: handleNavigate
799
+ };
800
+ };
801
+ //#endregion
802
+ //#region src/components/EditorToolbar/blocks.ts
803
+ var blockTypes = {
804
+ blockquote: "ph--quotes--regular",
805
+ codeblock: "ph--code-block--regular",
806
+ table: "ph--table--regular"
807
+ };
808
+ /** Add block actions to the builder. */
809
+ var addBlocks = (state, getView) => (builder) => {
810
+ const value = state?.blockQuote ? "blockquote" : state.blockType ?? "";
811
+ builder.group("block", {
812
+ label: ["block.label", { ns: translationKey }],
813
+ iconOnly: true,
814
+ variant: "toggleGroup",
815
+ selectCardinality: "single",
816
+ value
817
+ }, (group) => {
818
+ for (const [type, icon] of Object.entries(blockTypes)) {
819
+ const checked = type === value;
820
+ group.action(type, {
821
+ label: [`block.${type}.label`, { ns: translationKey }],
822
+ checked,
823
+ ...type === "table" && { disabled: !!state.blankLine },
824
+ icon
825
+ }, () => {
826
+ const view = getView();
827
+ if (!view) return;
828
+ switch (type) {
829
+ case "blockquote":
830
+ checked ? removeBlockquote(view) : addBlockquote(view);
831
+ break;
832
+ case "codeblock":
833
+ checked ? removeCodeblock(view) : addCodeblock(view);
834
+ break;
835
+ case "table":
836
+ insertTable(view);
837
+ break;
838
+ }
839
+ });
840
+ }
841
+ });
842
+ };
843
+ //#endregion
844
+ //#region src/components/EditorToolbar/formatting.ts
845
+ var formats = {
846
+ strong: "ph--text-b--regular",
847
+ emphasis: "ph--text-italic--regular",
848
+ strikethrough: "ph--text-strikethrough--regular",
849
+ code: "ph--code--regular",
850
+ link: "ph--link--regular"
851
+ };
852
+ /** Add formatting actions to the builder. */
853
+ var addFormatting = (state, getView) => (builder) => {
854
+ const formatting = state;
855
+ builder.group("formatting", {
856
+ label: ["formatting.label", { ns: translationKey }],
857
+ iconOnly: true,
858
+ variant: "toggleGroup",
859
+ selectCardinality: "multiple",
860
+ value: Object.keys(formats).filter((key) => !!formatting[key])
861
+ }, (group) => {
862
+ for (const [type, icon] of Object.entries(formats)) {
863
+ const checked = !!formatting[type];
864
+ group.action(type, {
865
+ label: [`formatting.${type}.label`, { ns: translationKey }],
866
+ checked,
867
+ icon
868
+ }, () => {
869
+ const view = getView();
870
+ if (!view) return;
871
+ if (type === "link") {
872
+ checked ? removeLink(view) : addLink()(view);
873
+ return;
874
+ }
875
+ setStyle(type === "strong" ? Inline.Strong : type === "emphasis" ? Inline.Emphasis : type === "strikethrough" ? Inline.Strikethrough : Inline.Code, !checked)(view);
876
+ });
877
+ }
878
+ });
879
+ };
880
+ //#endregion
881
+ //#region src/components/EditorToolbar/headings.ts
882
+ var headingIcons = {
883
+ 0: "ph--paragraph--regular",
884
+ 1: "ph--text-h-one--regular",
885
+ 2: "ph--text-h-two--regular",
886
+ 3: "ph--text-h-three--regular",
887
+ 4: "ph--text-h-four--regular",
888
+ 5: "ph--text-h-five--regular",
889
+ 6: "ph--text-h-six--regular"
890
+ };
891
+ var computeHeadingValue = (state) => {
892
+ const blockType = state ? state.blockType : "paragraph";
893
+ const heading = blockType && /heading(\d)/.exec(blockType);
894
+ return heading ? heading[1] : blockType === "paragraph" || !blockType ? "0" : "";
895
+ };
896
+ /** Add heading actions to the builder. */
897
+ var addHeadings = (state, getView) => (builder) => {
898
+ const headingValue = computeHeadingValue(state);
899
+ builder.group("heading", {
900
+ label: ["heading.label", { ns: translationKey }],
901
+ icon: "ph--text-h--regular",
902
+ iconOnly: true,
903
+ variant: "dropdownMenu",
904
+ applyActive: true,
905
+ selectCardinality: "single",
906
+ value: headingValue
907
+ }, (group) => {
908
+ for (const [levelStr, icon] of Object.entries(headingIcons)) {
909
+ const level = parseInt(levelStr);
910
+ group.action(`heading--${levelStr}`, {
911
+ label: ["heading-level.label", {
912
+ count: level,
913
+ ns: translationKey
914
+ }],
915
+ icon,
916
+ checked: levelStr === headingValue
917
+ }, () => setHeading(level)(getView()));
918
+ }
919
+ });
920
+ };
921
+ //#endregion
922
+ //#region src/components/EditorToolbar/image.ts
923
+ /** Add image upload action to the builder. */
924
+ var addImageUpload = (onImageUpload) => (builder) => {
925
+ builder.action("image", {
926
+ label: ["image.label", { ns: translationKey }],
927
+ testId: "editor.toolbar.image",
928
+ icon: "ph--image-square--regular"
929
+ }, onImageUpload);
930
+ };
931
+ //#endregion
932
+ //#region src/components/EditorToolbar/lists.ts
933
+ var listStyles = {
934
+ bullet: "ph--list-bullets--regular",
935
+ ordered: "ph--list-numbers--regular",
936
+ task: "ph--list-checks--regular"
937
+ };
938
+ /** Add list actions to the builder. */
939
+ var addLists = (state, getView) => (builder) => {
940
+ const value = state.listStyle ?? "";
941
+ builder.group("list", {
942
+ label: ["list.label", { ns: translationKey }],
943
+ iconOnly: true,
944
+ variant: "toggleGroup",
945
+ selectCardinality: "single",
946
+ value
947
+ }, (group) => {
948
+ for (const [listStyle, icon] of Object.entries(listStyles)) {
949
+ const checked = value === listStyle;
950
+ group.action(`list-${listStyle}`, {
951
+ label: [`list.${listStyle}.label`, { ns: translationKey }],
952
+ checked,
953
+ icon
954
+ }, () => {
955
+ const view = getView();
956
+ if (!view) return;
957
+ toggleList(listStyle === "ordered" ? List.Ordered : listStyle === "bullet" ? List.Bullet : List.Task)(view);
958
+ });
959
+ }
960
+ });
961
+ };
962
+ //#endregion
963
+ //#region src/components/EditorToolbar/search.ts
964
+ /** Add search action to the builder. */
965
+ var addSearch = (getView) => (builder) => {
966
+ builder.action("search", {
967
+ label: ["search.label", { ns: translationKey }],
968
+ testId: "editor.toolbar.search",
969
+ icon: "ph--magnifying-glass--regular"
970
+ }, () => openSearchPanel(getView()));
971
+ };
972
+ //#endregion
973
+ //#region src/components/EditorToolbar/view-mode.ts
974
+ /**
975
+ * The built-in view modes: preview (rendered), source (raw edit), readonly (non-editable source). Typed
976
+ * so each `id` is an {@link EditorViewMode}, letting callers pass `item.id` to `onViewModeChange` without a cast.
977
+ */
978
+ var defaultViewModeItems = [
979
+ {
980
+ id: "preview",
981
+ icon: "ph--eye--regular"
982
+ },
983
+ {
984
+ id: "source",
985
+ icon: "ph--pencil-simple--regular"
986
+ },
987
+ {
988
+ id: "readonly",
989
+ icon: "ph--pencil-slash--regular"
990
+ }
991
+ ];
992
+ /**
993
+ * Add view mode actions to the builder. `items` defaults to the three built-in editor view modes;
994
+ * callers pass an extended list to surface contributed modes (see {@link ViewModeItem}) in the same
995
+ * single-select dropdown. The trigger tracks the checked item (via the group's `applyActive`).
996
+ */
997
+ var addViewMode = (state, onViewModeChange, items = defaultViewModeItems) => (builder) => {
998
+ const value = state.viewMode ?? "source";
999
+ builder.group("viewMode", {
1000
+ label: ["view-mode.label", { ns: translationKey }],
1001
+ icon: "ph--eye--regular",
1002
+ iconOnly: true,
1003
+ variant: "dropdownMenu",
1004
+ applyActive: true,
1005
+ selectCardinality: "single",
1006
+ value
1007
+ }, (group) => {
1008
+ for (const item of items) {
1009
+ const checked = item.checked ?? item.id === value;
1010
+ group.action(`view-mode--${item.id}`, {
1011
+ label: item.label ?? [`view-mode.${item.id}.label`, { ns: translationKey }],
1012
+ checked,
1013
+ icon: item.icon
1014
+ }, () => item.onSelect ? item.onSelect() : onViewModeChange(item.id));
1015
+ }
1016
+ });
1017
+ };
1018
+ //#endregion
1019
+ //#region src/components/EditorToolbar/EditorToolbar.tsx
1020
+ var SECTION_ROLE = "org.dxos.role.section";
1021
+ var EditorToolbar$1 = memo(({ classNames, role, attendableId, onAction, ...props }) => {
1022
+ const menuActions = useMarkdownMenuActions(props);
1023
+ return /* @__PURE__ */ jsx(ElevationProvider, {
1024
+ elevation: role === SECTION_ROLE ? "positioned" : "base",
1025
+ children: /* @__PURE__ */ jsx(Menu.Root, {
1026
+ ...menuActions,
1027
+ attendableId,
1028
+ onAction,
1029
+ children: /* @__PURE__ */ jsx(Menu.Toolbar, { classNames })
1030
+ })
1031
+ });
1032
+ });
1033
+ var useMarkdownMenuActions = ({ state, getView, customActions, ...features }) => {
1034
+ return useMenuActions(useMemo(() => createMarkdownActions({
1035
+ state,
1036
+ getView,
1037
+ customActions,
1038
+ ...features
1039
+ }), [
1040
+ state,
1041
+ getView,
1042
+ customActions,
1043
+ features?.showHeadings,
1044
+ features?.showFormatting,
1045
+ features?.showLists,
1046
+ features?.showBlocks,
1047
+ features?.showSearch,
1048
+ features?.onImageUpload,
1049
+ features?.onViewModeChange,
1050
+ features?.viewModes
1051
+ ]));
1052
+ };
1053
+ var createMarkdownActions = ({ state, getView, customActions, ...features }) => {
1054
+ return Atom.make((get) => {
1055
+ const stateSnapshot = get(state);
1056
+ return MenuBuilder.make().subgraph(features?.showHeadings !== false && addHeadings(stateSnapshot, getView)).subgraph(features?.showFormatting !== false && addFormatting(stateSnapshot, getView)).subgraph(features?.showLists !== false && addLists(stateSnapshot, getView)).subgraph(features?.showBlocks !== false && addBlocks(stateSnapshot, getView)).subgraph(features?.onImageUpload && addImageUpload(features.onImageUpload)).separator("gap").subgraph(customActions && get(customActions)).subgraph(features?.showSearch !== false && addSearch(getView)).subgraph(features?.onViewModeChange && addViewMode(stateSnapshot, features.onViewModeChange, features.viewModes)).build();
1057
+ });
1058
+ };
1059
+ //#endregion
1060
+ //#region src/hooks/useBasicMarkdownExtensions.ts
1061
+ /**
1062
+ * Returns the standard CodeMirror extension stack for an inline markdown editor:
1063
+ * basic editor behaviors, themed syntax highlighting (theme mode read from {@link useThemeContext}),
1064
+ * markdown parsing, and visual decorations.
1065
+ *
1066
+ * Used by surfaces such as `AgentProperties` and `MagazineProperties` to render the
1067
+ * `instructions` field of an object as a small editable markdown block.
1068
+ *
1069
+ * The memoization depends on primitive option values plus the `extensions` array reference,
1070
+ * so callers passing fresh option literals each render still hit the cache.
1071
+ */
1072
+ var useBasicMarkdownExtensions = ({ placeholder, decorate = true, readonly = false, extensions } = {}) => {
1073
+ const { themeMode } = useThemeContext();
1074
+ return useMemo(() => [
1075
+ createBasicExtensions({
1076
+ placeholder,
1077
+ readOnly: readonly
1078
+ }),
1079
+ createThemeExtensions({
1080
+ syntaxHighlighting: true,
1081
+ themeMode
1082
+ }),
1083
+ createMarkdownExtensions(),
1084
+ ...decorate ? [decorateMarkdown()] : [],
1085
+ ...extensions ?? []
1086
+ ], [
1087
+ placeholder,
1088
+ themeMode,
1089
+ decorate,
1090
+ readonly,
1091
+ extensions
1092
+ ]);
1093
+ };
1094
+ //#endregion
1095
+ //#region src/hooks/useTextEditor.ts
1096
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextEditor.ts";
1097
+ var instanceCount = 0;
1098
+ /**
1099
+ * Creates codemirror text editor.
1100
+ */
1101
+ var useTextEditor = (props = {}, deps = []) => {
1102
+ const { id, doc, initialValue, extensions, autoFocus, scrollTo, selection, selectionEnd, debug } = useMemo(() => getProviderValue(props), deps ?? []);
1103
+ const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
1104
+ const [view, setView] = useState(null);
1105
+ const parentRef = useRef(null);
1106
+ useEffect(() => {
1107
+ let view = null;
1108
+ if (parentRef.current) {
1109
+ log("create", {
1110
+ id,
1111
+ instanceId,
1112
+ doc: initialValue?.length ?? 0
1113
+ }, {
1114
+ "~LogMeta": "~LogMeta",
1115
+ F: __dxlog_file$1,
1116
+ L: 75,
1117
+ S: void 0
1118
+ });
1119
+ let initialSelection;
1120
+ if (selection?.anchor && initialValue?.length) {
1121
+ if (selection.anchor <= initialValue.length && (selection?.head ?? 0) <= initialValue.length) initialSelection = selection;
1122
+ } else if (selectionEnd && selection === void 0) {
1123
+ const index = initialValue?.indexOf("\n");
1124
+ initialSelection = { anchor: !index || index === -1 ? 0 : index };
1125
+ }
1126
+ const state = EditorState.create({
1127
+ doc: doc ?? initialValue,
1128
+ extensions: [
1129
+ id && documentId.of(id),
1130
+ extensions,
1131
+ EditorView.exceptionSink.of((err) => {
1132
+ log.catch(err, void 0, {
1133
+ "~LogMeta": "~LogMeta",
1134
+ F: __dxlog_file$1,
1135
+ L: 97,
1136
+ S: void 0
1137
+ });
1138
+ })
1139
+ ].filter(isTruthy)
1140
+ });
1141
+ view = new EditorView({
1142
+ parent: parentRef.current,
1143
+ state,
1144
+ scrollTo: scrollTo ? EditorView.scrollIntoView(scrollTo, { yMargin: 96 }) : void 0,
1145
+ dispatchTransactions: debug ? debugDispatcher : void 0
1146
+ });
1147
+ if (selectionEnd && !initialSelection) {
1148
+ const { to } = view.state.doc.lineAt(0);
1149
+ if (to) view.dispatch({ selection: { anchor: to } });
1150
+ }
1151
+ setView(view);
1152
+ }
1153
+ return () => {
1154
+ log("destroy", { id }, {
1155
+ "~LogMeta": "~LogMeta",
1156
+ F: __dxlog_file$1,
1157
+ L: 122,
1158
+ S: void 0
1159
+ });
1160
+ view?.destroy();
1161
+ };
1162
+ }, deps);
1163
+ useEffect(() => {
1164
+ if (view) {
1165
+ if (scrollTo || selection) {
1166
+ if (selection && selection.anchor > view.state.doc.length) {
1167
+ log.warn("invalid selection", {
1168
+ length: view.state.doc.length,
1169
+ scrollTo,
1170
+ selection
1171
+ }, {
1172
+ "~LogMeta": "~LogMeta",
1173
+ F: __dxlog_file$1,
1174
+ L: 131,
1175
+ S: void 0
1176
+ });
1177
+ return;
1178
+ }
1179
+ view.dispatch(createEditorStateTransaction({
1180
+ scrollTo,
1181
+ selection
1182
+ }));
1183
+ }
1184
+ }
1185
+ }, [
1186
+ view,
1187
+ scrollTo,
1188
+ selection
1189
+ ]);
1190
+ useEffect(() => {
1191
+ if (view && autoFocus) view.focus();
1192
+ }, [autoFocus, view]);
1193
+ return {
1194
+ parentRef,
1195
+ view,
1196
+ focusAttributes: {
1197
+ tabIndex: 0,
1198
+ onKeyDown: useCallback((event) => {
1199
+ const { key, target, currentTarget } = event;
1200
+ switch (key) {
1201
+ case "Escape":
1202
+ if (view?.state.field(modalStateField, false)) return;
1203
+ (view?.contentDOM.closest("[tabindex=\"0\"]"))?.focus();
1204
+ break;
1205
+ case "Enter":
1206
+ event.preventDefault();
1207
+ if (target === currentTarget) view?.focus();
1208
+ break;
1209
+ }
1210
+ }, [view])
1211
+ }
1212
+ };
1213
+ };
1214
+ //#endregion
1215
+ //#region src/components/Editor/EditorView.tsx
1216
+ /**
1217
+ * Minimal text editor.
1218
+ * NOTE: This shouold not be used with the automerge extension.
1219
+ */
1220
+ var EditorView$2 = forwardRef(({ classNames, id, extensions, selectionEnd, focusable = true, value, onChange, ...props }, forwardedRef) => {
1221
+ const onChangeRef = useRef(onChange);
1222
+ onChangeRef.current = onChange;
1223
+ const { parentRef, focusAttributes, view } = useTextEditor(() => ({
1224
+ id,
1225
+ initialValue: value,
1226
+ selectionEnd,
1227
+ extensions: [extensions ?? [], EditorView.updateListener.of(({ view, docChanged, transactions }) => {
1228
+ if (!transactions.some((tr) => tr.annotation(Transaction.userEvent) === initialSync.value) && docChanged) onChangeRef.current?.(view.state.doc.toString());
1229
+ })],
1230
+ ...props
1231
+ }), [
1232
+ id,
1233
+ extensions,
1234
+ selectionEnd
1235
+ ]);
1236
+ useImperativeHandle(forwardedRef, () => {
1237
+ return createEditorController(view);
1238
+ }, [id, view]);
1239
+ useEffect(() => {
1240
+ if (!view || value === void 0) return;
1241
+ const next = value;
1242
+ if (view.state.doc.toString() === next) return;
1243
+ requestAnimationFrame(() => {
1244
+ if (view.state.doc.toString() === next) return;
1245
+ view.dispatch({
1246
+ annotations: initialSync,
1247
+ changes: [{
1248
+ from: 0,
1249
+ to: view.state.doc.length,
1250
+ insert: next
1251
+ }],
1252
+ selection: selectionEnd ? { anchor: next.length } : void 0
1253
+ });
1254
+ if (selectionEnd) view.focus();
1255
+ });
1256
+ }, [
1257
+ view,
1258
+ value,
1259
+ selectionEnd
1260
+ ]);
1261
+ return /* @__PURE__ */ jsx("div", {
1262
+ className: mx("w-full outline-hidden", classNames),
1263
+ ...focusable ? focusAttributes : {},
1264
+ ref: parentRef
1265
+ });
1266
+ });
1267
+ //#endregion
1268
+ //#region src/components/Editor/Editor.tsx
1269
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/Editor/Editor.tsx";
1270
+ /**
1271
+ * Root component for the Editor compound component.
1272
+ * Provides context for all child components and manages the editor controller state.
1273
+ */
1274
+ var EditorRoot = forwardRef(({ children, extensions: extensionsProp, widgets, viewMode, numItems, searchPlaceholder, ...props }, forwardedRef) => {
1275
+ const state = useMemo(() => Atom.make({ viewMode }), [viewMode]);
1276
+ const { groupsRef, extension, ...menuProps } = useEditorMenu(props);
1277
+ const extensions = useMemo(() => [extension, extensionsProp].filter(isNonNullable).flat(), [extension, extensionsProp]);
1278
+ const [controller, setController] = useState(noopController);
1279
+ useImperativeHandle(forwardedRef, () => controller, [controller]);
1280
+ return /* @__PURE__ */ jsx(EditorContextProvider, {
1281
+ controller,
1282
+ setController,
1283
+ extensions,
1284
+ widgets,
1285
+ state,
1286
+ children: /* @__PURE__ */ jsx(EditorMenuProvider, {
1287
+ getView: useCallback(() => controller?.view ?? null, [controller]),
1288
+ groups: groupsRef.current,
1289
+ numItems,
1290
+ searchPlaceholder,
1291
+ ...menuProps,
1292
+ children
1293
+ })
1294
+ });
1295
+ });
1296
+ EditorRoot.displayName = "Editor.Root";
1297
+ var EDITOR_CONTENT_NAME = "Editor.Content";
1298
+ /**
1299
+ * Content component that wraps the toolbar and editor view area.
1300
+ */
1301
+ var EditorContent = ({ classNames, children }) => {
1302
+ return /* @__PURE__ */ jsx("div", {
1303
+ className: mx("grid grid-rows-[min-content_1fr] h-full overflow-hidden", classNames),
1304
+ children
1305
+ });
1306
+ };
1307
+ EditorContent.displayName = EDITOR_CONTENT_NAME;
1308
+ var EDITOR_VIEW_NAME = "Editor.View";
1309
+ /**
1310
+ * View component that renders the actual CodeMirror editor.
1311
+ * Automatically registers the editor controller with the context.
1312
+ */
1313
+ var EditorView$1 = ({ extensions: providedExtensions, ...props }) => {
1314
+ const { extensions: additionalExtensions = [], setController } = useEditorContext(EDITOR_VIEW_NAME);
1315
+ const extensions = useMemo(() => [additionalExtensions, providedExtensions].filter(isNonNullable).flat(), [providedExtensions, additionalExtensions]);
1316
+ return /* @__PURE__ */ jsx(EditorView$2, {
1317
+ ...props,
1318
+ extensions,
1319
+ ref: setController
1320
+ });
1321
+ };
1322
+ EditorView$1.displayName = EDITOR_VIEW_NAME;
1323
+ var EDITOR_TOOLBAR_NAME = "Editor.Toolbar";
1324
+ /**
1325
+ * Toolbar component that provides editor formatting and control actions.
1326
+ * Automatically connects to the editor view through context.
1327
+ */
1328
+ var EditorToolbar = (props) => {
1329
+ const { controller, state } = useEditorContext(EDITOR_TOOLBAR_NAME);
1330
+ const getView = useCallback(() => {
1331
+ invariant(controller?.view, void 0, {
1332
+ "~LogMeta": "~LogMeta",
1333
+ F: __dxlog_file,
1334
+ L: 155,
1335
+ S: void 0,
1336
+ A: ["controller?.view", ""]
1337
+ });
1338
+ return controller?.view;
1339
+ }, [controller]);
1340
+ return /* @__PURE__ */ jsx(EditorToolbar$1, {
1341
+ ...props,
1342
+ getView,
1343
+ state
1344
+ });
1345
+ };
1346
+ EditorToolbar.displayName = EDITOR_TOOLBAR_NAME;
1347
+ var EDITOR_BLOCKS_NAME = "Editor.Blocks";
1348
+ /**
1349
+ * Renders the xmlTags widget portals (embedded blocks) contributed via the editor's `setWidgets`
1350
+ * callback. Place inside `Editor.Root`; the widgets come from context (pass them to `Editor.Root`).
1351
+ */
1352
+ var EditorBlocks = () => {
1353
+ const { widgets = [] } = useEditorContext(EDITOR_BLOCKS_NAME);
1354
+ return /* @__PURE__ */ jsx(Fragment$1, { children: widgets.map(({ id, root, Component, props }) => /* @__PURE__ */ jsx("div", { children: createPortal(/* @__PURE__ */ jsx(Component, { ...props }), root) }, id)) });
1355
+ };
1356
+ EditorBlocks.displayName = EDITOR_BLOCKS_NAME;
1357
+ var EDITOR_DIAGNOSTICS_NAME = "Editor.Diagnostics";
1358
+ /** Rough leaf count of a (recursively nested) CodeMirror extension tree. */
1359
+ var countExtensions = (extension) => extension == null ? 0 : Array.isArray(extension) ? extension.reduce((sum, child) => sum + countExtensions(child), 0) : 1;
1360
+ /**
1361
+ * Developer panel showing live editor state read from the CodeMirror `EditorState` (document size,
1362
+ * selection, viewport, configured-extension count). A debugging aid — gate it behind a dev/debug
1363
+ * setting; unrelated to `@codemirror/lint` diagnostics. Place inside `Editor.Root`, below the content.
1364
+ */
1365
+ var EditorDiagnostics = ({ classNames }) => {
1366
+ const { controller, extensions } = useEditorContext(EDITOR_DIAGNOSTICS_NAME);
1367
+ const view = controller?.view ?? null;
1368
+ const [, setRevision] = useState(0);
1369
+ const lastState = useRef(null);
1370
+ useEffect(() => {
1371
+ if (!view) return;
1372
+ let frame = 0;
1373
+ const tick = () => {
1374
+ if (view.state !== lastState.current) {
1375
+ lastState.current = view.state;
1376
+ setRevision((revision) => revision + 1);
1377
+ }
1378
+ frame = requestAnimationFrame(tick);
1379
+ };
1380
+ frame = requestAnimationFrame(tick);
1381
+ return () => cancelAnimationFrame(frame);
1382
+ }, [view]);
1383
+ if (!view) return null;
1384
+ const { state } = view;
1385
+ const { doc, selection } = state;
1386
+ const { main } = selection;
1387
+ const head = doc.lineAt(main.head);
1388
+ const extensionCount = countExtensions(extensions);
1389
+ const item = (label, value) => /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs("span", {
1390
+ className: "text-subdued",
1391
+ children: [label, " "]
1392
+ }), value] });
1393
+ return /* @__PURE__ */ jsxs("div", {
1394
+ className: mx("flex flex-wrap gap-x-4 gap-y-0.5 border-bs border-separator px-2 py-1 text-xs font-mono text-description", classNames),
1395
+ "data-testid": "editor.diagnostics",
1396
+ children: [
1397
+ item("chars", doc.length),
1398
+ item("lines", doc.lines),
1399
+ item("cursor", main.empty ? `${head.number}:${main.head - head.from + 1}` : `${main.from}–${main.to} (${main.to - main.from})`),
1400
+ selection.ranges.length > 1 && item("ranges", selection.ranges.length),
1401
+ item("viewport", `${view.viewport.from}–${view.viewport.to}`),
1402
+ item("mode", state.readOnly ? "read-only" : "editable"),
1403
+ item("extensions", extensionCount)
1404
+ ]
1405
+ });
1406
+ };
1407
+ EditorDiagnostics.displayName = EDITOR_DIAGNOSTICS_NAME;
1408
+ var Editor = {
1409
+ Root: EditorRoot,
1410
+ Toolbar: EditorToolbar,
1411
+ Content: EditorContent,
1412
+ View: EditorView$1,
1413
+ Blocks: EditorBlocks,
1414
+ Diagnostics: EditorDiagnostics
1415
+ };
1416
+ //#endregion
1417
+ //#region src/components/EditorPreviewProvider/EditorPreviewContext.ts
1418
+ var [EditorPreviewContextProvider, useEditorPreview] = createContext("PreviewPopover", {});
1419
+ //#endregion
1420
+ //#region src/components/EditorPreviewProvider/EditorPreviewProvider.tsx
1421
+ /**
1422
+ * Standalone anchor-preview popover for use outside Composer (storybook, tests).
1423
+ * In Composer, PreviewPlugin listens on window for DX_ANCHOR_ACTIVATE events and
1424
+ * dispatches LayoutOperation.UpdatePopover instead; this provider is not needed there.
1425
+ */
1426
+ var EditorPreviewProvider = ({ children, onLookup }) => {
1427
+ const triggerRef = useRef(null);
1428
+ const [value, setValue] = useState({});
1429
+ const [open, setOpen] = useState(false);
1430
+ const handleActivate = useCallback((event) => {
1431
+ const { dxn, label, trigger } = event;
1432
+ setValue((value) => ({
1433
+ ...value,
1434
+ link: {
1435
+ label,
1436
+ dxn
1437
+ },
1438
+ pending: true
1439
+ }));
1440
+ triggerRef.current = trigger;
1441
+ queueMicrotask(() => setOpen(true));
1442
+ onLookup?.({
1443
+ label,
1444
+ dxn
1445
+ }).then((target) => setValue((value) => ({
1446
+ ...value,
1447
+ target: target ?? void 0,
1448
+ pending: false
1449
+ })));
1450
+ }, [onLookup]);
1451
+ const [root, setRoot] = useState(null);
1452
+ useEffect(() => {
1453
+ if (!root || !handleActivate) return;
1454
+ return addEventListener(root, DX_ANCHOR_ACTIVATE, handleActivate, {
1455
+ capture: true,
1456
+ passive: false
1457
+ });
1458
+ }, [root, handleActivate]);
1459
+ return /* @__PURE__ */ jsx(EditorPreviewContextProvider, {
1460
+ pending: value.pending,
1461
+ link: value.link,
1462
+ target: value.target,
1463
+ children: /* @__PURE__ */ jsxs(Popover.Root, {
1464
+ open,
1465
+ onOpenChange: setOpen,
1466
+ children: [/* @__PURE__ */ jsx(Popover.VirtualTrigger, { virtualRef: triggerRef }), /* @__PURE__ */ jsx("div", {
1467
+ className: "contents",
1468
+ ref: setRoot,
1469
+ children
1470
+ })]
1471
+ })
1472
+ });
1473
+ };
1474
+ //#endregion
1475
+ export { Editor, EditorMenuProvider, EditorPreviewProvider, addViewMode, createEditorController, createMenuGroup, defaultViewModeItems, filterMenuGroups, formattingCommands, getMenuItem, getNextMenuItem, getPreviousMenuItem, linkSlashCommands, noopController, popover, popoverRangeEffect, popoverStateField, useBasicMarkdownExtensions, useEditorContext, useEditorMenu, useEditorPreview, useTextEditor };
1476
+
1477
+ //# sourceMappingURL=index.mjs.map