@liminis/editor 0.1.0-rc.1

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 (219) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +376 -0
  3. package/dist/annotations/anchor-align.d.ts +50 -0
  4. package/dist/annotations/anchor-align.js +74 -0
  5. package/dist/annotations/anchor-model.d.ts +81 -0
  6. package/dist/annotations/anchor-model.js +80 -0
  7. package/dist/annotations/anchor-resolver.d.ts +92 -0
  8. package/dist/annotations/anchor-resolver.js +231 -0
  9. package/dist/annotations/block-structure.d.ts +65 -0
  10. package/dist/annotations/block-structure.js +113 -0
  11. package/dist/annotations/types.d.ts +159 -0
  12. package/dist/annotations/types.js +50 -0
  13. package/dist/annotations.d.ts +25 -0
  14. package/dist/annotations.js +27 -0
  15. package/dist/app/App.d.ts +64 -0
  16. package/dist/app/App.js +180 -0
  17. package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
  18. package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
  19. package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
  20. package/dist/app/editor/AnchorScrollPlugin.js +134 -0
  21. package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
  22. package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
  23. package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
  24. package/dist/app/editor/AnnotationPlugin.js +135 -0
  25. package/dist/app/editor/AnnotationSurface.d.ts +34 -0
  26. package/dist/app/editor/AnnotationSurface.js +115 -0
  27. package/dist/app/editor/AssetContext.d.ts +20 -0
  28. package/dist/app/editor/AssetContext.js +81 -0
  29. package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
  30. package/dist/app/editor/BlockClickPlugin.js +126 -0
  31. package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
  32. package/dist/app/editor/CodeBlockPlugin.js +219 -0
  33. package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
  34. package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
  35. package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
  36. package/dist/app/editor/DragHandlePlugin.js +300 -0
  37. package/dist/app/editor/Editor.d.ts +84 -0
  38. package/dist/app/editor/Editor.js +539 -0
  39. package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
  40. package/dist/app/editor/FrontmatterPlugin.js +301 -0
  41. package/dist/app/editor/ImageModal.d.ts +13 -0
  42. package/dist/app/editor/ImageModal.js +267 -0
  43. package/dist/app/editor/ImagePlugin.d.ts +3 -0
  44. package/dist/app/editor/ImagePlugin.js +187 -0
  45. package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
  46. package/dist/app/editor/LinkClickPlugin.js +90 -0
  47. package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
  48. package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
  49. package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
  50. package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
  51. package/dist/app/editor/SearchPlugin.d.ts +1 -0
  52. package/dist/app/editor/SearchPlugin.js +379 -0
  53. package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
  54. package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
  55. package/dist/app/editor/SlashMenu.d.ts +11 -0
  56. package/dist/app/editor/SlashMenu.js +494 -0
  57. package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
  58. package/dist/app/editor/SlashMenuPlugin.js +168 -0
  59. package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
  60. package/dist/app/editor/TableActionsPlugin.js +398 -0
  61. package/dist/app/editor/TogglePlugin.d.ts +1 -0
  62. package/dist/app/editor/TogglePlugin.js +159 -0
  63. package/dist/app/editor/Toolbar.d.ts +10 -0
  64. package/dist/app/editor/Toolbar.js +263 -0
  65. package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
  66. package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
  67. package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
  68. package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
  69. package/dist/app/editor/annotation-marks.d.ts +177 -0
  70. package/dist/app/editor/annotation-marks.js +750 -0
  71. package/dist/app/editor/annotationCommands.d.ts +13 -0
  72. package/dist/app/editor/annotationCommands.js +11 -0
  73. package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
  74. package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
  75. package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
  76. package/dist/app/editor/c4/edge-clipping.js +139 -0
  77. package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
  78. package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
  79. package/dist/app/editor/c4/layout.d.ts +25 -0
  80. package/dist/app/editor/c4/layout.js +839 -0
  81. package/dist/app/editor/c4/parser.d.ts +19 -0
  82. package/dist/app/editor/c4/parser.js +410 -0
  83. package/dist/app/editor/c4/render-to-string.d.ts +24 -0
  84. package/dist/app/editor/c4/render-to-string.js +34 -0
  85. package/dist/app/editor/c4/renderer.d.ts +64 -0
  86. package/dist/app/editor/c4/renderer.js +569 -0
  87. package/dist/app/editor/c4/types.d.ts +203 -0
  88. package/dist/app/editor/c4/types.js +43 -0
  89. package/dist/app/editor/correction-yaml.d.ts +10 -0
  90. package/dist/app/editor/correction-yaml.js +40 -0
  91. package/dist/app/editor/editorNodes.d.ts +9 -0
  92. package/dist/app/editor/editorNodes.js +36 -0
  93. package/dist/app/editor/index.d.ts +1 -0
  94. package/dist/app/editor/index.js +1 -0
  95. package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
  96. package/dist/app/editor/mark-overlap-resolver.js +45 -0
  97. package/dist/app/editor/nodes/C4Component.d.ts +14 -0
  98. package/dist/app/editor/nodes/C4Component.js +284 -0
  99. package/dist/app/editor/nodes/C4Node.d.ts +32 -0
  100. package/dist/app/editor/nodes/C4Node.js +112 -0
  101. package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
  102. package/dist/app/editor/nodes/CalloutNode.js +94 -0
  103. package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
  104. package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
  105. package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
  106. package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
  107. package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
  108. package/dist/app/editor/nodes/CustomListNode.js +54 -0
  109. package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
  110. package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
  111. package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
  112. package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
  113. package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
  114. package/dist/app/editor/nodes/EquationComponent.js +130 -0
  115. package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
  116. package/dist/app/editor/nodes/EquationNode.js +179 -0
  117. package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
  118. package/dist/app/editor/nodes/FootnoteNode.js +151 -0
  119. package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
  120. package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
  121. package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
  122. package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
  123. package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
  124. package/dist/app/editor/nodes/HtmlNode.js +121 -0
  125. package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
  126. package/dist/app/editor/nodes/ImageComponent.js +228 -0
  127. package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
  128. package/dist/app/editor/nodes/ImageNode.js +183 -0
  129. package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
  130. package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
  131. package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
  132. package/dist/app/editor/nodes/MermaidComponent.js +216 -0
  133. package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
  134. package/dist/app/editor/nodes/MermaidNode.js +96 -0
  135. package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
  136. package/dist/app/editor/nodes/ToggleNode.js +237 -0
  137. package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
  138. package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
  139. package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
  140. package/dist/app/editor/nodes/diagram-utils.js +29 -0
  141. package/dist/app/editor/nodes/index.d.ts +30 -0
  142. package/dist/app/editor/nodes/index.js +17 -0
  143. package/dist/app/editor/utils.d.ts +2 -0
  144. package/dist/app/editor/utils.js +17 -0
  145. package/dist/app/mapper/index.d.ts +3 -0
  146. package/dist/app/mapper/index.js +2 -0
  147. package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
  148. package/dist/app/mapper/lexicalToMdast.js +1937 -0
  149. package/dist/app/mapper/mdastToLexical.d.ts +31 -0
  150. package/dist/app/mapper/mdastToLexical.js +1075 -0
  151. package/dist/contract.d.ts +8 -0
  152. package/dist/contract.js +8 -0
  153. package/dist/headless.d.ts +40 -0
  154. package/dist/headless.js +45 -0
  155. package/dist/host/context.d.ts +20 -0
  156. package/dist/host/context.js +24 -0
  157. package/dist/host/defaults.d.ts +21 -0
  158. package/dist/host/defaults.js +47 -0
  159. package/dist/host/messages.d.ts +26 -0
  160. package/dist/host/messages.js +47 -0
  161. package/dist/host/types.d.ts +75 -0
  162. package/dist/host/types.js +13 -0
  163. package/dist/index.d.ts +35 -0
  164. package/dist/index.js +51 -0
  165. package/dist/markdown/annotate-sentinels.d.ts +42 -0
  166. package/dist/markdown/annotate-sentinels.js +86 -0
  167. package/dist/markdown/parse.d.ts +56 -0
  168. package/dist/markdown/parse.js +516 -0
  169. package/dist/markdown/stringify.d.ts +7 -0
  170. package/dist/markdown/stringify.js +439 -0
  171. package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
  172. package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
  173. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
  174. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
  175. package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
  176. package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
  177. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
  178. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
  179. package/dist/markdown.d.ts +28 -0
  180. package/dist/markdown.js +44 -0
  181. package/dist/mathjax-config.d.ts +89 -0
  182. package/dist/mathjax-config.js +133 -0
  183. package/dist/nodes.d.ts +34 -0
  184. package/dist/nodes.js +34 -0
  185. package/dist/stores/correctionStore.d.ts +15 -0
  186. package/dist/stores/correctionStore.js +12 -0
  187. package/dist/stores/editorStore.d.ts +16 -0
  188. package/dist/stores/editorStore.js +27 -0
  189. package/dist/styles.css +2477 -0
  190. package/dist/types.d.ts +296 -0
  191. package/dist/types.js +91 -0
  192. package/dist/utils/file-types.d.ts +10 -0
  193. package/dist/utils/file-types.js +46 -0
  194. package/docs/annotations.md +134 -0
  195. package/docs/decisions/README.md +72 -0
  196. package/docs/decisions/adr-002.md +27 -0
  197. package/docs/decisions/adr-007.md +27 -0
  198. package/docs/decisions/adr-010.md +26 -0
  199. package/docs/decisions/adr-024.md +124 -0
  200. package/docs/decisions/adr-024b.md +0 -0
  201. package/docs/decisions/adr-025.md +103 -0
  202. package/docs/decisions/adr-027.md +66 -0
  203. package/docs/decisions/adr-057.md +67 -0
  204. package/docs/decisions/adr-070.md +131 -0
  205. package/docs/decisions/adr-075.md +410 -0
  206. package/docs/decisions/adr-076.md +183 -0
  207. package/docs/decisions/adr-077.md +410 -0
  208. package/docs/decisions/adr-078.md +243 -0
  209. package/docs/decisions/adr-079.md +167 -0
  210. package/docs/decisions/adr-080.md +325 -0
  211. package/docs/decisions/adr-081.md +108 -0
  212. package/docs/decisions/adr-082.md +123 -0
  213. package/docs/decisions/adr-083.md +204 -0
  214. package/docs/decisions/adr-084.md +179 -0
  215. package/docs/editor-api.md +106 -0
  216. package/docs/markdown-pipeline.md +282 -0
  217. package/docs/provenance.md +106 -0
  218. package/docs/zusammen-editor-capability-map.md +232 -0
  219. package/package.json +175 -0
