@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,301 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * FrontmatterPlugin - Shows frontmatter in a collapsible tray overlay
4
+ *
5
+ * The actual FrontmatterNode is hidden via CSS but remains in the Lexical tree.
6
+ * This plugin provides a UI to view/edit frontmatter in a sliding tray.
7
+ *
8
+ * The toggle button is rendered externally (in the editor header).
9
+ * This plugin communicates via Zustand store for state management.
10
+ *
11
+ * For .mdc files (Cursor rules), a specialized form UI is shown with:
12
+ * - alwaysApply dropdown (true/false)
13
+ * - description text input
14
+ * - globs array editor
15
+ */
16
+ import { useCallback, useEffect, useState, useRef, useMemo } from 'react';
17
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
18
+ import { $getRoot, $getNodeByKey, $createTextNode, $isElementNode } from 'lexical';
19
+ import { $isFrontmatterNode } from './nodes/index.js';
20
+ import { useEditorStore } from '../../stores/editorStore.js';
21
+ function parseMdcFrontmatter(content) {
22
+ const lines = content.split('\n');
23
+ const result = {
24
+ description: '',
25
+ alwaysApply: null,
26
+ globs: [],
27
+ otherLines: [],
28
+ };
29
+ let inGlobs = false;
30
+ for (const line of lines) {
31
+ const trimmed = line.trim();
32
+ // Check for description
33
+ const descMatch = /^description:\s*(.*)$/.exec(line);
34
+ if (descMatch) {
35
+ result.description = descMatch[1].trim().replace(/^["']|["']$/g, '');
36
+ inGlobs = false;
37
+ continue;
38
+ }
39
+ // Check for alwaysApply
40
+ const alwaysApplyMatch = /^alwaysApply:\s*(.*)$/.exec(line);
41
+ if (alwaysApplyMatch) {
42
+ const value = alwaysApplyMatch[1].trim().toLowerCase();
43
+ result.alwaysApply = value === 'true';
44
+ inGlobs = false;
45
+ continue;
46
+ }
47
+ // Check for globs array start
48
+ if (/^globs:\s*$/.exec(line)) {
49
+ inGlobs = true;
50
+ continue;
51
+ }
52
+ // Check for globs inline array: globs: ["*.ts", "*.tsx"]
53
+ const inlineGlobsMatch = /^globs:\s*\[(.*)\]$/.exec(line);
54
+ if (inlineGlobsMatch) {
55
+ const items = inlineGlobsMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(s => s);
56
+ result.globs = items;
57
+ inGlobs = false;
58
+ continue;
59
+ }
60
+ // If we're inside globs array, parse array items
61
+ if (inGlobs && trimmed.startsWith('-')) {
62
+ const item = trimmed.slice(1).trim().replace(/^["']|["']$/g, '');
63
+ if (item) {
64
+ result.globs.push(item);
65
+ }
66
+ continue;
67
+ }
68
+ // Any other non-empty line that's not a continuation of globs
69
+ if (trimmed && !inGlobs) {
70
+ inGlobs = false;
71
+ result.otherLines.push(line);
72
+ }
73
+ else if (trimmed && inGlobs && !trimmed.startsWith('-')) {
74
+ // Non-array item while in globs means globs section ended
75
+ inGlobs = false;
76
+ result.otherLines.push(line);
77
+ }
78
+ }
79
+ return result;
80
+ }
81
+ function serializeMdcFrontmatter(fm) {
82
+ const lines = [];
83
+ // Description first if set
84
+ if (fm.description) {
85
+ // Quote the description if it contains special characters
86
+ const needsQuotes = /[:#[\]{}|>&*!?,]/.test(fm.description);
87
+ lines.push(`description: ${needsQuotes ? `"${fm.description}"` : fm.description}`);
88
+ }
89
+ // alwaysApply if set
90
+ if (fm.alwaysApply !== null) {
91
+ lines.push(`alwaysApply: ${fm.alwaysApply}`);
92
+ }
93
+ // Globs array
94
+ if (fm.globs.length > 0) {
95
+ lines.push('globs:');
96
+ for (const glob of fm.globs) {
97
+ lines.push(` - ${glob}`);
98
+ }
99
+ }
100
+ // Preserve other lines
101
+ for (const line of fm.otherLines) {
102
+ lines.push(line);
103
+ }
104
+ return lines.join('\n');
105
+ }
106
+ // Find frontmatter node in the tree (should only be one, at the start)
107
+ function findFrontmatterNode(node) {
108
+ if ($isFrontmatterNode(node)) {
109
+ return node;
110
+ }
111
+ if ($isElementNode(node)) {
112
+ for (const child of node.getChildren()) {
113
+ const found = findFrontmatterNode(child);
114
+ if (found)
115
+ return found;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ function getRuleType(fm) {
121
+ if (fm.alwaysApply === true)
122
+ return 'always';
123
+ if (fm.globs.length > 0)
124
+ return 'auto-attach';
125
+ if (fm.description)
126
+ return 'intelligent';
127
+ return 'manual';
128
+ }
129
+ const RULE_TYPE_INFO = {
130
+ 'always': {
131
+ label: 'Always Apply',
132
+ description: 'Included in every chat session automatically',
133
+ cursorLabel: 'Always Apply'
134
+ },
135
+ 'auto-attach': {
136
+ label: 'Auto-Attach to Files',
137
+ description: 'Applied when files matching glob patterns are in context',
138
+ cursorLabel: 'Apply to Specific Files'
139
+ },
140
+ 'intelligent': {
141
+ label: 'Agent Decides',
142
+ description: 'Agent decides relevance based on description',
143
+ cursorLabel: 'Apply Intelligently'
144
+ },
145
+ 'manual': {
146
+ label: 'Manual Only',
147
+ description: 'Only applied when @-mentioned (e.g., @rule-name)',
148
+ cursorLabel: 'Apply Manually'
149
+ }
150
+ };
151
+ function MdcFrontmatterForm({ content, onContentChange }) {
152
+ const parsed = useMemo(() => parseMdcFrontmatter(content), [content]);
153
+ const [newGlob, setNewGlob] = useState('');
154
+ const currentRuleType = useMemo(() => getRuleType(parsed), [parsed]);
155
+ const handleDescriptionChange = useCallback((e) => {
156
+ const updated = { ...parsed, description: e.target.value };
157
+ onContentChange(serializeMdcFrontmatter(updated));
158
+ }, [parsed, onContentChange]);
159
+ const handleRuleTypeChange = useCallback((e) => {
160
+ const newType = e.target.value;
161
+ const updated = { ...parsed };
162
+ switch (newType) {
163
+ case 'always':
164
+ updated.alwaysApply = true;
165
+ // Keep existing globs and description, they're just ignored
166
+ break;
167
+ case 'auto-attach':
168
+ updated.alwaysApply = null; // Remove alwaysApply
169
+ // If no globs, add a placeholder to show intent
170
+ if (updated.globs.length === 0) {
171
+ updated.globs = ['**/*.md']; // Example pattern
172
+ }
173
+ break;
174
+ case 'intelligent':
175
+ updated.alwaysApply = false;
176
+ updated.globs = []; // Clear globs
177
+ // Ensure description exists
178
+ if (!updated.description) {
179
+ updated.description = 'Describe when this rule should apply...';
180
+ }
181
+ break;
182
+ case 'manual':
183
+ updated.alwaysApply = false;
184
+ updated.globs = [];
185
+ updated.description = ''; // Clear description
186
+ break;
187
+ }
188
+ onContentChange(serializeMdcFrontmatter(updated));
189
+ }, [parsed, onContentChange]);
190
+ const handleRemoveGlob = useCallback((index) => {
191
+ const newGlobs = parsed.globs.filter((_, i) => i !== index);
192
+ const updated = { ...parsed, globs: newGlobs };
193
+ onContentChange(serializeMdcFrontmatter(updated));
194
+ }, [parsed, onContentChange]);
195
+ const handleAddGlob = useCallback(() => {
196
+ if (!newGlob.trim())
197
+ return;
198
+ const updated = { ...parsed, globs: [...parsed.globs, newGlob.trim()] };
199
+ onContentChange(serializeMdcFrontmatter(updated));
200
+ setNewGlob('');
201
+ }, [parsed, newGlob, onContentChange]);
202
+ const handleGlobKeyDown = useCallback((e) => {
203
+ if (e.key === 'Enter') {
204
+ e.preventDefault();
205
+ handleAddGlob();
206
+ }
207
+ }, [handleAddGlob]);
208
+ const info = RULE_TYPE_INFO[currentRuleType];
209
+ return (_jsxs("div", { className: "mdc-form", children: [_jsxs("div", { className: "mdc-form-field", children: [_jsxs("label", { htmlFor: "mdc-rule-type", className: "mdc-form-label", children: ["Rule Type", _jsxs("span", { className: "mdc-form-label-cursor", children: ["(Cursor: ", info.cursorLabel, ")"] })] }), _jsxs("select", { id: "mdc-rule-type", className: "mdc-form-select", value: currentRuleType, onChange: handleRuleTypeChange, children: [_jsx("option", { value: "always", children: "Always Apply \u2014 every chat session" }), _jsx("option", { value: "auto-attach", children: "Auto-Attach \u2014 when files match globs" }), _jsx("option", { value: "intelligent", children: "Agent Decides \u2014 based on description" }), _jsx("option", { value: "manual", children: "Manual Only \u2014 requires @mention" })] }), _jsx("span", { className: "mdc-form-hint mdc-form-hint-info", children: info.description })] }), _jsxs("div", { className: "mdc-form-field", children: [_jsxs("label", { htmlFor: "mdc-description", className: "mdc-form-label", children: ["Description", currentRuleType === 'intelligent' && _jsx("span", { className: "mdc-form-required", children: "*" })] }), _jsx("input", { id: "mdc-description", type: "text", className: "mdc-form-input", value: parsed.description, onChange: handleDescriptionChange, placeholder: currentRuleType === 'intelligent'
210
+ ? "Required: describe when this rule should apply..."
211
+ : "Optional: describe what this rule does" }), currentRuleType === 'intelligent' && (_jsx("span", { className: "mdc-form-hint mdc-form-hint-warn", children: "\u26A0\uFE0F Agent uses this to decide relevance \u2014 be specific!" }))] }), currentRuleType === 'auto-attach' && (_jsxs("div", { className: "mdc-form-field", children: [_jsxs("label", { className: "mdc-form-label", children: ["File Patterns (globs)", _jsx("span", { className: "mdc-form-required", children: "*" })] }), _jsx("div", { className: "mdc-globs-list", children: parsed.globs.map((glob, index) => (_jsxs("div", { className: "mdc-glob-item", children: [_jsx("code", { className: "mdc-glob-pattern", children: glob }), _jsx("button", { type: "button", className: "mdc-glob-remove", onClick: () => handleRemoveGlob(index), title: "Remove glob", children: _jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }, index))) }), _jsxs("div", { className: "mdc-glob-add", children: [_jsx("input", { type: "text", className: "mdc-form-input", value: newGlob, onChange: (e) => setNewGlob(e.target.value), onKeyDown: handleGlobKeyDown, placeholder: "e.g., **/*.ts or src/components/**" }), _jsx("button", { type: "button", className: "mdc-glob-add-btn", onClick: handleAddGlob, disabled: !newGlob.trim(), children: "Add" })] }), _jsx("span", { className: "mdc-form-hint", children: "Rule auto-attaches when files matching these patterns are in context" })] })), parsed.otherLines.length > 0 && (_jsxs("div", { className: "mdc-form-field", children: [_jsx("label", { className: "mdc-form-label", children: "Other Fields (raw YAML)" }), _jsx("pre", { className: "mdc-other-yaml", children: parsed.otherLines.join('\n') })] }))] }));
212
+ }
213
+ function FrontmatterTray({ isOpen, content, onClose, onContentChange, isMdcFile }) {
214
+ const textareaRef = useRef(null);
215
+ const [showRaw, setShowRaw] = useState(false);
216
+ // Auto-resize textarea to fit content
217
+ useEffect(() => {
218
+ if (textareaRef.current) {
219
+ textareaRef.current.style.height = 'auto';
220
+ textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
221
+ }
222
+ }, [content, isOpen, showRaw]);
223
+ // Focus textarea when tray opens (only in raw mode)
224
+ useEffect(() => {
225
+ if (isOpen && textareaRef.current && (!isMdcFile || showRaw)) {
226
+ textareaRef.current.focus();
227
+ }
228
+ }, [isOpen, isMdcFile, showRaw]);
229
+ return (_jsxs("div", { className: `frontmatter-tray ${isOpen ? 'frontmatter-tray-open' : ''}`, children: [_jsxs("div", { className: "frontmatter-tray-header", children: [_jsx("span", { className: "frontmatter-tray-title", children: isMdcFile ? 'Cursor Rule Config' : 'Frontmatter' }), _jsxs("div", { className: "frontmatter-tray-actions", children: [isMdcFile && (_jsx("button", { className: "frontmatter-tray-toggle-raw", onClick: () => setShowRaw(!showRaw), title: showRaw ? 'Show form view' : 'Show raw YAML', children: showRaw ? 'Form' : 'Raw' })), _jsx("button", { className: "frontmatter-tray-close", onClick: onClose, title: "Close", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] })] }), _jsx("div", { className: "frontmatter-tray-content", children: isMdcFile && !showRaw ? (_jsx(MdcFrontmatterForm, { content: content, onContentChange: onContentChange })) : (_jsx("textarea", { ref: textareaRef, className: "frontmatter-tray-textarea", value: content, onChange: (e) => onContentChange(e.target.value), placeholder: "---\ntitle: Document Title\n---", spellCheck: false })) })] }));
230
+ }
231
+ export function FrontmatterPlugin({ filePath }) {
232
+ const [editor] = useLexicalComposerContext();
233
+ const [frontmatterContent, setFrontmatterContent] = useState('');
234
+ const [frontmatterNodeKey, setFrontmatterNodeKey] = useState(null);
235
+ // Use Zustand store for state management
236
+ const setHasFrontmatter = useEditorStore((state) => state.setHasFrontmatter);
237
+ const setFrontmatterOpen = useEditorStore((state) => state.setFrontmatterOpen);
238
+ const isFrontmatterOpen = useEditorStore((state) => state.isFrontmatterOpen);
239
+ // Detect if we're editing an MDC file (Cursor rules)
240
+ const isMdcFile = useMemo(() => {
241
+ return filePath?.endsWith('.mdc') ?? false;
242
+ }, [filePath]);
243
+ // Track frontmatter node and content
244
+ useEffect(() => {
245
+ const updateFrontmatter = () => {
246
+ editor.getEditorState().read(() => {
247
+ const root = $getRoot();
248
+ const frontmatterNode = findFrontmatterNode(root);
249
+ if (frontmatterNode) {
250
+ setHasFrontmatter(true);
251
+ setFrontmatterContent(frontmatterNode.getTextContent());
252
+ setFrontmatterNodeKey(frontmatterNode.getKey());
253
+ }
254
+ else {
255
+ setHasFrontmatter(false);
256
+ setFrontmatterContent('');
257
+ setFrontmatterNodeKey(null);
258
+ }
259
+ });
260
+ };
261
+ // Initial check
262
+ updateFrontmatter();
263
+ // Listen for updates
264
+ const unsubscribe = editor.registerUpdateListener(() => {
265
+ updateFrontmatter();
266
+ });
267
+ return unsubscribe;
268
+ }, [editor, setHasFrontmatter]);
269
+ // Listen for toggle commands from the UI
270
+ useEffect(() => {
271
+ // Subscribe to store changes to detect toggle requests
272
+ // The toggle function in the store handles the state change
273
+ // We don't need to do anything here as the state is already managed
274
+ }, []);
275
+ // Update frontmatter content in the Lexical node
276
+ const handleContentChange = useCallback((newContent) => {
277
+ setFrontmatterContent(newContent);
278
+ if (!frontmatterNodeKey)
279
+ return;
280
+ editor.update(() => {
281
+ const node = $getNodeByKey(frontmatterNodeKey);
282
+ if (node && $isFrontmatterNode(node)) {
283
+ // Clear existing children and add new content
284
+ node.clear();
285
+ if (newContent) {
286
+ node.append($createTextNode(newContent));
287
+ }
288
+ }
289
+ });
290
+ }, [editor, frontmatterNodeKey]);
291
+ const handleClose = useCallback(() => {
292
+ setFrontmatterOpen(false);
293
+ }, [setFrontmatterOpen]);
294
+ // Get hasFrontmatter from store
295
+ const hasFrontmatter = useEditorStore((state) => state.hasFrontmatter);
296
+ // Don't render anything if no frontmatter
297
+ if (!hasFrontmatter) {
298
+ return null;
299
+ }
300
+ return (_jsx(FrontmatterTray, { isOpen: isFrontmatterOpen, content: frontmatterContent, onClose: handleClose, onContentChange: handleContentChange, isMdcFile: isMdcFile }));
301
+ }
@@ -0,0 +1,13 @@
1
+ export interface ImageData {
2
+ src: string;
3
+ alt: string;
4
+ isLocal: boolean;
5
+ dataUri?: string;
6
+ }
7
+ interface ImageModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ onInsert: (data: ImageData) => void;
11
+ }
12
+ export declare function ImageModal({ isOpen, onClose, onInsert }: ImageModalProps): import("react").JSX.Element | null;
13
+ export {};
@@ -0,0 +1,267 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
+ // SECURITY: Maximum file size for uploaded images (5MB)
4
+ const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
5
+ // SECURITY: Maximum URL length
6
+ const MAX_URL_LENGTH = 2048;
7
+ /**
8
+ * Check if a path is a relative/local path (not an absolute URL)
9
+ */
10
+ function isRelativePath(path) {
11
+ // Relative paths don't start with a protocol
12
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path)) {
13
+ return false;
14
+ }
15
+ // Common relative path patterns
16
+ return path.startsWith('./') ||
17
+ path.startsWith('../') ||
18
+ path.startsWith('assets/') ||
19
+ path.startsWith('images/') ||
20
+ // Any path that looks like a file path (contains / but no protocol)
21
+ (/^[^:]+\/[^:]*$/.test(path) && !path.includes('://'));
22
+ }
23
+ /**
24
+ * SECURITY: Validate image URL or path
25
+ * - Allows http/https protocols for remote images
26
+ * - Allows relative paths for local images (e.g., assets/image.png)
27
+ * - Blocks dangerous protocols (javascript:, data:, file:)
28
+ */
29
+ function validateImageUrl(url) {
30
+ if (!url) {
31
+ return { valid: false, error: 'URL or path is required' };
32
+ }
33
+ if (url.length > MAX_URL_LENGTH) {
34
+ return { valid: false, error: `Path is too long. Maximum ${MAX_URL_LENGTH} characters.` };
35
+ }
36
+ // Check if it's a relative/local path
37
+ if (isRelativePath(url)) {
38
+ // Basic validation for relative paths
39
+ // SECURITY: Block path traversal attempts that try to escape the workspace
40
+ const normalizedPath = url.replace(/\\/g, '/');
41
+ // Count how many directories up we go vs down
42
+ const parts = normalizedPath.split('/');
43
+ let depth = 0;
44
+ for (const part of parts) {
45
+ if (part === '..') {
46
+ depth--;
47
+ if (depth < -2) {
48
+ // Allow some traversal (e.g., ../assets/) but not excessive
49
+ return { valid: false, error: 'Path traversal not allowed' };
50
+ }
51
+ }
52
+ else if (part && part !== '.') {
53
+ depth++;
54
+ }
55
+ }
56
+ return { valid: true, isLocal: true };
57
+ }
58
+ // It's an absolute URL - validate it
59
+ try {
60
+ const parsed = new URL(url);
61
+ // SECURITY: Block dangerous protocols
62
+ const blockedProtocols = ['javascript:', 'data:', 'file:', 'vbscript:'];
63
+ if (blockedProtocols.includes(parsed.protocol)) {
64
+ return { valid: false, error: 'This URL protocol is not allowed' };
65
+ }
66
+ // Only allow http/https protocols for remote URLs
67
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
68
+ return { valid: false, error: 'Only http:// and https:// URLs are allowed for remote images' };
69
+ }
70
+ return { valid: true, isLocal: false };
71
+ }
72
+ catch {
73
+ // If URL parsing fails but it looks like a path, treat as relative
74
+ if (url.includes('/') || url.includes('.')) {
75
+ return { valid: true, isLocal: true };
76
+ }
77
+ return { valid: false, error: 'Invalid URL or path format' };
78
+ }
79
+ }
80
+ export function ImageModal({ isOpen, onClose, onInsert }) {
81
+ const [activeTab, setActiveTab] = useState('upload');
82
+ const [url, setUrl] = useState('');
83
+ const [alt, setAlt] = useState('');
84
+ const [previewUrl, setPreviewUrl] = useState(null);
85
+ const [dragOver, setDragOver] = useState(false);
86
+ const [error, setError] = useState(null);
87
+ const fileInputRef = useRef(null);
88
+ const modalRef = useRef(null);
89
+ const selectedFileRef = useRef(null);
90
+ // Reset state when modal opens - track previous isOpen to detect open transition
91
+ const prevIsOpenRef = useRef(isOpen);
92
+ useEffect(() => {
93
+ // Only reset when transitioning from closed to open
94
+ if (isOpen && !prevIsOpenRef.current) {
95
+ // Use microtask to avoid setState during render
96
+ queueMicrotask(() => {
97
+ setUrl('');
98
+ setAlt('');
99
+ setPreviewUrl(null);
100
+ setError(null);
101
+ setActiveTab('upload');
102
+ selectedFileRef.current = null;
103
+ });
104
+ }
105
+ prevIsOpenRef.current = isOpen;
106
+ }, [isOpen]);
107
+ // Keyboard handling: Escape to close, Enter to submit
108
+ useEffect(() => {
109
+ if (!isOpen)
110
+ return;
111
+ const handleKeyDown = (e) => {
112
+ if (e.key === 'Escape') {
113
+ onClose();
114
+ }
115
+ };
116
+ document.addEventListener('keydown', handleKeyDown);
117
+ return () => document.removeEventListener('keydown', handleKeyDown);
118
+ }, [isOpen, onClose]);
119
+ // Handle click outside to close
120
+ useEffect(() => {
121
+ if (!isOpen)
122
+ return;
123
+ const handleClickOutside = (e) => {
124
+ if (modalRef.current && !modalRef.current.contains(e.target)) {
125
+ onClose();
126
+ }
127
+ };
128
+ // Delay adding the listener to avoid immediate close
129
+ const timeoutId = setTimeout(() => {
130
+ document.addEventListener('mousedown', handleClickOutside);
131
+ }, 0);
132
+ return () => {
133
+ clearTimeout(timeoutId);
134
+ document.removeEventListener('mousedown', handleClickOutside);
135
+ };
136
+ }, [isOpen, onClose]);
137
+ const handleFileSelect = useCallback((file) => {
138
+ if (!file.type.startsWith('image/')) {
139
+ setError('Please select an image file');
140
+ return;
141
+ }
142
+ // SECURITY: Check file size before reading
143
+ if (file.size > MAX_IMAGE_SIZE) {
144
+ setError(`Image too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB`);
145
+ return;
146
+ }
147
+ const reader = new FileReader();
148
+ reader.onload = (e) => {
149
+ const dataUri = e.target?.result;
150
+ selectedFileRef.current = { dataUri, name: file.name };
151
+ setPreviewUrl(dataUri);
152
+ setError(null);
153
+ // Auto-generate alt text from filename (without extension)
154
+ if (!alt) {
155
+ const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
156
+ setAlt(nameWithoutExt);
157
+ }
158
+ };
159
+ reader.onerror = () => {
160
+ setError('Failed to read file');
161
+ };
162
+ reader.readAsDataURL(file);
163
+ }, [alt]);
164
+ const handleDrop = useCallback((e) => {
165
+ e.preventDefault();
166
+ e.stopPropagation();
167
+ setDragOver(false);
168
+ const file = e.dataTransfer.files[0];
169
+ if (file) {
170
+ handleFileSelect(file);
171
+ }
172
+ }, [handleFileSelect]);
173
+ const handleDragOver = useCallback((e) => {
174
+ e.preventDefault();
175
+ e.stopPropagation();
176
+ setDragOver(true);
177
+ }, []);
178
+ const handleDragLeave = useCallback((e) => {
179
+ e.preventDefault();
180
+ e.stopPropagation();
181
+ setDragOver(false);
182
+ }, []);
183
+ const handleFileInputChange = useCallback((e) => {
184
+ const file = e.target.files?.[0];
185
+ if (file) {
186
+ handleFileSelect(file);
187
+ }
188
+ }, [handleFileSelect]);
189
+ const handleUrlChange = useCallback((e) => {
190
+ const newUrl = e.target.value;
191
+ setUrl(newUrl);
192
+ setError(null);
193
+ // SECURITY: Validate URL before showing preview
194
+ if (newUrl) {
195
+ const validation = validateImageUrl(newUrl);
196
+ if (validation.valid) {
197
+ // For local paths, we can't preview them directly in the modal
198
+ // (they need to be resolved through the asset context)
199
+ // For remote URLs, show the preview
200
+ if (!validation.isLocal) {
201
+ setPreviewUrl(newUrl);
202
+ }
203
+ else {
204
+ // Local path - no preview available, but it's valid
205
+ setPreviewUrl(null);
206
+ }
207
+ }
208
+ else {
209
+ setPreviewUrl(null);
210
+ // Don't show error while typing, only on submit
211
+ }
212
+ }
213
+ else {
214
+ setPreviewUrl(null);
215
+ }
216
+ }, []);
217
+ const handleInsert = useCallback(() => {
218
+ if (activeTab === 'url') {
219
+ if (!url) {
220
+ setError('Please enter an image URL or local path');
221
+ return;
222
+ }
223
+ // SECURITY: Validate URL before inserting
224
+ const validation = validateImageUrl(url);
225
+ if (!validation.valid) {
226
+ setError(validation.error || 'Invalid URL or path');
227
+ return;
228
+ }
229
+ onInsert({
230
+ src: url,
231
+ alt: alt || '',
232
+ isLocal: validation.isLocal || false,
233
+ });
234
+ }
235
+ else {
236
+ if (!selectedFileRef.current) {
237
+ setError('Please select an image');
238
+ return;
239
+ }
240
+ onInsert({
241
+ src: '', // Will be set after upload
242
+ alt: alt || '',
243
+ isLocal: true,
244
+ dataUri: selectedFileRef.current.dataUri,
245
+ });
246
+ }
247
+ onClose();
248
+ }, [activeTab, url, alt, onInsert, onClose]);
249
+ // Handle Enter key to submit
250
+ const handleKeyPress = useCallback((e) => {
251
+ if (e.key === 'Enter' && !e.shiftKey) {
252
+ e.preventDefault();
253
+ handleInsert();
254
+ }
255
+ }, [handleInsert]);
256
+ const handleTabChange = useCallback((tab) => {
257
+ setActiveTab(tab);
258
+ setError(null);
259
+ setPreviewUrl(null);
260
+ if (tab === 'url') {
261
+ selectedFileRef.current = null;
262
+ }
263
+ }, []);
264
+ if (!isOpen)
265
+ return null;
266
+ return (_jsx("div", { className: "image-modal-overlay", children: _jsxs("div", { className: "image-modal", ref: modalRef, onKeyDown: handleKeyPress, children: [_jsxs("div", { className: "image-modal-header", children: [_jsx("h3", { children: "Insert Image" }), _jsx("button", { className: "image-modal-close", onClick: onClose, "aria-label": "Close", children: "\u00D7" })] }), _jsxs("div", { className: "image-modal-tabs", children: [_jsx("button", { className: `image-modal-tab ${activeTab === 'upload' ? 'active' : ''}`, onClick: () => handleTabChange('upload'), children: "Upload" }), _jsx("button", { className: `image-modal-tab ${activeTab === 'url' ? 'active' : ''}`, onClick: () => handleTabChange('url'), children: "URL / Path" })] }), _jsxs("div", { className: "image-modal-content", children: [activeTab === 'upload' ? (_jsxs("div", { className: `image-modal-dropzone ${dragOver ? 'drag-over' : ''} ${previewUrl ? 'has-preview' : ''}`, onDrop: handleDrop, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onClick: () => fileInputRef.current?.click(), children: [previewUrl ? (_jsx("img", { src: previewUrl, alt: "Preview", className: "image-modal-preview" })) : (_jsxs("div", { className: "image-modal-dropzone-content", children: [_jsx("span", { className: "image-modal-dropzone-icon", children: "\uD83D\uDDBC" }), _jsx("span", { children: "Drop an image here or click to browse" }), _jsxs("span", { className: "image-modal-size-hint", children: ["Maximum size: ", MAX_IMAGE_SIZE / (1024 * 1024), "MB"] })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileInputChange, style: { display: 'none' } })] })) : (_jsxs("div", { className: "image-modal-url-section", children: [_jsx("input", { type: "text", className: "image-modal-input", placeholder: "assets/image.png or https://example.com/image.png", value: url, onChange: handleUrlChange, autoFocus: true }), previewUrl && (_jsx("div", { className: "image-modal-url-preview", children: _jsx("img", { src: previewUrl, alt: "Preview", className: "image-modal-preview", onError: () => setPreviewUrl(null) }) }))] })), _jsxs("div", { className: "image-modal-alt-section", children: [_jsx("label", { htmlFor: "image-alt", className: "image-modal-label", children: "Alt text (optional)" }), _jsx("input", { id: "image-alt", type: "text", className: "image-modal-input", placeholder: "Describe the image for accessibility", value: alt, onChange: (e) => setAlt(e.target.value) })] }), error && _jsx("div", { className: "image-modal-error", children: error })] }), _jsxs("div", { className: "image-modal-footer", children: [_jsx("button", { className: "image-modal-button secondary", onClick: onClose, children: "Cancel" }), _jsx("button", { className: "image-modal-button primary", onClick: handleInsert, children: "Insert" })] })] }) }));
267
+ }
@@ -0,0 +1,3 @@
1
+ import { LexicalCommand } from 'lexical';
2
+ export declare const INSERT_IMAGE_COMMAND: LexicalCommand<void>;
3
+ export declare function ImagePlugin(): import("react").JSX.Element;