@dxos/react-ui-editor 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29

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 (144) hide show
  1. package/dist/lib/browser/index.mjs +754 -731
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +39 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +754 -731
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +41 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/components/Editor/Editor.d.ts +36 -25
  12. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  13. package/dist/types/src/components/Editor/Editor.stories.d.ts +4 -4
  14. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/{EditorContent/EditorContent.d.ts → Editor/EditorView.d.ts} +5 -5
  16. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -0
  17. package/dist/types/src/components/Editor/controller.d.ts.map +1 -0
  18. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +1 -3
  19. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -1
  21. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +2 -1
  23. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +2 -2
  27. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  28. package/dist/types/src/components/EditorToolbar/blocks.d.ts +4 -19
  29. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/formatting.d.ts +4 -19
  31. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  32. package/dist/types/src/components/EditorToolbar/headings.d.ts +4 -19
  33. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  34. package/dist/types/src/components/EditorToolbar/image.d.ts +3 -9
  35. package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -1
  36. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -2
  37. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  38. package/dist/types/src/components/EditorToolbar/lists.d.ts +6 -0
  39. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -0
  40. package/dist/types/src/components/EditorToolbar/search.d.ts +3 -9
  41. package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -1
  42. package/dist/types/src/components/EditorToolbar/types.d.ts +6 -0
  43. package/dist/types/src/components/EditorToolbar/types.d.ts.map +1 -0
  44. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +5 -20
  45. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  46. package/dist/types/src/components/index.d.ts +0 -2
  47. package/dist/types/src/components/index.d.ts.map +1 -1
  48. package/dist/types/src/extensions/Assistant.stories.d.ts +10 -0
  49. package/dist/types/src/extensions/Assistant.stories.d.ts.map +1 -0
  50. package/dist/types/src/extensions/assistant-extension.d.ts +24 -0
  51. package/dist/types/src/extensions/assistant-extension.d.ts.map +1 -0
  52. package/dist/types/src/extensions/index.d.ts +2 -0
  53. package/dist/types/src/extensions/index.d.ts.map +1 -0
  54. package/dist/types/src/hooks/index.d.ts +1 -0
  55. package/dist/types/src/hooks/index.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts +25 -0
  57. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts.map +1 -0
  58. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  59. package/dist/types/src/index.d.ts +1 -2
  60. package/dist/types/src/index.d.ts.map +1 -1
  61. package/dist/types/src/stories/Automerge.stories.d.ts +24 -24
  62. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  63. package/dist/types/src/stories/Comments.stories.d.ts +1 -1
  64. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  65. package/dist/types/src/stories/EditorToolbar.stories.d.ts +28 -26
  66. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  67. package/dist/types/src/stories/Experimental.stories.d.ts +2 -2
  68. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  69. package/dist/types/src/stories/Markdown.stories.d.ts +1 -1
  70. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  71. package/dist/types/src/stories/Outliner.stories.d.ts +2 -2
  72. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  73. package/dist/types/src/stories/Popover.stories.d.ts +2 -2
  74. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  75. package/dist/types/src/stories/Preview.stories.d.ts +1 -1
  76. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  77. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  78. package/dist/types/src/stories/TextEditor.stories.d.ts +1 -1
  79. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  80. package/dist/types/src/stories/Theme.stories.d.ts.map +1 -1
  81. package/dist/types/src/stories/components/EditorStory.d.ts +2 -2
  82. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  83. package/dist/types/src/stories/components/util.d.ts +2 -1
  84. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  85. package/dist/types/src/translations.d.ts +24 -24
  86. package/dist/types/src/translations.d.ts.map +1 -1
  87. package/dist/types/src/util/react.d.ts +1 -1
  88. package/dist/types/src/util/react.d.ts.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +57 -48
  91. package/src/components/Editor/Editor.stories.tsx +15 -21
  92. package/src/components/Editor/Editor.tsx +54 -53
  93. package/src/components/Editor/EditorView.tsx +102 -0
  94. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +3 -5
  95. package/src/components/EditorMenuProvider/menu-presets.ts +1 -0
  96. package/src/components/EditorMenuProvider/popover.ts +3 -1
  97. package/src/components/EditorMenuProvider/useEditorMenu.ts +2 -1
  98. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -1
  99. package/src/components/EditorToolbar/EditorToolbar.tsx +29 -48
  100. package/src/components/EditorToolbar/blocks.ts +54 -46
  101. package/src/components/EditorToolbar/formatting.ts +44 -45
  102. package/src/components/EditorToolbar/headings.ts +44 -50
  103. package/src/components/EditorToolbar/image.ts +16 -21
  104. package/src/components/EditorToolbar/index.ts +2 -3
  105. package/src/components/EditorToolbar/lists.ts +58 -0
  106. package/src/components/EditorToolbar/search.ts +16 -21
  107. package/src/components/EditorToolbar/types.ts +8 -0
  108. package/src/components/EditorToolbar/view-mode.ts +37 -43
  109. package/src/components/index.ts +0 -3
  110. package/src/extensions/Assistant.stories.tsx +112 -0
  111. package/src/extensions/assistant-extension.tsx +223 -0
  112. package/src/extensions/index.ts +5 -0
  113. package/src/hooks/index.ts +1 -0
  114. package/src/hooks/useBasicMarkdownExtensions.ts +55 -0
  115. package/src/index.ts +1 -4
  116. package/src/stories/Automerge.stories.tsx +4 -3
  117. package/src/stories/Comments.stories.tsx +2 -2
  118. package/src/stories/EditorToolbar.stories.tsx +36 -64
  119. package/src/stories/Experimental.stories.tsx +10 -10
  120. package/src/stories/Outliner.stories.tsx +3 -4
  121. package/src/stories/Popover.stories.tsx +6 -7
  122. package/src/stories/Preview.stories.tsx +6 -7
  123. package/src/stories/Theme.stories.tsx +2 -2
  124. package/src/stories/components/EditorStory.tsx +17 -8
  125. package/src/stories/components/util.tsx +37 -35
  126. package/src/translations.ts +29 -24
  127. package/src/util/react.tsx +1 -1
  128. package/dist/types/src/components/EditorContent/EditorContent.d.ts.map +0 -1
  129. package/dist/types/src/components/EditorContent/controller.d.ts.map +0 -1
  130. package/dist/types/src/components/EditorContent/index.d.ts +0 -3
  131. package/dist/types/src/components/EditorContent/index.d.ts.map +0 -1
  132. package/dist/types/src/components/EditorToolbar/actions.d.ts +0 -25
  133. package/dist/types/src/components/EditorToolbar/actions.d.ts.map +0 -1
  134. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts +0 -11
  135. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts.map +0 -1
  136. package/dist/types/src/stories/CommandDialog.stories.d.ts +0 -14
  137. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +0 -1
  138. package/src/components/EditorContent/EditorContent.tsx +0 -83
  139. package/src/components/EditorContent/index.ts +0 -6
  140. package/src/components/EditorToolbar/actions.ts +0 -87
  141. package/src/components/EditorToolbar/useEditorToolbar.ts +0 -20
  142. package/src/stories/CommandDialog.stories.tsx +0 -81
  143. /package/dist/types/src/components/{EditorContent → Editor}/controller.d.ts +0 -0
  144. /package/src/components/{EditorContent → Editor}/controller.ts +0 -0