@@ -0,0 +1,219 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState, useRef } from 'react';
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import { $getRoot, $getNodeByKey, $isElementNode } from 'lexical';
5
+ import { $isCodeNode } from '@lexical/code';
6
+ import { $isFrontmatterNode } from './nodes/index.js';
7
+ // Shared helper to recursively find all code nodes in a tree
8
+ // Excludes FrontmatterNode since it shouldn't show the language selector
9
+ function findCodeNodesInTree(node) {
10
+ const codeNodes = [];
11
+ if ($isCodeNode(node) && !$isFrontmatterNode(node)) {
12
+ codeNodes.push(node);
13
+ }
14
+ if ($isElementNode(node)) {
15
+ for (const child of node.getChildren()) {
16
+ codeNodes.push(...findCodeNodesInTree(child));
17
+ }
18
+ }
19
+ return codeNodes;
20
+ }
21
+ // Popular languages for the dropdown
22
+ // Note: 'plain' is a valid Prism language that provides no syntax highlighting
23
+ const LANGUAGES = [
24
+ { value: 'plain', label: 'Plain Text' },
25
+ { value: 'javascript', label: 'JavaScript' },
26
+ { value: 'typescript', label: 'TypeScript' },
27
+ { value: 'jsx', label: 'JSX' },
28
+ { value: 'tsx', label: 'TSX' },
29
+ { value: 'python', label: 'Python' },
30
+ { value: 'java', label: 'Java' },
31
+ { value: 'c', label: 'C' },
32
+ { value: 'cpp', label: 'C++' },
33
+ { value: 'csharp', label: 'C#' },
34
+ { value: 'go', label: 'Go' },
35
+ { value: 'rust', label: 'Rust' },
36
+ { value: 'ruby', label: 'Ruby' },
37
+ { value: 'php', label: 'PHP' },
38
+ { value: 'swift', label: 'Swift' },
39
+ { value: 'kotlin', label: 'Kotlin' },
40
+ { value: 'markup', label: 'HTML' },
41
+ { value: 'css', label: 'CSS' },
42
+ { value: 'scss', label: 'SCSS' },
43
+ { value: 'json', label: 'JSON' },
44
+ { value: 'yaml', label: 'YAML' },
45
+ { value: 'markdown', label: 'Markdown' },
46
+ { value: 'sql', label: 'SQL' },
47
+ { value: 'bash', label: 'Bash' },
48
+ { value: 'powershell', label: 'PowerShell' },
49
+ { value: 'docker', label: 'Dockerfile' },
50
+ { value: 'graphql', label: 'GraphQL' },
51
+ ];
52
+ function LanguageSelector({ nodeKey, currentLanguage, position, onClose }) {
53
+ const [editor] = useLexicalComposerContext();
54
+ const [searchQuery, setSearchQuery] = useState('');
55
+ const menuRef = useRef(null);
56
+ const inputRef = useRef(null);
57
+ const filteredLanguages = LANGUAGES.filter((lang) => lang.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
58
+ lang.value.toLowerCase().includes(searchQuery.toLowerCase()));
59
+ // Focus input on mount
60
+ useEffect(() => {
61
+ inputRef.current?.focus();
62
+ }, []);
63
+ // Close on click outside
64
+ useEffect(() => {
65
+ const handleClickOutside = (e) => {
66
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
67
+ onClose();
68
+ }
69
+ };
70
+ document.addEventListener('mousedown', handleClickOutside);
71
+ return () => document.removeEventListener('mousedown', handleClickOutside);
72
+ }, [onClose]);
73
+ const selectLanguage = useCallback((language) => {
74
+ // Set the language and wait for the update to complete before closing
75
+ editor.update(() => {
76
+ const node = $getNodeByKey(nodeKey);
77
+ if (node && $isCodeNode(node)) {
78
+ // Use 'plain' for Plain Text - it's a valid Prism language with no highlighting
79
+ // This avoids Lexical's transform that resets undefined/null languages to 'javascript'
80
+ node.setLanguage(language);
81
+ }
82
+ }, {
83
+ onUpdate: () => {
84
+ // Close the selector after the update is committed
85
+ onClose();
86
+ },
87
+ });
88
+ }, [editor, nodeKey, onClose]);
89
+ const handleKeyDown = useCallback((e) => {
90
+ if (e.key === 'Escape') {
91
+ e.preventDefault();
92
+ onClose();
93
+ }
94
+ }, [onClose]);
95
+ const handleItemClick = useCallback((e, language) => {
96
+ e.preventDefault();
97
+ e.stopPropagation();
98
+ selectLanguage(language);
99
+ }, [selectLanguage]);
100
+ // Normalize for comparison
101
+ const normalizedCurrent = currentLanguage || 'plain';
102
+ return (_jsxs("div", { ref: menuRef, className: "code-language-menu", style: {
103
+ position: 'fixed',
104
+ top: position.top,
105
+ right: position.right,
106
+ }, children: [_jsx("input", { ref: inputRef, type: "text", className: "code-language-search", placeholder: "Search languages...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyDown: handleKeyDown }), _jsx("div", { className: "code-language-list", children: filteredLanguages.length === 0 ? (_jsx("div", { className: "code-language-empty", children: "No languages found" })) : (filteredLanguages.map((lang) => {
107
+ const isCurrentLang = lang.value === normalizedCurrent;
108
+ return (_jsxs("div", { className: `code-language-item ${isCurrentLang ? 'current' : ''}`, onMouseDown: (e) => handleItemClick(e, lang.value), children: [lang.label, isCurrentLang && (_jsx("span", { className: "code-language-check", children: "\u2713" }))] }, lang.value || 'plain-text'));
109
+ })) })] }));
110
+ }
111
+ export function CodeBlockPlugin() {
112
+ const [editor] = useLexicalComposerContext();
113
+ const [codeBlocks, setCodeBlocks] = useState([]);
114
+ const [selectorState, setSelectorState] = useState(null);
115
+ const updateScheduledRef = useRef(false);
116
+ // Track code blocks and their positions
117
+ useEffect(() => {
118
+ // Batched update function that reads all DOM positions in a single frame
119
+ const updateCodeBlocks = () => {
120
+ updateScheduledRef.current = false;
121
+ const editorElement = editor.getRootElement();
122
+ if (!editorElement)
123
+ return;
124
+ editor.getEditorState().read(() => {
125
+ const root = $getRoot();
126
+ const codeNodes = findCodeNodesInTree(root);
127
+ // Collect node keys, languages, and text content first (no DOM reads)
128
+ const nodeData = [];
129
+ for (const node of codeNodes) {
130
+ nodeData.push({
131
+ nodeKey: node.getKey(),
132
+ language: node.getLanguage() || '',
133
+ textContent: node.getTextContent(),
134
+ });
135
+ }
136
+ // Batch DOM reads in a single pass after the read()
137
+ requestAnimationFrame(() => {
138
+ const blocks = [];
139
+ for (const { nodeKey, language, textContent } of nodeData) {
140
+ const element = editor.getElementByKey(nodeKey);
141
+ if (element) {
142
+ blocks.push({
143
+ nodeKey,
144
+ language,
145
+ textContent,
146
+ rect: element.getBoundingClientRect(),
147
+ });
148
+ }
149
+ }
150
+ setCodeBlocks(blocks);
151
+ });
152
+ });
153
+ };
154
+ // Debounced update scheduler
155
+ const scheduleUpdate = () => {
156
+ if (!updateScheduledRef.current) {
157
+ updateScheduledRef.current = true;
158
+ requestAnimationFrame(updateCodeBlocks);
159
+ }
160
+ };
161
+ const timeoutId = setTimeout(updateCodeBlocks, 200);
162
+ const unsubscribe = editor.registerUpdateListener(() => {
163
+ scheduleUpdate();
164
+ });
165
+ // Also update on scroll/resize with debouncing
166
+ const handleScrollResize = () => {
167
+ scheduleUpdate();
168
+ };
169
+ window.addEventListener('scroll', handleScrollResize, true);
170
+ window.addEventListener('resize', handleScrollResize);
171
+ return () => {
172
+ clearTimeout(timeoutId);
173
+ unsubscribe();
174
+ window.removeEventListener('scroll', handleScrollResize, true);
175
+ window.removeEventListener('resize', handleScrollResize);
176
+ };
177
+ }, [editor]);
178
+ const handleOpenSelector = useCallback((nodeKey, language, buttonRect) => {
179
+ setSelectorState({
180
+ nodeKey,
181
+ language,
182
+ position: {
183
+ top: buttonRect.bottom + 4,
184
+ right: window.innerWidth - buttonRect.right,
185
+ },
186
+ });
187
+ }, []);
188
+ const handleCloseSelector = useCallback(() => {
189
+ setSelectorState(null);
190
+ }, []);
191
+ // Track which code blocks show "Copied" feedback
192
+ const [copiedNodeKey, setCopiedNodeKey] = useState(null);
193
+ const handleCopyCode = useCallback((nodeKey, textContent) => {
194
+ void navigator.clipboard.writeText(textContent).then(() => {
195
+ setCopiedNodeKey(nodeKey);
196
+ setTimeout(() => setCopiedNodeKey(null), 1500);
197
+ });
198
+ }, []);
199
+ return (_jsxs(_Fragment, { children: [codeBlocks.map((block) => {
200
+ // Normalize empty/null/undefined to 'plain' for comparison
201
+ const normalizedLang = block.language || 'plain';
202
+ const langLabel = LANGUAGES.find((l) => l.value === normalizedLang)?.label || normalizedLang;
203
+ const isCopied = copiedNodeKey === block.nodeKey;
204
+ return (_jsxs("div", { className: "code-block-toolbar", style: {
205
+ position: 'fixed',
206
+ top: block.rect.top + 6,
207
+ right: window.innerWidth - block.rect.right + 8,
208
+ }, children: [_jsx("button", { className: "code-language-button", onClick: (e) => {
209
+ e.preventDefault();
210
+ e.stopPropagation();
211
+ const rect = e.target.getBoundingClientRect();
212
+ handleOpenSelector(block.nodeKey, block.language, rect);
213
+ }, children: langLabel }), _jsx("button", { className: `code-copy-button ${isCopied ? 'copied' : ''}`, onClick: (e) => {
214
+ e.preventDefault();
215
+ e.stopPropagation();
216
+ handleCopyCode(block.nodeKey, block.textContent);
217
+ }, title: "Copy code", children: isCopied ? (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) })) : (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] })) })] }, block.nodeKey));
218
+ }), selectorState && (_jsx(LanguageSelector, { nodeKey: selectorState.nodeKey, currentLanguage: selectorState.language, position: selectorState.position, onClose: handleCloseSelector }))] }));
219
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CorrectionPanelPlugin — floating correction panel inside the Lexical editor.
3
+ *
4
+ * Opens when useCorrectionStore.isOpen is true. Loads suggestions from three
5
+ * sources progressively: the host's corrections store (immediate), then entity
6
+ * and passage suggestions from the host's knowledge services (async).
7
+ *
8
+ * On confirm, merges a same_as entry into the corrections document and asks the
9
+ * host to apply it. All persistence and knowledge-graph access crosses the
10
+ * package seam through `CorrectionHostServices` (see ADR-075); when the host
11
+ * supplies no correction services the panel still opens, shows no suggestions,
12
+ * and confirming performs only the in-document replacement.
13
+ */
14
+ export declare function CorrectionPanelPlugin(): JSX.Element | null;
@@ -0,0 +1,333 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * CorrectionPanelPlugin — floating correction panel inside the Lexical editor.
4
+ *
5
+ * Opens when useCorrectionStore.isOpen is true. Loads suggestions from three
6
+ * sources progressively: the host's corrections store (immediate), then entity
7
+ * and passage suggestions from the host's knowledge services (async).
8
+ *
9
+ * On confirm, merges a same_as entry into the corrections document and asks the
10
+ * host to apply it. All persistence and knowledge-graph access crosses the
11
+ * package seam through `CorrectionHostServices` (see ADR-075); when the host
12
+ * supplies no correction services the panel still opens, shows no suggestions,
13
+ * and confirming performs only the in-document replacement.
14
+ */
15
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
17
+ import { $getRoot, $isTextNode } from 'lexical';
18
+ import { useCorrectionStore } from '../../stores/correctionStore.js';
19
+ import { useEditorHost } from '../../host/context.js';
20
+ import { parseCorrectionsYaml, serializeCorrectionsYaml, mergeCorrection, isExistingCanonical, } from './correction-yaml.js';
21
+ // --- Style helpers ---
22
+ const isDark = () => typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
23
+ const panelStyle = () => {
24
+ const dark = isDark();
25
+ return {
26
+ position: 'fixed',
27
+ zIndex: 10001,
28
+ borderRadius: '8px',
29
+ padding: '12px',
30
+ minWidth: '280px',
31
+ maxWidth: '360px',
32
+ background: `var(--vscode-menu-background, ${dark ? '#252526' : '#ffffff'})`,
33
+ border: `1px solid var(--vscode-menu-border, ${dark ? '#454545' : '#d4d4d4'})`,
34
+ boxShadow: dark ? '0 4px 16px rgba(0,0,0,0.4)' : '0 4px 16px rgba(0,0,0,0.15)',
35
+ color: `var(--vscode-menu-foreground, ${dark ? '#cccccc' : '#333333'})`,
36
+ fontFamily: 'system-ui, -apple-system, sans-serif',
37
+ fontSize: '13px',
38
+ };
39
+ };
40
+ // --- Main component ---
41
+ export function CorrectionPanelPlugin() {
42
+ const [editor] = useLexicalComposerContext();
43
+ const { corrections, notifyError } = useEditorHost();
44
+ const { isOpen, position, selectedText } = useCorrectionStore();
45
+ const panelRef = useRef(null);
46
+ const yamlEntriesRef = useRef([]);
47
+ const [yamlSuggestions, setYamlSuggestions] = useState([]);
48
+ const [entitySuggestions, setEntitySuggestions] = useState([]);
49
+ const [passageSuggestions, setPassageSuggestions] = useState([]);
50
+ const [loadingEntities, setLoadingEntities] = useState(false);
51
+ const [loadingPassages, setLoadingPassages] = useState(false);
52
+ const [canonicalInput, setCanonicalInput] = useState('');
53
+ const [replaceAll, setReplaceAll] = useState(false);
54
+ const [confirmPhase, setConfirmPhase] = useState('idle');
55
+ const [isSubmitting, setIsSubmitting] = useState(false);
56
+ const close = useCallback(() => {
57
+ useCorrectionStore.getState().close();
58
+ }, []);
59
+ // Reset state when panel opens
60
+ useEffect(() => {
61
+ if (!isOpen)
62
+ return;
63
+ setYamlSuggestions([]);
64
+ setEntitySuggestions([]);
65
+ setPassageSuggestions([]);
66
+ setCanonicalInput('');
67
+ setReplaceAll(false);
68
+ setConfirmPhase('idle');
69
+ setIsSubmitting(false);
70
+ yamlEntriesRef.current = [];
71
+ }, [isOpen]);
72
+ // Outside-click and Escape dismissal
73
+ useEffect(() => {
74
+ if (!isOpen)
75
+ return;
76
+ const handleMousedown = (e) => {
77
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
78
+ close();
79
+ }
80
+ };
81
+ const handleKey = (e) => {
82
+ if (e.key === 'Escape')
83
+ close();
84
+ };
85
+ document.addEventListener('mousedown', handleMousedown);
86
+ document.addEventListener('keydown', handleKey);
87
+ return () => {
88
+ document.removeEventListener('mousedown', handleMousedown);
89
+ document.removeEventListener('keydown', handleKey);
90
+ };
91
+ }, [isOpen, close]);
92
+ // Load suggestions when panel opens
93
+ useEffect(() => {
94
+ if (!isOpen || !selectedText || !corrections)
95
+ return;
96
+ let cancelled = false;
97
+ // Load YAML suggestions synchronously after file read
98
+ const loadYaml = async () => {
99
+ try {
100
+ const raw = await corrections.readCorrections();
101
+ if (cancelled)
102
+ return;
103
+ const entries = parseCorrectionsYaml(raw);
104
+ yamlEntriesRef.current = entries;
105
+ const matches = entries
106
+ .filter((e) => e.type === 'same_as' && e.aliases.includes(selectedText))
107
+ .map((e) => e.canonical);
108
+ setYamlSuggestions(matches);
109
+ }
110
+ catch {
111
+ // File may not exist yet — that's fine
112
+ yamlEntriesRef.current = [];
113
+ }
114
+ };
115
+ // Load entity suggestions via MCP
116
+ const loadEntities = async () => {
117
+ setLoadingEntities(true);
118
+ try {
119
+ const names = await corrections.suggestEntities(selectedText, 5);
120
+ if (cancelled)
121
+ return;
122
+ setEntitySuggestions(names);
123
+ }
124
+ catch {
125
+ // Knowledge graph unavailable — silent, not required for panel function
126
+ }
127
+ finally {
128
+ if (!cancelled)
129
+ setLoadingEntities(false);
130
+ }
131
+ };
132
+ // Load passage suggestions via MCP
133
+ const loadPassages = async () => {
134
+ setLoadingPassages(true);
135
+ try {
136
+ const names = await corrections.suggestPassages(selectedText, 5, 0.3);
137
+ if (cancelled)
138
+ return;
139
+ setPassageSuggestions(names);
140
+ }
141
+ catch {
142
+ // Knowledge graph unavailable — silent
143
+ }
144
+ finally {
145
+ if (!cancelled)
146
+ setLoadingPassages(false);
147
+ }
148
+ };
149
+ // Hosts back these services with an MCP stdio transport, which multiplexes
150
+ // nothing: a second concurrent call to the same server is rejected rather
151
+ // than queued. So serialize the two knowledge suggestion calls, while
152
+ // keeping the corrections read concurrent (it is a different service).
153
+ // The host relies on this ordering — do not parallelise.
154
+ const loadKnowledgeSuggestions = async () => {
155
+ await loadEntities();
156
+ if (cancelled)
157
+ return;
158
+ await loadPassages();
159
+ };
160
+ void loadYaml();
161
+ void loadKnowledgeSuggestions();
162
+ return () => {
163
+ cancelled = true;
164
+ };
165
+ }, [isOpen, selectedText, corrections]);
166
+ // Deduplicate suggestions across sources (case-insensitive), preserving order
167
+ const allSuggestions = useMemo(() => {
168
+ const seen = new Set();
169
+ const out = [];
170
+ for (const name of [...yamlSuggestions, ...entitySuggestions, ...passageSuggestions]) {
171
+ const key = name.toLowerCase();
172
+ if (!seen.has(key)) {
173
+ seen.add(key);
174
+ out.push(name);
175
+ }
176
+ }
177
+ return out;
178
+ }, [yamlSuggestions, entitySuggestions, passageSuggestions]);
179
+ const handleConfirm = useCallback(async () => {
180
+ const canonical = canonicalInput.trim();
181
+ if (!canonical)
182
+ return;
183
+ // R13: warn if selectedText is an existing canonical name
184
+ if (confirmPhase === 'idle' && isExistingCanonical(yamlEntriesRef.current, selectedText)) {
185
+ setConfirmPhase('needs-canonical-warning');
186
+ return;
187
+ }
188
+ setIsSubmitting(true);
189
+ try {
190
+ // (a) Replace all occurrences in the document if requested
191
+ if (replaceAll) {
192
+ editor.update(() => {
193
+ // Escape selectedText for use in a regex, then add word boundaries so
194
+ // "Tito" matches "Tito" and "Tito's" but not "Otito" or "titanium".
195
+ const escaped = selectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
196
+ const regex = new RegExp('\\b' + escaped + '\\b', 'g');
197
+ const walk = (node) => {
198
+ if ($isTextNode(node)) {
199
+ // Skip text inside code blocks — don't replace identifiers
200
+ const parentType = node.getParent()?.getType() ?? '';
201
+ if (parentType === 'code' || parentType === 'code-highlight')
202
+ return;
203
+ const content = node.getTextContent();
204
+ const replaced = content.replace(regex, canonical);
205
+ if (replaced !== content)
206
+ node.setTextContent(replaced);
207
+ return;
208
+ }
209
+ if ('getChildren' in node && typeof node.getChildren === 'function') {
210
+ for (const child of node.getChildren()) {
211
+ walk(child);
212
+ }
213
+ }
214
+ };
215
+ walk($getRoot());
216
+ });
217
+ }
218
+ // (b) No host persistence available — the in-document replacement above is
219
+ // all this package can do on its own.
220
+ if (!corrections) {
221
+ setIsSubmitting(false);
222
+ close();
223
+ return;
224
+ }
225
+ // (c) Re-read, merge, and write atomically — avoids overwriting corrections
226
+ // added by other sources while the panel was open. The host owns mkdir and
227
+ // atomic-write semantics.
228
+ let latestEntries = yamlEntriesRef.current;
229
+ try {
230
+ const fresh = await corrections.readCorrections();
231
+ latestEntries = parseCorrectionsYaml(fresh);
232
+ }
233
+ catch {
234
+ // File missing or unreadable — start from empty (yamlEntriesRef is still the best fallback)
235
+ }
236
+ const updated = mergeCorrection(latestEntries, selectedText, canonical);
237
+ const yaml = serializeCorrectionsYaml(updated);
238
+ await corrections.writeCorrections(yaml);
239
+ yamlEntriesRef.current = updated;
240
+ }
241
+ catch (err) {
242
+ notifyError('Failed to save correction', err instanceof Error ? err.message : String(err));
243
+ setIsSubmitting(false);
244
+ return;
245
+ }
246
+ // (d) Apply corrections — isolated so a KG failure never blocks panel close
247
+ try {
248
+ const applied = await corrections.applyCorrections();
249
+ if (!applied) {
250
+ notifyError('Knowledge graph sync failed', 'The correction was saved. It will be applied on the next ingestion pass.');
251
+ }
252
+ }
253
+ catch {
254
+ notifyError('Knowledge graph sync failed', 'The correction was saved. It will be applied on the next ingestion pass.');
255
+ }
256
+ // (f) Close panel — reached only after successful YAML write
257
+ setIsSubmitting(false);
258
+ close();
259
+ }, [canonicalInput, confirmPhase, replaceAll, selectedText, editor, close, corrections, notifyError]);
260
+ if (!isOpen)
261
+ return null;
262
+ const dark = isDark();
263
+ const borderColor = `var(--vscode-menu-border, ${dark ? '#454545' : '#d4d4d4'})`;
264
+ const hoverBg = `var(--vscode-menu-selectionBackground, ${dark ? '#094771' : '#e8e8e8'})`;
265
+ const mutedColor = dark ? '#888888' : '#999999';
266
+ const inputBg = dark ? '#1e1e1e' : '#f5f5f5';
267
+ const warningBg = dark ? '#4a3500' : '#fff8e1';
268
+ const warningBorder = dark ? '#8a6800' : '#f0c000';
269
+ const btnBase = {
270
+ padding: '4px 12px',
271
+ borderRadius: '4px',
272
+ fontSize: '12px',
273
+ cursor: 'pointer',
274
+ border: `1px solid ${borderColor}`,
275
+ background: 'transparent',
276
+ color: `var(--vscode-menu-foreground, ${dark ? '#cccccc' : '#333333'})`,
277
+ };
278
+ const btnPrimary = {
279
+ ...btnBase,
280
+ background: `var(--vscode-button-background, ${dark ? '#0e639c' : '#007acc'})`,
281
+ color: `var(--vscode-button-foreground, #ffffff)`,
282
+ border: 'none',
283
+ opacity: isSubmitting ? 0.6 : 1,
284
+ };
285
+ return (_jsxs("div", { ref: panelRef, style: {
286
+ ...panelStyle(),
287
+ left: Math.min(position.x, window.innerWidth - 380),
288
+ top: Math.min(position.y, window.innerHeight - 450),
289
+ }, children: [_jsxs("div", { style: { marginBottom: '8px' }, children: [_jsx("span", { style: { color: mutedColor, fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.05em' }, children: "Correcting" }), _jsx("div", { style: { fontWeight: 600, marginTop: '2px', wordBreak: 'break-word' }, children: selectedText })] }), _jsx("hr", { style: { margin: '8px 0', border: 'none', borderTop: `1px solid ${borderColor}` } }), (allSuggestions.length > 0 || loadingEntities || loadingPassages) && (_jsxs("div", { style: { marginBottom: '8px' }, children: [_jsx("div", { style: { color: mutedColor, fontSize: '11px', marginBottom: '4px' }, children: "Suggestions" }), yamlSuggestions.length > 0 && (_jsx("div", { children: yamlSuggestions.map((name) => (_jsx(SuggestionItem, { name: name, hoverBg: hoverBg, onClick: () => setCanonicalInput(name) }, `yaml-${name}`))) })), loadingEntities ? (_jsx("div", { style: { color: mutedColor, fontSize: '12px', padding: '4px 0' }, children: "Loading entity matches\u2026" })) : entitySuggestions.length > 0 ? (_jsx("div", { children: entitySuggestions
290
+ .filter((n) => !yamlSuggestions.some((y) => y.toLowerCase() === n.toLowerCase()))
291
+ .map((name) => (_jsx(SuggestionItem, { name: name, hoverBg: hoverBg, onClick: () => setCanonicalInput(name) }, `entity-${name}`))) })) : null, loadingPassages ? (_jsx("div", { style: { color: mutedColor, fontSize: '12px', padding: '4px 0' }, children: "Loading passage matches\u2026" })) : passageSuggestions.length > 0 ? (_jsx("div", { children: passageSuggestions
292
+ .filter((n) => !yamlSuggestions.some((y) => y.toLowerCase() === n.toLowerCase()) &&
293
+ !entitySuggestions.some((e) => e.toLowerCase() === n.toLowerCase()))
294
+ .map((name) => (_jsx(SuggestionItem, { name: name, hoverBg: hoverBg, onClick: () => setCanonicalInput(name) }, `passage-${name}`))) })) : null] })), _jsxs("div", { style: { marginBottom: '10px' }, children: [_jsx("label", { style: { display: 'block', color: mutedColor, fontSize: '11px', marginBottom: '4px' }, children: "Canonical name" }), _jsx("input", { type: "text", value: canonicalInput, onChange: (e) => setCanonicalInput(e.target.value), placeholder: "Enter correct name\u2026", style: {
295
+ width: '100%',
296
+ padding: '5px 8px',
297
+ borderRadius: '4px',
298
+ border: `1px solid ${borderColor}`,
299
+ background: inputBg,
300
+ color: `var(--vscode-menu-foreground, ${dark ? '#cccccc' : '#333333'})`,
301
+ fontSize: '13px',
302
+ boxSizing: 'border-box',
303
+ }, onKeyDown: (e) => {
304
+ if (e.key === 'Enter')
305
+ void handleConfirm();
306
+ } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '10px', cursor: 'pointer' }, children: [_jsx("input", { type: "checkbox", checked: replaceAll, onChange: (e) => setReplaceAll(e.target.checked) }), _jsx("span", { style: { fontSize: '12px' }, children: "Replace all occurrences in this document" })] }), confirmPhase === 'needs-canonical-warning' && (_jsx("div", { style: {
307
+ marginBottom: '10px',
308
+ padding: '8px',
309
+ borderRadius: '4px',
310
+ background: warningBg,
311
+ border: `1px solid ${warningBorder}`,
312
+ fontSize: '12px',
313
+ }, children: "This term is already a canonical name for other aliases. Proceeding will re-canonicalize it." })), _jsxs("div", { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' }, children: [_jsx("button", { style: btnBase, onClick: close, children: "Cancel" }), confirmPhase === 'needs-canonical-warning' ? (_jsx("button", { style: btnPrimary, onClick: handleConfirm, disabled: isSubmitting || !canonicalInput.trim(), children: "Proceed anyway" })) : (_jsx("button", { style: btnPrimary, onClick: handleConfirm, disabled: isSubmitting || !canonicalInput.trim(), children: isSubmitting ? 'Saving…' : 'Confirm' }))] })] }));
314
+ }
315
+ function SuggestionItem({ name, hoverBg, onClick, }) {
316
+ const dark = isDark();
317
+ return (_jsx("button", { onClick: onClick, style: {
318
+ display: 'block',
319
+ width: '100%',
320
+ padding: '4px 8px',
321
+ background: 'transparent',
322
+ border: 'none',
323
+ textAlign: 'left',
324
+ cursor: 'pointer',
325
+ fontSize: '13px',
326
+ borderRadius: '3px',
327
+ color: `var(--vscode-menu-foreground, ${dark ? '#cccccc' : '#333333'})`,
328
+ }, onMouseEnter: (e) => {
329
+ e.currentTarget.style.background = hoverBg;
330
+ }, onMouseLeave: (e) => {
331
+ e.currentTarget.style.background = 'transparent';
332
+ }, children: name }));
333
+ }
@@ -0,0 +1 @@
1
+ export declare function DragHandlePlugin(): import("react").JSX.Element | null;