@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,494 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import { $createParagraphNode, $createTextNode, $getSelection, $isRangeSelection, TextNode, } from 'lexical';
5
+ import { $createHeadingNode, $createQuoteNode } from '@lexical/rich-text';
6
+ import { $createCodeNode } from '@lexical/code';
7
+ import { $createHorizontalRuleNode, $createCalloutNode, $createToggleNode, $createEquationNode, $createMermaidNode, $createC4Node, $createCustomListNode, $createCustomListItemNode } from './nodes/index.js';
8
+ import { $createTableNode, $createTableRowNode, $createTableCellNode, TableCellHeaderStates, } from '@lexical/table';
9
+ import { INSERT_IMAGE_COMMAND } from './ImagePlugin.js';
10
+ // Block options defined outside component to avoid recreation
11
+ const BLOCK_OPTIONS = [
12
+ {
13
+ key: 'paragraph',
14
+ label: 'Paragraph',
15
+ description: 'Plain text',
16
+ icon: '¶',
17
+ keywords: ['text', 'plain', 'paragraph'],
18
+ onSelect: (editor) => {
19
+ editor.update(() => {
20
+ const selection = $getSelection();
21
+ if ($isRangeSelection(selection)) {
22
+ const node = $createParagraphNode();
23
+ selection.insertNodes([node]);
24
+ }
25
+ });
26
+ },
27
+ },
28
+ {
29
+ key: 'heading1',
30
+ label: 'Heading 1',
31
+ description: 'Large heading',
32
+ icon: 'H1',
33
+ keywords: ['h1', 'heading', 'title', 'large'],
34
+ onSelect: (editor) => {
35
+ editor.update(() => {
36
+ const selection = $getSelection();
37
+ if ($isRangeSelection(selection)) {
38
+ const node = $createHeadingNode('h1');
39
+ selection.insertNodes([node]);
40
+ }
41
+ });
42
+ },
43
+ },
44
+ {
45
+ key: 'heading2',
46
+ label: 'Heading 2',
47
+ description: 'Medium heading',
48
+ icon: 'H2',
49
+ keywords: ['h2', 'heading', 'subtitle', 'medium'],
50
+ onSelect: (editor) => {
51
+ editor.update(() => {
52
+ const selection = $getSelection();
53
+ if ($isRangeSelection(selection)) {
54
+ const node = $createHeadingNode('h2');
55
+ selection.insertNodes([node]);
56
+ }
57
+ });
58
+ },
59
+ },
60
+ {
61
+ key: 'heading3',
62
+ label: 'Heading 3',
63
+ description: 'Small heading',
64
+ icon: 'H3',
65
+ keywords: ['h3', 'heading', 'small'],
66
+ onSelect: (editor) => {
67
+ editor.update(() => {
68
+ const selection = $getSelection();
69
+ if ($isRangeSelection(selection)) {
70
+ const node = $createHeadingNode('h3');
71
+ selection.insertNodes([node]);
72
+ }
73
+ });
74
+ },
75
+ },
76
+ {
77
+ key: 'bulletList',
78
+ label: 'Bullet List',
79
+ description: 'Unordered list',
80
+ icon: '•',
81
+ keywords: ['bullet', 'list', 'unordered', 'ul'],
82
+ onSelect: (editor) => {
83
+ editor.update(() => {
84
+ const selection = $getSelection();
85
+ if ($isRangeSelection(selection)) {
86
+ const list = $createCustomListNode('bullet');
87
+ const item = $createCustomListItemNode();
88
+ list.append(item);
89
+ selection.insertNodes([list]);
90
+ }
91
+ });
92
+ },
93
+ },
94
+ {
95
+ key: 'numberedList',
96
+ label: 'Numbered List',
97
+ description: 'Ordered list',
98
+ icon: '1.',
99
+ keywords: ['numbered', 'list', 'ordered', 'ol'],
100
+ onSelect: (editor) => {
101
+ editor.update(() => {
102
+ const selection = $getSelection();
103
+ if ($isRangeSelection(selection)) {
104
+ const list = $createCustomListNode('number');
105
+ const item = $createCustomListItemNode();
106
+ list.append(item);
107
+ selection.insertNodes([list]);
108
+ }
109
+ });
110
+ },
111
+ },
112
+ {
113
+ key: 'todoList',
114
+ label: 'Todo List',
115
+ description: 'Task list with checkboxes',
116
+ icon: '☐',
117
+ keywords: ['todo', 'task', 'checkbox', 'check'],
118
+ onSelect: (editor) => {
119
+ editor.update(() => {
120
+ const selection = $getSelection();
121
+ if ($isRangeSelection(selection)) {
122
+ const list = $createCustomListNode('check');
123
+ // Explicit false (not the default undefined): once export reads
124
+ // __taskChecked instead of Lexical's parent-type-gated
125
+ // getChecked(), an implicit undefined would default to null (plain
126
+ // bullet) and silently drop the checkbox on export.
127
+ const item = $createCustomListItemNode(false);
128
+ list.append(item);
129
+ selection.insertNodes([list]);
130
+ }
131
+ });
132
+ },
133
+ },
134
+ {
135
+ key: 'quote',
136
+ label: 'Quote',
137
+ description: 'Blockquote',
138
+ icon: '"',
139
+ keywords: ['quote', 'blockquote', 'citation'],
140
+ onSelect: (editor) => {
141
+ editor.update(() => {
142
+ const selection = $getSelection();
143
+ if ($isRangeSelection(selection)) {
144
+ const node = $createQuoteNode();
145
+ selection.insertNodes([node]);
146
+ }
147
+ });
148
+ },
149
+ },
150
+ {
151
+ key: 'code',
152
+ label: 'Code Block',
153
+ description: 'Code with syntax highlighting',
154
+ icon: '</>',
155
+ keywords: ['code', 'snippet', 'programming'],
156
+ onSelect: (editor) => {
157
+ editor.update(() => {
158
+ const selection = $getSelection();
159
+ if ($isRangeSelection(selection)) {
160
+ const node = $createCodeNode();
161
+ selection.insertNodes([node]);
162
+ }
163
+ });
164
+ },
165
+ },
166
+ {
167
+ key: 'divider',
168
+ label: 'Divider',
169
+ description: 'Horizontal rule',
170
+ icon: '—',
171
+ keywords: ['divider', 'hr', 'line', 'horizontal'],
172
+ onSelect: (editor) => {
173
+ editor.update(() => {
174
+ const selection = $getSelection();
175
+ if ($isRangeSelection(selection)) {
176
+ const node = $createHorizontalRuleNode();
177
+ selection.insertNodes([node]);
178
+ }
179
+ });
180
+ },
181
+ },
182
+ {
183
+ key: 'image',
184
+ label: 'Image',
185
+ description: 'Upload or embed an image',
186
+ icon: '🖼',
187
+ keywords: ['image', 'picture', 'photo', 'img', 'upload'],
188
+ onSelect: (editor) => {
189
+ editor.dispatchCommand(INSERT_IMAGE_COMMAND, undefined);
190
+ },
191
+ },
192
+ {
193
+ key: 'callout-note',
194
+ label: 'Note',
195
+ description: 'Info callout',
196
+ icon: 'ℹ',
197
+ keywords: ['note', 'info', 'callout', 'admonition'],
198
+ onSelect: (editor) => {
199
+ editor.update(() => {
200
+ const selection = $getSelection();
201
+ if ($isRangeSelection(selection)) {
202
+ const node = $createCalloutNode('note', '');
203
+ selection.insertNodes([node]);
204
+ }
205
+ });
206
+ },
207
+ },
208
+ {
209
+ key: 'callout-tip',
210
+ label: 'Tip',
211
+ description: 'Helpful tip',
212
+ icon: '💡',
213
+ keywords: ['tip', 'hint', 'callout'],
214
+ onSelect: (editor) => {
215
+ editor.update(() => {
216
+ const selection = $getSelection();
217
+ if ($isRangeSelection(selection)) {
218
+ const node = $createCalloutNode('tip', '');
219
+ selection.insertNodes([node]);
220
+ }
221
+ });
222
+ },
223
+ },
224
+ {
225
+ key: 'callout-warning',
226
+ label: 'Warning',
227
+ description: 'Warning callout',
228
+ icon: '⚠',
229
+ keywords: ['warning', 'caution', 'alert', 'callout'],
230
+ onSelect: (editor) => {
231
+ editor.update(() => {
232
+ const selection = $getSelection();
233
+ if ($isRangeSelection(selection)) {
234
+ const node = $createCalloutNode('warning', '');
235
+ selection.insertNodes([node]);
236
+ }
237
+ });
238
+ },
239
+ },
240
+ {
241
+ key: 'toggle',
242
+ label: 'Toggle',
243
+ description: 'Collapsible section',
244
+ icon: '▶',
245
+ keywords: ['toggle', 'collapse', 'details', 'accordion'],
246
+ onSelect: (editor) => {
247
+ editor.update(() => {
248
+ const selection = $getSelection();
249
+ if ($isRangeSelection(selection)) {
250
+ const node = $createToggleNode('Toggle title', '', false);
251
+ selection.insertNodes([node]);
252
+ }
253
+ });
254
+ },
255
+ },
256
+ {
257
+ key: 'table',
258
+ label: 'Table',
259
+ description: 'Insert a table',
260
+ icon: '⊞',
261
+ keywords: ['table', 'grid', 'matrix', 'spreadsheet'],
262
+ onSelect: (editor) => {
263
+ editor.update(() => {
264
+ const selection = $getSelection();
265
+ if ($isRangeSelection(selection)) {
266
+ // Create a 3x3 table with header row
267
+ const table = $createTableNode();
268
+ // Header row
269
+ const headerRow = $createTableRowNode();
270
+ for (let col = 0; col < 3; col++) {
271
+ const cell = $createTableCellNode(TableCellHeaderStates.ROW);
272
+ const paragraph = $createParagraphNode();
273
+ paragraph.append($createTextNode(`Header ${col + 1}`));
274
+ cell.append(paragraph);
275
+ headerRow.append(cell);
276
+ }
277
+ table.append(headerRow);
278
+ // Data rows
279
+ for (let row = 0; row < 2; row++) {
280
+ const tableRow = $createTableRowNode();
281
+ for (let col = 0; col < 3; col++) {
282
+ const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
283
+ const paragraph = $createParagraphNode();
284
+ paragraph.append($createTextNode(''));
285
+ cell.append(paragraph);
286
+ tableRow.append(cell);
287
+ }
288
+ table.append(tableRow);
289
+ }
290
+ selection.insertNodes([table]);
291
+ }
292
+ });
293
+ },
294
+ },
295
+ {
296
+ key: 'equation-inline',
297
+ label: 'Inline Equation',
298
+ description: 'Math formula inline with text',
299
+ icon: '∑',
300
+ keywords: ['math', 'equation', 'latex', 'katex', 'formula', 'inline'],
301
+ onSelect: (editor) => {
302
+ editor.update(() => {
303
+ const selection = $getSelection();
304
+ if ($isRangeSelection(selection)) {
305
+ const node = $createEquationNode('E = mc^2', true);
306
+ selection.insertNodes([node]);
307
+ }
308
+ });
309
+ },
310
+ },
311
+ {
312
+ key: 'equation-block',
313
+ label: 'Block Equation',
314
+ description: 'Centered math formula',
315
+ icon: '∫',
316
+ keywords: ['math', 'equation', 'latex', 'katex', 'formula', 'block', 'display'],
317
+ onSelect: (editor) => {
318
+ editor.update(() => {
319
+ const selection = $getSelection();
320
+ if ($isRangeSelection(selection)) {
321
+ const node = $createEquationNode('\\int_{a}^{b} f(x) \\, dx', false);
322
+ selection.insertNodes([node]);
323
+ }
324
+ });
325
+ },
326
+ },
327
+ {
328
+ key: 'mermaid',
329
+ label: 'Mermaid Diagram',
330
+ description: 'Flowchart, sequence, or other diagram',
331
+ icon: '📊',
332
+ keywords: ['mermaid', 'diagram', 'flowchart', 'sequence', 'graph', 'chart', 'uml'],
333
+ onSelect: (editor) => {
334
+ editor.update(() => {
335
+ const selection = $getSelection();
336
+ if ($isRangeSelection(selection)) {
337
+ const node = $createMermaidNode(`flowchart TD
338
+ A[Start] --> B{Decision}
339
+ B -->|Yes| C[OK]
340
+ B -->|No| D[End]`);
341
+ selection.insertNodes([node]);
342
+ }
343
+ });
344
+ },
345
+ },
346
+ {
347
+ key: 'c4',
348
+ label: 'C4 Architecture Diagram',
349
+ description: 'System context, container, or component diagram',
350
+ icon: '🏗',
351
+ keywords: ['c4', 'architecture', 'diagram', 'system', 'container', 'component'],
352
+ onSelect: (editor) => {
353
+ editor.update(() => {
354
+ const selection = $getSelection();
355
+ if ($isRangeSelection(selection)) {
356
+ const node = $createC4Node(`Person(user, "User", "End user of the system")
357
+
358
+ System_Boundary(mySystem, "My System") {
359
+ Container(web, "Web App", "React", "Delivers the UI")
360
+ Container(api, "API Server", "Node.js", "Business logic")
361
+ ContainerDb(db, "Database", "PostgreSQL", "Stores data")
362
+ }
363
+
364
+ Rel(user, web, "Uses", "HTTPS")
365
+ Rel(web, api, "Calls", "REST/JSON")
366
+ Rel(api, db, "Reads/Writes", "SQL")`);
367
+ selection.insertNodes([node]);
368
+ }
369
+ });
370
+ },
371
+ },
372
+ ];
373
+ export function SlashMenu({ isOpen, position, query, onClose }) {
374
+ const [editor] = useLexicalComposerContext();
375
+ const [selectedIndex, setSelectedIndex] = useState(0);
376
+ const listRef = useRef(null);
377
+ const filteredOptions = useMemo(() => {
378
+ if (!query)
379
+ return BLOCK_OPTIONS;
380
+ const lowerQuery = query.toLowerCase();
381
+ // Filter and score options
382
+ const scored = BLOCK_OPTIONS
383
+ .map((option) => {
384
+ const labelLower = option.label.toLowerCase();
385
+ let score = 0;
386
+ // Exact label match gets highest score
387
+ if (labelLower === lowerQuery) {
388
+ score = 100;
389
+ }
390
+ // Label starts with query
391
+ else if (labelLower.startsWith(lowerQuery)) {
392
+ score = 80;
393
+ }
394
+ // Label contains query
395
+ else if (labelLower.includes(lowerQuery)) {
396
+ score = 60;
397
+ }
398
+ // Keyword starts with query
399
+ else if (option.keywords.some((kw) => kw.startsWith(lowerQuery))) {
400
+ score = 40;
401
+ }
402
+ // Keyword contains query
403
+ else if (option.keywords.some((kw) => kw.includes(lowerQuery))) {
404
+ score = 20;
405
+ }
406
+ return { option, score };
407
+ })
408
+ .filter(({ score }) => score > 0)
409
+ .sort((a, b) => b.score - a.score);
410
+ return scored.map(({ option }) => option);
411
+ }, [query]);
412
+ // Reset selection when query changes - use ref to track previous query
413
+ const prevQueryRef = useRef(query);
414
+ useEffect(() => {
415
+ if (query !== prevQueryRef.current) {
416
+ prevQueryRef.current = query;
417
+ // Use microtask to avoid setState during render cycle
418
+ queueMicrotask(() => setSelectedIndex(0));
419
+ }
420
+ }, [query]);
421
+ // Scroll selected item into view
422
+ useEffect(() => {
423
+ if (listRef.current && isOpen) {
424
+ const selectedItem = listRef.current.children[selectedIndex];
425
+ if (selectedItem) {
426
+ selectedItem.scrollIntoView({ block: 'nearest' });
427
+ }
428
+ }
429
+ }, [selectedIndex, isOpen]);
430
+ const handleSelect = useCallback((option) => {
431
+ // Remove the slash and query text before inserting
432
+ editor.update(() => {
433
+ const selection = $getSelection();
434
+ if ($isRangeSelection(selection)) {
435
+ const anchor = selection.anchor;
436
+ const anchorNode = anchor.getNode();
437
+ if (anchorNode instanceof TextNode) {
438
+ const text = anchorNode.getTextContent();
439
+ const slashIndex = text.lastIndexOf('/');
440
+ if (slashIndex >= 0) {
441
+ // Remove the "/" and query text
442
+ const before = text.substring(0, slashIndex);
443
+ anchorNode.setTextContent(before);
444
+ }
445
+ }
446
+ }
447
+ });
448
+ option.onSelect(editor);
449
+ onClose();
450
+ }, [editor, onClose]);
451
+ // Handle keyboard navigation with native events (capture phase to intercept before Lexical)
452
+ useEffect(() => {
453
+ if (!isOpen)
454
+ return;
455
+ const handleKeyDown = (event) => {
456
+ switch (event.key) {
457
+ case 'ArrowDown':
458
+ event.preventDefault();
459
+ event.stopPropagation();
460
+ setSelectedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev);
461
+ break;
462
+ case 'ArrowUp':
463
+ event.preventDefault();
464
+ event.stopPropagation();
465
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
466
+ break;
467
+ case 'Enter':
468
+ event.preventDefault();
469
+ event.stopPropagation();
470
+ if (filteredOptions[selectedIndex]) {
471
+ handleSelect(filteredOptions[selectedIndex]);
472
+ }
473
+ break;
474
+ case 'Escape':
475
+ event.preventDefault();
476
+ event.stopPropagation();
477
+ onClose();
478
+ break;
479
+ }
480
+ };
481
+ // Use capture phase to intercept events before they reach Lexical
482
+ document.addEventListener('keydown', handleKeyDown, true);
483
+ return () => {
484
+ document.removeEventListener('keydown', handleKeyDown, true);
485
+ };
486
+ }, [isOpen, filteredOptions, selectedIndex, handleSelect, onClose]);
487
+ if (!isOpen || !position)
488
+ return null;
489
+ return (_jsxs("div", { className: "slash-menu", style: {
490
+ position: 'fixed',
491
+ top: position.top,
492
+ left: position.left,
493
+ }, children: [_jsx("div", { className: "slash-menu-header", children: "INSERT BLOCK" }), filteredOptions.length === 0 ? (_jsx("div", { className: "slash-menu-empty", children: "No matching blocks" })) : (_jsx("ul", { className: "slash-menu-list", role: "listbox", ref: listRef, children: filteredOptions.map((option, index) => (_jsxs("li", { role: "option", "aria-selected": index === selectedIndex, className: `slash-menu-item ${index === selectedIndex ? 'selected' : ''}`, onClick: () => handleSelect(option), onMouseEnter: () => setSelectedIndex(index), children: [_jsx("span", { className: "slash-menu-icon", children: option.icon }), _jsxs("div", { className: "slash-menu-content", children: [_jsx("span", { className: "slash-menu-label", children: option.label }), _jsx("span", { className: "slash-menu-description", children: option.description })] })] }, option.key))) }))] }));
494
+ }
@@ -0,0 +1 @@
1
+ export declare function SlashMenuPlugin(): import("react").JSX.Element;
@@ -0,0 +1,168 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState, useRef } from 'react';
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
+ import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, TextNode, } from 'lexical';
5
+ import { SlashMenu } from './SlashMenu.js';
6
+ // Compare positions with tolerance to avoid updates for sub-pixel differences
7
+ function positionsEqual(a, b) {
8
+ if (a === null && b === null)
9
+ return true;
10
+ if (a === null || b === null)
11
+ return false;
12
+ return Math.abs(a.top - b.top) < 1 && Math.abs(a.left - b.left) < 1;
13
+ }
14
+ export function SlashMenuPlugin() {
15
+ const [editor] = useLexicalComposerContext();
16
+ const [menuState, setMenuState] = useState({
17
+ isOpen: false,
18
+ position: null,
19
+ query: '',
20
+ });
21
+ // Use refs to access state without causing effect re-registration
22
+ const menuStateRef = useRef(menuState);
23
+ // Sync state to ref in effect, not during render
24
+ useEffect(() => {
25
+ menuStateRef.current = menuState;
26
+ }, [menuState]);
27
+ const closeMenu = useCallback(() => {
28
+ setMenuState({ isOpen: false, position: null, query: '' });
29
+ }, []);
30
+ useEffect(() => {
31
+ const updateListener = editor.registerUpdateListener(({ editorState }) => {
32
+ editorState.read(() => {
33
+ const selection = $getSelection();
34
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
35
+ if (menuStateRef.current.isOpen) {
36
+ closeMenu();
37
+ }
38
+ return;
39
+ }
40
+ const anchor = selection.anchor;
41
+ const anchorNode = anchor.getNode();
42
+ if (!(anchorNode instanceof TextNode)) {
43
+ if (menuStateRef.current.isOpen) {
44
+ closeMenu();
45
+ }
46
+ return;
47
+ }
48
+ const text = anchorNode.getTextContent();
49
+ const offset = anchor.offset;
50
+ const textBeforeCursor = text.substring(0, offset);
51
+ // Find the last "/" in the text before cursor
52
+ const slashIndex = textBeforeCursor.lastIndexOf('/');
53
+ if (slashIndex === -1) {
54
+ if (menuStateRef.current.isOpen) {
55
+ closeMenu();
56
+ }
57
+ return;
58
+ }
59
+ // Check if slash is at start of line or preceded by whitespace
60
+ const beforeSlash = textBeforeCursor.substring(0, slashIndex);
61
+ if (beforeSlash.length > 0 && !/\s$/.test(beforeSlash)) {
62
+ if (menuStateRef.current.isOpen) {
63
+ closeMenu();
64
+ }
65
+ return;
66
+ }
67
+ const query = textBeforeCursor.substring(slashIndex + 1);
68
+ // Don't show menu if there's a space in the query
69
+ if (/\s/.test(query)) {
70
+ if (menuStateRef.current.isOpen) {
71
+ closeMenu();
72
+ }
73
+ return;
74
+ }
75
+ // Get position for menu - position at the slash character
76
+ const nativeSelection = window.getSelection();
77
+ if (!nativeSelection || nativeSelection.rangeCount === 0) {
78
+ return;
79
+ }
80
+ const range = nativeSelection.getRangeAt(0);
81
+ // Create a range that starts at the slash position
82
+ const slashRange = document.createRange();
83
+ const textNode = range.startContainer;
84
+ if (textNode.nodeType === Node.TEXT_NODE) {
85
+ // Find the actual position of the slash in the text node
86
+ const textContent = textNode.textContent || '';
87
+ const slashPosInNode = textContent.lastIndexOf('/');
88
+ if (slashPosInNode >= 0) {
89
+ slashRange.setStart(textNode, slashPosInNode);
90
+ slashRange.setEnd(textNode, slashPosInNode + 1);
91
+ }
92
+ else {
93
+ slashRange.setStart(textNode, 0);
94
+ slashRange.setEnd(textNode, 0);
95
+ }
96
+ }
97
+ else {
98
+ slashRange.setStart(textNode, 0);
99
+ slashRange.setEnd(textNode, 0);
100
+ }
101
+ const rect = slashRange.getBoundingClientRect();
102
+ const menuHeight = 320; // max-height from CSS
103
+ const menuWidth = 280; // min-width from CSS
104
+ const padding = 4;
105
+ // Check if menu would overflow bottom of viewport
106
+ const spaceBelow = window.innerHeight - rect.bottom - padding;
107
+ const spaceAbove = rect.top - padding;
108
+ let top;
109
+ if (spaceBelow >= menuHeight) {
110
+ // Enough space below - position below cursor
111
+ top = rect.bottom + padding;
112
+ }
113
+ else if (spaceAbove >= menuHeight) {
114
+ // Not enough below, but enough above - position above cursor
115
+ top = rect.top - menuHeight - padding;
116
+ }
117
+ else {
118
+ // Not enough space either way - position where there's more room
119
+ top =
120
+ spaceBelow > spaceAbove
121
+ ? rect.bottom + padding
122
+ : rect.top - menuHeight - padding;
123
+ }
124
+ // Check horizontal overflow
125
+ let left = rect.left;
126
+ if (left + menuWidth > window.innerWidth) {
127
+ left = Math.max(padding, window.innerWidth - menuWidth - padding);
128
+ }
129
+ const newPosition = { top, left };
130
+ // Only update state if something changed
131
+ const currentState = menuStateRef.current;
132
+ if (!currentState.isOpen ||
133
+ currentState.query !== query ||
134
+ !positionsEqual(currentState.position, newPosition)) {
135
+ setMenuState({
136
+ isOpen: true,
137
+ position: newPosition,
138
+ query,
139
+ });
140
+ }
141
+ });
142
+ });
143
+ return updateListener;
144
+ }, [editor, closeMenu]);
145
+ // Close menu on backspace if query is empty and menu is open
146
+ useEffect(() => {
147
+ return editor.registerCommand(KEY_BACKSPACE_COMMAND, () => {
148
+ if (menuStateRef.current.isOpen && menuStateRef.current.query === '') {
149
+ closeMenu();
150
+ }
151
+ return false;
152
+ }, COMMAND_PRIORITY_LOW);
153
+ }, [editor, closeMenu]);
154
+ // Close menu on click outside
155
+ useEffect(() => {
156
+ if (!menuState.isOpen)
157
+ return;
158
+ const handleClick = (event) => {
159
+ const target = event.target;
160
+ if (!target.closest('.slash-menu')) {
161
+ closeMenu();
162
+ }
163
+ };
164
+ document.addEventListener('click', handleClick);
165
+ return () => document.removeEventListener('click', handleClick);
166
+ }, [menuState.isOpen, closeMenu]);
167
+ return (_jsx(SlashMenu, { isOpen: menuState.isOpen, position: menuState.position, query: menuState.query, onClose: closeMenu }));
168
+ }
@@ -0,0 +1 @@
1
+ export declare function TableActionsPlugin(): import("react").JSX.Element | null;