@@ -2,54 +2,48 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Node } from '@dxos/app-graph';
6
- import { type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
7
- import { type EditorViewMode } from '@dxos/ui-editor';
5
+ import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
6
+ import { type EditorViewMode } from '@dxos/ui-editor/types';
8
7
 
9
- import { translationKey } from '../../translations';
8
+ import { translationKey } from '#translations';
10
9
 
11
- import { createEditorAction, createEditorActionGroup } from './actions';
12
- import { type EditorToolbarState } from './useEditorToolbar';
10
+ import { type EditorToolbarState } from './types';
13
11
 
14
- const createViewModeGroupAction = (value: string) =>
15
- createEditorActionGroup(
16
- 'viewMode',
17
- {
18
- variant: 'dropdownMenu',
19
- applyActive: true,
20
- selectCardinality: 'single',
21
- value,
22
- } as ToolbarMenuActionGroupProperties,
23
- 'ph--eye--regular',
24
- );
12
+ const viewModes = {
13
+ preview: 'ph--eye--regular',
14
+ source: 'ph--pencil-simple--regular',
15
+ readonly: 'ph--pencil-slash--regular',
16
+ };
25
17
 
26
- const createViewModeActions = (value: string, onViewModeChange: (mode: EditorViewMode) => void) =>
27
- Object.entries({
28
- preview: 'ph--eye--regular',
29
- source: 'ph--pencil-simple--regular',
30
- readonly: 'ph--pencil-slash--regular',
31
- }).map(([viewMode, icon]) => {
32
- const checked = viewMode === value;
33
- return createEditorAction(
34
- `view-mode--${viewMode}`,
18
+ /** Add view mode actions to the builder. */
19
+ export const addViewMode =
20
+ (state: EditorToolbarState, onViewModeChange: (mode: EditorViewMode) => void): ActionGroupBuilderFn =>
21
+ (builder) => {
22
+ const value = state.viewMode ?? 'source';
23
+ builder.group(
24
+ 'viewMode',
35
25
  {
36
- label: [`${viewMode} mode label`, { ns: translationKey }],
37
- checked,
38
- icon,
26
+ label: ['view-mode.label', { ns: translationKey }],
27
+ icon: 'ph--eye--regular',
28
+ iconOnly: true,
29
+ variant: 'dropdownMenu',
30
+ applyActive: true,
31
+ selectCardinality: 'single',
32
+ value,
33
+ } as ToolbarMenuActionGroupProperties,
34
+ (group) => {
35
+ for (const [viewMode, icon] of Object.entries(viewModes)) {
36
+ const checked = viewMode === value;
37
+ group.action(
38
+ `view-mode--${viewMode}`,
39
+ {
40
+ label: [`view-mode.${viewMode}.label`, { ns: translationKey }],
41
+ checked,
42
+ icon,
43
+ },
44
+ () => onViewModeChange(viewMode as EditorViewMode),
45
+ );
46
+ }
39
47
  },
40
- () => onViewModeChange(viewMode as EditorViewMode),
41
48
  );
42
- });
43
-
44
- export const createViewMode = (state: EditorToolbarState, onViewModeChange: (mode: EditorViewMode) => void) => {
45
- const value = state.viewMode ?? 'source';
46
- const viewModeGroupAction = createViewModeGroupAction(value);
47
- const viewModeActions = createViewModeActions(value, onViewModeChange);
48
- return {
49
- nodes: [viewModeGroupAction as Node.NodeArg<any>, ...viewModeActions],
50
- edges: [
51
- { source: 'root', target: 'viewMode', relation: 'child' },
52
- ...viewModeActions.map(({ id }) => ({ source: viewModeGroupAction.id, target: id, relation: 'child' })),
53
- ],
54
49
  };
55
- };
@@ -4,8 +4,5 @@
4
4
 
5
5
  export * from './Editor';
6
6
 
7
- // TODO(burdon): Remove once Editor is fully migrated.
8
- export { EditorContent, createEditorController } from './EditorContent';
9
7
  export * from './EditorMenuProvider';
10
8
  export * from './EditorPreviewProvider';
11
- export { EditorToolbar, type EditorToolbarProps, type EditorToolbarState, useEditorToolbar } from './EditorToolbar';
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as LanguageModel from '@effect/ai/LanguageModel';
6
+ import type { Meta, StoryObj } from '@storybook/react-vite';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Layer from 'effect/Layer';
9
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import React, { useEffect, useMemo, useState } from 'react';
11
+
12
+ import { AiService } from '@dxos/ai';
13
+ import { AiServiceTestingPreset } from '@dxos/ai/testing';
14
+ import { useThemeContext } from '@dxos/react-ui';
15
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { compactSlots, createBasicExtensions, createThemeExtensions } from '@dxos/ui-editor';
17
+ import { trim } from '@dxos/util';
18
+
19
+ import { translations } from '#translations';
20
+
21
+ import { Editor, type EditorViewProps } from '../components';
22
+ import { assistant, type AssistantOptions } from './assistant-extension';
23
+
24
+ // TODO(burdon): Factor out.
25
+ const useTestGenerate = () => {
26
+ const [generate, setGenerate] = useState<AssistantOptions['generate']>();
27
+ useEffect(() => {
28
+ let disposed = false;
29
+ const rt = ManagedRuntime.make(
30
+ AiService.model('@anthropic/claude-haiku-4-5').pipe(
31
+ Layer.provide(AiServiceTestingPreset('edge-remote')),
32
+ Layer.orDie,
33
+ ),
34
+ );
35
+
36
+ if (!disposed) {
37
+ setGenerate(
38
+ () =>
39
+ ({ instructions, content }: { instructions: string; content: string }) =>
40
+ rt.runPromise(
41
+ Effect.gen(function* () {
42
+ const prompt = [instructions, content].join('\n\n');
43
+ const response = yield* LanguageModel.generateText({ prompt });
44
+ return response.text;
45
+ }),
46
+ ),
47
+ );
48
+ }
49
+
50
+ return () => {
51
+ disposed = true;
52
+ void rt.dispose();
53
+ };
54
+ }, []);
55
+
56
+ return generate;
57
+ };
58
+
59
+ type DefaultStoryProps = Pick<EditorViewProps, 'value'>;
60
+
61
+ const DefaultStory = (props: DefaultStoryProps) => {
62
+ const { themeMode } = useThemeContext();
63
+ const generate = useTestGenerate();
64
+ const extensions = useMemo(
65
+ () =>
66
+ generate
67
+ ? [
68
+ createBasicExtensions({ placeholder: 'Type here...' }),
69
+ createThemeExtensions({ themeMode, slots: compactSlots }),
70
+ assistant({ generate }),
71
+ ]
72
+ : [],
73
+ [generate, themeMode],
74
+ );
75
+
76
+ if (extensions.length === 0) {
77
+ return <Loading />;
78
+ }
79
+
80
+ return (
81
+ <Editor.Root>
82
+ <Editor.View {...props} classNames='dx-container border border-subdued-separator' extensions={extensions} />
83
+ </Editor.Root>
84
+ );
85
+ };
86
+
87
+ const meta: Meta<typeof DefaultStory> = {
88
+ title: 'ui/react-ui-editor/Assistant',
89
+ render: DefaultStory,
90
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
91
+ tags: ['experimental'],
92
+ parameters: {
93
+ layout: 'fullscreen',
94
+ translations,
95
+ },
96
+ };
97
+
98
+ export default meta;
99
+
100
+ type Story = StoryObj<typeof meta>;
101
+
102
+ export const Default: Story = {
103
+ args: {
104
+ value: trim`
105
+ This text has a speling mistake.
106
+
107
+ And it grammatical errors.
108
+
109
+ But we can fix it.
110
+ `,
111
+ },
112
+ };
@@ -0,0 +1,223 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Diagnostic, forEachDiagnostic, linter, setDiagnostics } from '@codemirror/lint';
6
+ import { ChangeSet, type Extension } from '@codemirror/state';
7
+ import { EditorView } from '@codemirror/view';
8
+
9
+ import { log } from '@dxos/log';
10
+ import { safeParseJson, trim } from '@dxos/util';
11
+
12
+ const DEFAULT_INSTRUCTIONS = trim`
13
+ Proofread the input text below.
14
+ Identify typos and grammatical errors.
15
+ Return ONLY a valid JSON array of objects with fields: "original" (string), "replacement" (string), "context" (string, 3-5 words around match).
16
+ --
17
+ `;
18
+
19
+ export type AssistantOptions = {
20
+ /**
21
+ * Invoke the language model with the given prompt and return the raw text response.
22
+ */
23
+ generate: (request: { instructions: string; content: string }) => Promise<string>;
24
+ /**
25
+ * Instructions to use for the language model.
26
+ */
27
+ instructions?: string;
28
+ /**
29
+ * Show panel automatically.
30
+ */
31
+ autoPanel?: boolean;
32
+ /**
33
+ * Debounce delay.
34
+ */
35
+ delay?: number;
36
+ };
37
+
38
+ const underline = (color: string) => {
39
+ const svg = trim`
40
+ <svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">
41
+ <path d="m0 3 l2 -2 l1 0 l2 2 l1 0" stroke="${color}" fill="none" stroke-width="1"/>
42
+ </svg>
43
+ `;
44
+
45
+ return `url('data:image/svg+xml;base64,${btoa(svg)}') !important`;
46
+ };
47
+
48
+ export const assistant = (options: AssistantOptions): Extension[] => {
49
+ const styles = getComputedStyle(document.documentElement);
50
+ const style = {
51
+ info: styles.getPropertyValue('--color-green-500').trim(),
52
+ warning: styles.getPropertyValue('--color-orange-500').trim(),
53
+ error: styles.getPropertyValue('--color-rose-500').trim(),
54
+ };
55
+
56
+ return [
57
+ assistantLinter(options),
58
+ EditorView.baseTheme({
59
+ '.cm-lintRange-info': {
60
+ backgroundImage: underline(style.info),
61
+ },
62
+ '.cm-lintRange-warning': {
63
+ backgroundImage: underline(style.warning),
64
+ },
65
+ '.cm-lintRange-error': {
66
+ backgroundImage: underline(style.error),
67
+ },
68
+
69
+ '.cm-panels-bottom': {
70
+ borderTop: '1px solid var(--color-separator) !important',
71
+ },
72
+ '.cm-panel-lint .cm-panel': {
73
+ outline: 'none !important',
74
+ },
75
+ /** @apply dx-button */
76
+ '.cm-panel button': {
77
+ color: 'var(--color-base-surface-text) !important',
78
+ },
79
+ '.cm-panel.cm-panel-lint ul': {
80
+ color: 'var(--color-base-surface-text) !important',
81
+ backgroundColor: 'var(--color-base-surface) !important',
82
+ marginRight: '2rem !important',
83
+ },
84
+ '.cm-panel.cm-panel-lint ul [aria-selected]': {
85
+ color: 'var(--color-base-surface-text) !important',
86
+ backgroundColor: 'var(--color-base-surface) !important',
87
+ },
88
+ '.cm-panel.cm-panel-lint ul li': {
89
+ display: 'grid',
90
+ gridTemplateColumns: '1fr auto',
91
+ alignItems: 'center',
92
+ },
93
+ '.cm-panel.cm-panel-lint ul li .cm-diagnosticText': {
94
+ paddingRight: '8px !important',
95
+ },
96
+ '.cm-panel.cm-panel-lint ul li button.cm-diagnosticAction': {
97
+ margin: 'none !important',
98
+ },
99
+ '.cm-diagnostic': {
100
+ padding: '0px 8px !important',
101
+ whiteSpace: 'pre-wrap !important',
102
+ },
103
+ '.cm-diagnostic-info': {
104
+ border: 'none !important',
105
+ },
106
+ }),
107
+ ];
108
+ };
109
+
110
+ //
111
+ // Linter
112
+ //
113
+
114
+ type Suggestion = {
115
+ original: string;
116
+ replacement: string;
117
+ context: string;
118
+ };
119
+
120
+ const isSuggestion = (value: unknown): value is Suggestion =>
121
+ typeof value === 'object' &&
122
+ value !== null &&
123
+ typeof (value as Suggestion).original === 'string' &&
124
+ typeof (value as Suggestion).replacement === 'string' &&
125
+ typeof (value as Suggestion).context === 'string';
126
+
127
+ /**
128
+ * Find the index of `original` within `content`, using `context` to disambiguate when there are multiple occurrences.
129
+ */
130
+ const findSuggestionIndex = (content: string, suggestion: Suggestion): number => {
131
+ const firstIdx = content.indexOf(suggestion.original);
132
+ if (firstIdx === -1) {
133
+ return -1;
134
+ }
135
+
136
+ // Check for duplicate occurrences; use context to disambiguate.
137
+ const secondIdx = content.indexOf(suggestion.original, firstIdx + 1);
138
+ if (secondIdx === -1) {
139
+ return firstIdx;
140
+ }
141
+
142
+ // Find the occurrence whose surrounding text best matches the context.
143
+ const contextIdx = content.indexOf(suggestion.context);
144
+ if (contextIdx !== -1) {
145
+ const contextEnd = contextIdx + suggestion.context.length;
146
+ if (secondIdx >= contextIdx && secondIdx <= contextEnd) {
147
+ return secondIdx;
148
+ }
149
+ }
150
+
151
+ return firstIdx;
152
+ };
153
+
154
+ const replaceTextAndDropLintAtRange = (view: EditorView, from: number, to: number, insert: string) => {
155
+ const kept: Diagnostic[] = [];
156
+ forEachDiagnostic(view.state, (diagnostic, diagnosticFrom, diagnosticTo) => {
157
+ if (diagnosticFrom < to && diagnosticTo > from) {
158
+ return;
159
+ }
160
+ kept.push({ ...diagnostic, from: diagnosticFrom, to: diagnosticTo });
161
+ });
162
+ const changeSet = ChangeSet.of({ from, to, insert }, view.state.doc.length);
163
+ const next = kept.map((d) => ({
164
+ ...d,
165
+ from: changeSet.mapPos(d.from, 1),
166
+ to: changeSet.mapPos(d.to, -1),
167
+ }));
168
+ view.dispatch({
169
+ changes: { from, to, insert },
170
+ ...setDiagnostics(view.state, next),
171
+ });
172
+ };
173
+
174
+ const assistantLinter = ({
175
+ generate,
176
+ instructions = DEFAULT_INSTRUCTIONS,
177
+ autoPanel = true,
178
+ delay = 2_000,
179
+ }: AssistantOptions) =>
180
+ linter(
181
+ async (view) => {
182
+ try {
183
+ const content = view.state.doc.toString();
184
+ const result = await generate({ instructions, content });
185
+
186
+ const [match] = result.match(/\[.*\]/s) ?? [];
187
+ const parsed = match ? safeParseJson<unknown[]>(match, []) : [];
188
+ const suggestions = Array.isArray(parsed) ? parsed.filter(isSuggestion) : [];
189
+ if (suggestions && suggestions.length > 0) {
190
+ const diagnostics: Diagnostic[] = [];
191
+ for (const suggestion of suggestions) {
192
+ const idx = findSuggestionIndex(content, suggestion);
193
+ if (idx !== -1) {
194
+ diagnostics.push({
195
+ from: idx,
196
+ to: idx + suggestion.original.length,
197
+ severity: 'info',
198
+ message: `Suggestion: ${suggestion.replacement}`,
199
+ actions: [
200
+ {
201
+ name: 'Apply',
202
+ apply: (view, from, to) => {
203
+ replaceTextAndDropLintAtRange(view, from, to, suggestion.replacement);
204
+ },
205
+ },
206
+ ],
207
+ });
208
+ }
209
+ }
210
+
211
+ return diagnostics;
212
+ }
213
+ } catch (err) {
214
+ log.catch(err);
215
+ }
216
+
217
+ return [];
218
+ },
219
+ {
220
+ delay,
221
+ autoPanel,
222
+ },
223
+ );
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './assistant-extension';
@@ -2,4 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './useBasicMarkdownExtensions';
5
6
  export * from './useTextEditor';
@@ -0,0 +1,55 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { useMemo } from 'react';
7
+
8
+ import { useThemeContext } from '@dxos/react-ui';
9
+ import {
10
+ createBasicExtensions,
11
+ createMarkdownExtensions,
12
+ createThemeExtensions,
13
+ decorateMarkdown,
14
+ } from '@dxos/ui-editor';
15
+
16
+ export type UseBasicMarkdownExtensionsOptions = {
17
+ /** Placeholder text shown when the editor is empty. */
18
+ placeholder?: string;
19
+ /** Enables markdown visual decorations. Pass `false` to disable. Defaults to `true`. */
20
+ decorate?: boolean;
21
+ /**
22
+ * Extra extensions appended to the returned array.
23
+ * Callers must memoize this array — it is used as a dependency of {@link useMemo}.
24
+ */
25
+ extensions?: Extension[];
26
+ };
27
+
28
+ /**
29
+ * Returns the standard CodeMirror extension stack for an inline markdown editor:
30
+ * basic editor behaviors, themed syntax highlighting (theme mode read from {@link useThemeContext}),
31
+ * markdown parsing, and visual decorations.
32
+ *
33
+ * Used by surfaces such as `AgentProperties` and `MagazineProperties` to render the
34
+ * `instructions` field of an object as a small editable markdown block.
35
+ *
36
+ * The memoization depends on primitive option values plus the `extensions` array reference,
37
+ * so callers passing fresh option literals each render still hit the cache.
38
+ */
39
+ export const useBasicMarkdownExtensions = ({
40
+ placeholder,
41
+ decorate = true,
42
+ extensions,
43
+ }: UseBasicMarkdownExtensionsOptions = {}): Extension[] => {
44
+ const { themeMode } = useThemeContext();
45
+ return useMemo(
46
+ () => [
47
+ createBasicExtensions({ placeholder }),
48
+ createThemeExtensions({ syntaxHighlighting: true, themeMode }),
49
+ createMarkdownExtensions(),
50
+ ...(decorate ? [decorateMarkdown()] : []),
51
+ ...(extensions ?? []),
52
+ ],
53
+ [placeholder, themeMode, decorate, extensions],
54
+ );
55
+ };
package/src/index.ts CHANGED
@@ -2,9 +2,6 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { translations } from './translations';
6
-
7
5
  export * from './components';
6
+ export * from './extensions';
8
7
  export * from './hooks';
9
-
10
- export { translations };
@@ -8,8 +8,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
8
  import React, { useCallback, useEffect, useState } from 'react';
9
9
 
10
10
  import { Obj, Ref } from '@dxos/echo';
11
- import { TestSchema } from '@dxos/echo/testing';
12
11
  import { DocAccessor, createDocAccessor } from '@dxos/echo-db';
12
+ import { TestSchema } from '@dxos/echo/testing';
13
13
  import { log } from '@dxos/log';
14
14
  import { type Messenger } from '@dxos/protocols';
15
15
  import { Query, useQuery, useSpace } from '@dxos/react-client/echo';
@@ -19,8 +19,9 @@ import { Button, useThemeContext } from '@dxos/react-ui';
19
19
  import { withLayout, withTheme, Loading } from '@dxos/react-ui/testing';
20
20
  import { createBasicExtensions, createDataExtensions, createThemeExtensions } from '@dxos/ui-editor';
21
21
 
22
+ import { translations } from '#translations';
23
+
22
24
  import { useTextEditor } from '../hooks';
23
- import { translations } from '../translations';
24
25
 
25
26
  const initialContent = 'Hello world!';
26
27
 
@@ -43,7 +44,7 @@ const Editor = ({ source, messenger, identity, autoFocus }: EditorProps) => {
43
44
  initialValue: DocAccessor.getValue(source),
44
45
  extensions: [
45
46
  createBasicExtensions({ placeholder: 'Type here...', search: true }),
46
- createThemeExtensions({ themeMode, slots: { scroll: { className: 'p-2' } } }),
47
+ createThemeExtensions({ themeMode, slots: { scroller: { className: 'p-2' } } }),
47
48
  createDataExtensions({ id: 'test', text: source, messenger, identity }),
48
49
  ],
49
50
  }),
@@ -11,10 +11,10 @@ import { PublicKey } from '@dxos/keys';
11
11
  import { log } from '@dxos/log';
12
12
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
13
  import { withRegistry } from '@dxos/storybook-utils';
14
- import { type Comment, annotations, comments, createExternalCommentSync } from '@dxos/ui-editor';
14
+ import { annotations, comments, createExternalCommentSync } from '@dxos/ui-editor';
15
+ import { type Comment } from '@dxos/ui-editor/types';
15
16
 
16
17
  import { createRenderer, str } from '../util';
17
-
18
18
  import { EditorStory, content, longText } from './components';
19
19
 
20
20
  const meta = {