@agent-native/toolkit 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/dist/composer/AgentComposerFrame.d.ts +19 -0
  16. package/dist/composer/AgentComposerFrame.d.ts.map +1 -0
  17. package/dist/composer/AgentComposerFrame.js +14 -0
  18. package/dist/composer/AgentComposerFrame.js.map +1 -0
  19. package/dist/composer/ComposerPlusMenu.d.ts +17 -0
  20. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -0
  21. package/dist/composer/ComposerPlusMenu.js +501 -0
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -0
  23. package/dist/composer/MentionPopover.d.ts +29 -0
  24. package/dist/composer/MentionPopover.d.ts.map +1 -0
  25. package/dist/composer/MentionPopover.js +180 -0
  26. package/dist/composer/MentionPopover.js.map +1 -0
  27. package/dist/composer/PastedTextChip.d.ts +9 -0
  28. package/dist/composer/PastedTextChip.d.ts.map +1 -0
  29. package/dist/composer/PastedTextChip.js +47 -0
  30. package/dist/composer/PastedTextChip.js.map +1 -0
  31. package/dist/composer/PromptComposer.d.ts +114 -0
  32. package/dist/composer/PromptComposer.d.ts.map +1 -0
  33. package/dist/composer/PromptComposer.js +302 -0
  34. package/dist/composer/PromptComposer.js.map +1 -0
  35. package/dist/composer/PromptComposer.spec.d.ts +2 -0
  36. package/dist/composer/PromptComposer.spec.d.ts.map +1 -0
  37. package/dist/composer/PromptComposer.spec.js +64 -0
  38. package/dist/composer/PromptComposer.spec.js.map +1 -0
  39. package/dist/composer/RealtimeVoiceMode.d.ts +104 -0
  40. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -0
  41. package/dist/composer/RealtimeVoiceMode.js +245 -0
  42. package/dist/composer/RealtimeVoiceMode.js.map +1 -0
  43. package/dist/composer/RealtimeVoiceMode.spec.d.ts +2 -0
  44. package/dist/composer/RealtimeVoiceMode.spec.d.ts.map +1 -0
  45. package/dist/composer/RealtimeVoiceMode.spec.js +439 -0
  46. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -0
  47. package/dist/composer/TiptapComposer.d.ts +221 -0
  48. package/dist/composer/TiptapComposer.d.ts.map +1 -0
  49. package/dist/composer/TiptapComposer.js +1838 -0
  50. package/dist/composer/TiptapComposer.js.map +1 -0
  51. package/dist/composer/TiptapComposer.spec.d.ts +2 -0
  52. package/dist/composer/TiptapComposer.spec.d.ts.map +1 -0
  53. package/dist/composer/TiptapComposer.spec.js +277 -0
  54. package/dist/composer/TiptapComposer.spec.js.map +1 -0
  55. package/dist/composer/VoiceButton.d.ts +25 -0
  56. package/dist/composer/VoiceButton.d.ts.map +1 -0
  57. package/dist/composer/VoiceButton.js +100 -0
  58. package/dist/composer/VoiceButton.js.map +1 -0
  59. package/dist/composer/VoiceButton.spec.d.ts +2 -0
  60. package/dist/composer/VoiceButton.spec.d.ts.map +1 -0
  61. package/dist/composer/VoiceButton.spec.js +19 -0
  62. package/dist/composer/VoiceButton.spec.js.map +1 -0
  63. package/dist/composer/asset-picker-url.d.ts +16 -0
  64. package/dist/composer/asset-picker-url.d.ts.map +1 -0
  65. package/dist/composer/asset-picker-url.js +64 -0
  66. package/dist/composer/asset-picker-url.js.map +1 -0
  67. package/dist/composer/asset-picker-url.spec.d.ts +2 -0
  68. package/dist/composer/asset-picker-url.spec.d.ts.map +1 -0
  69. package/dist/composer/asset-picker-url.spec.js +23 -0
  70. package/dist/composer/asset-picker-url.spec.js.map +1 -0
  71. package/dist/composer/attachment-accept.d.ts +9 -0
  72. package/dist/composer/attachment-accept.d.ts.map +1 -0
  73. package/dist/composer/attachment-accept.js +65 -0
  74. package/dist/composer/attachment-accept.js.map +1 -0
  75. package/dist/composer/attachment-accept.spec.d.ts +2 -0
  76. package/dist/composer/attachment-accept.spec.d.ts.map +1 -0
  77. package/dist/composer/attachment-accept.spec.js +15 -0
  78. package/dist/composer/attachment-accept.spec.js.map +1 -0
  79. package/dist/composer/draft-key.d.ts +2 -0
  80. package/dist/composer/draft-key.d.ts.map +1 -0
  81. package/dist/composer/draft-key.js +8 -0
  82. package/dist/composer/draft-key.js.map +1 -0
  83. package/dist/composer/draft-key.spec.d.ts +2 -0
  84. package/dist/composer/draft-key.spec.d.ts.map +1 -0
  85. package/dist/composer/draft-key.spec.js +13 -0
  86. package/dist/composer/draft-key.spec.js.map +1 -0
  87. package/dist/composer/extensions/FileReference.d.ts +3 -0
  88. package/dist/composer/extensions/FileReference.d.ts.map +1 -0
  89. package/dist/composer/extensions/FileReference.js +36 -0
  90. package/dist/composer/extensions/FileReference.js.map +1 -0
  91. package/dist/composer/extensions/MentionReference.d.ts +3 -0
  92. package/dist/composer/extensions/MentionReference.d.ts.map +1 -0
  93. package/dist/composer/extensions/MentionReference.js +66 -0
  94. package/dist/composer/extensions/MentionReference.js.map +1 -0
  95. package/dist/composer/extensions/SkillReference.d.ts +3 -0
  96. package/dist/composer/extensions/SkillReference.d.ts.map +1 -0
  97. package/dist/composer/extensions/SkillReference.js +40 -0
  98. package/dist/composer/extensions/SkillReference.js.map +1 -0
  99. package/dist/composer/index.d.ts +14 -0
  100. package/dist/composer/index.d.ts.map +1 -0
  101. package/dist/composer/index.js +13 -0
  102. package/dist/composer/index.js.map +1 -0
  103. package/dist/composer/pasted-text.d.ts +31 -0
  104. package/dist/composer/pasted-text.d.ts.map +1 -0
  105. package/dist/composer/pasted-text.js +130 -0
  106. package/dist/composer/pasted-text.js.map +1 -0
  107. package/dist/composer/pasted-text.spec.d.ts +2 -0
  108. package/dist/composer/pasted-text.spec.d.ts.map +1 -0
  109. package/dist/composer/pasted-text.spec.js +107 -0
  110. package/dist/composer/pasted-text.spec.js.map +1 -0
  111. package/dist/composer/prompt-attachments.d.ts +18 -0
  112. package/dist/composer/prompt-attachments.d.ts.map +1 -0
  113. package/dist/composer/prompt-attachments.js +65 -0
  114. package/dist/composer/prompt-attachments.js.map +1 -0
  115. package/dist/composer/prompt-attachments.spec.d.ts +2 -0
  116. package/dist/composer/prompt-attachments.spec.d.ts.map +1 -0
  117. package/dist/composer/prompt-attachments.spec.js +63 -0
  118. package/dist/composer/prompt-attachments.spec.js.map +1 -0
  119. package/dist/composer/realtime-voice-audio-level.d.ts +14 -0
  120. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -0
  121. package/dist/composer/realtime-voice-audio-level.js +47 -0
  122. package/dist/composer/realtime-voice-audio-level.js.map +1 -0
  123. package/dist/composer/realtime-voice-audio-level.spec.d.ts +2 -0
  124. package/dist/composer/realtime-voice-audio-level.spec.d.ts.map +1 -0
  125. package/dist/composer/realtime-voice-audio-level.spec.js +25 -0
  126. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -0
  127. package/dist/composer/realtime-voice-transcript.d.ts +26 -0
  128. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -0
  129. package/dist/composer/realtime-voice-transcript.js +103 -0
  130. package/dist/composer/realtime-voice-transcript.js.map +1 -0
  131. package/dist/composer/realtime-voice-transcript.spec.d.ts +2 -0
  132. package/dist/composer/realtime-voice-transcript.spec.d.ts.map +1 -0
  133. package/dist/composer/realtime-voice-transcript.spec.js +76 -0
  134. package/dist/composer/realtime-voice-transcript.spec.js.map +1 -0
  135. package/dist/composer/runtime-adapters.d.ts +163 -0
  136. package/dist/composer/runtime-adapters.d.ts.map +1 -0
  137. package/dist/composer/runtime-adapters.js +130 -0
  138. package/dist/composer/runtime-adapters.js.map +1 -0
  139. package/dist/composer/types.d.ts +60 -0
  140. package/dist/composer/types.d.ts.map +1 -0
  141. package/dist/composer/types.js +2 -0
  142. package/dist/composer/types.js.map +1 -0
  143. package/dist/composer/use-file-search.d.ts +6 -0
  144. package/dist/composer/use-file-search.d.ts.map +1 -0
  145. package/dist/composer/use-file-search.js +53 -0
  146. package/dist/composer/use-file-search.js.map +1 -0
  147. package/dist/composer/use-mention-search.d.ts +6 -0
  148. package/dist/composer/use-mention-search.d.ts.map +1 -0
  149. package/dist/composer/use-mention-search.js +75 -0
  150. package/dist/composer/use-mention-search.js.map +1 -0
  151. package/dist/composer/use-skills.d.ts +7 -0
  152. package/dist/composer/use-skills.d.ts.map +1 -0
  153. package/dist/composer/use-skills.js +40 -0
  154. package/dist/composer/use-skills.js.map +1 -0
  155. package/dist/composer/useRealtimeVoiceMode.d.ts +179 -0
  156. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -0
  157. package/dist/composer/useRealtimeVoiceMode.js +1657 -0
  158. package/dist/composer/useRealtimeVoiceMode.js.map +1 -0
  159. package/dist/composer/useRealtimeVoiceMode.spec.d.ts +2 -0
  160. package/dist/composer/useRealtimeVoiceMode.spec.d.ts.map +1 -0
  161. package/dist/composer/useRealtimeVoiceMode.spec.js +618 -0
  162. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -0
  163. package/dist/composer/useVoiceDictation.d.ts +57 -0
  164. package/dist/composer/useVoiceDictation.d.ts.map +1 -0
  165. package/dist/composer/useVoiceDictation.js +1004 -0
  166. package/dist/composer/useVoiceDictation.js.map +1 -0
  167. package/dist/composer/useVoiceDictation.spec.d.ts +2 -0
  168. package/dist/composer/useVoiceDictation.spec.d.ts.map +1 -0
  169. package/dist/composer/useVoiceDictation.spec.js +32 -0
  170. package/dist/composer/useVoiceDictation.spec.js.map +1 -0
  171. package/dist/editor/RegistryBlockContext.d.ts +68 -0
  172. package/dist/editor/RegistryBlockContext.d.ts.map +1 -0
  173. package/dist/editor/RegistryBlockContext.js +10 -0
  174. package/dist/editor/RegistryBlockContext.js.map +1 -0
  175. package/dist/editor/RegistryBlockNode.d.ts +66 -0
  176. package/dist/editor/RegistryBlockNode.d.ts.map +1 -0
  177. package/dist/editor/RegistryBlockNode.js +406 -0
  178. package/dist/editor/RegistryBlockNode.js.map +1 -0
  179. package/dist/editor/RegistryBlockNode.spec.d.ts +2 -0
  180. package/dist/editor/RegistryBlockNode.spec.d.ts.map +1 -0
  181. package/dist/editor/RegistryBlockNode.spec.js +215 -0
  182. package/dist/editor/RegistryBlockNode.spec.js.map +1 -0
  183. package/dist/editor/index.d.ts +2 -0
  184. package/dist/editor/index.d.ts.map +1 -1
  185. package/dist/editor/index.js +2 -0
  186. package/dist/editor/index.js.map +1 -1
  187. package/dist/editor/registrySlashCommands.d.ts +68 -0
  188. package/dist/editor/registrySlashCommands.d.ts.map +1 -0
  189. package/dist/editor/registrySlashCommands.js +51 -0
  190. package/dist/editor/registrySlashCommands.js.map +1 -0
  191. package/package.json +32 -1
  192. package/src/app-shell/header-actions.tsx +84 -0
  193. package/src/app-shell/index.ts +1 -0
  194. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  195. package/src/chat-history/ChatHistoryList.tsx +469 -0
  196. package/src/chat-history/index.ts +7 -0
  197. package/src/chat-history.css +314 -0
  198. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  199. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  200. package/src/collab-ui/PresenceBar.tsx +346 -0
  201. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  202. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  203. package/src/collab-ui/agent-identity.ts +16 -0
  204. package/src/collab-ui/index.ts +39 -0
  205. package/src/collab-ui/lead-client.ts +32 -0
  206. package/src/collab-ui/types.spec.ts +18 -0
  207. package/src/collab-ui/types.ts +96 -0
  208. package/src/composer/AgentComposerFrame.tsx +59 -0
  209. package/src/composer/ComposerPlusMenu.tsx +983 -0
  210. package/src/composer/MentionPopover.tsx +412 -0
  211. package/src/composer/PastedTextChip.tsx +150 -0
  212. package/src/composer/PromptComposer.spec.ts +73 -0
  213. package/src/composer/PromptComposer.tsx +718 -0
  214. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  215. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  216. package/src/composer/TiptapComposer.spec.ts +390 -0
  217. package/src/composer/TiptapComposer.tsx +2936 -0
  218. package/src/composer/VoiceButton.spec.ts +33 -0
  219. package/src/composer/VoiceButton.tsx +255 -0
  220. package/src/composer/asset-picker-url.spec.ts +56 -0
  221. package/src/composer/asset-picker-url.ts +80 -0
  222. package/src/composer/attachment-accept.spec.ts +22 -0
  223. package/src/composer/attachment-accept.ts +71 -0
  224. package/src/composer/draft-key.spec.ts +19 -0
  225. package/src/composer/draft-key.ts +7 -0
  226. package/src/composer/extensions/FileReference.tsx +55 -0
  227. package/src/composer/extensions/MentionReference.tsx +93 -0
  228. package/src/composer/extensions/SkillReference.tsx +56 -0
  229. package/src/composer/index.ts +79 -0
  230. package/src/composer/pasted-text.spec.ts +162 -0
  231. package/src/composer/pasted-text.ts +160 -0
  232. package/src/composer/prompt-attachments.spec.ts +80 -0
  233. package/src/composer/prompt-attachments.ts +93 -0
  234. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  235. package/src/composer/realtime-voice-audio-level.ts +64 -0
  236. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  237. package/src/composer/realtime-voice-transcript.ts +134 -0
  238. package/src/composer/runtime-adapters.tsx +343 -0
  239. package/src/composer/types.ts +66 -0
  240. package/src/composer/use-file-search.ts +64 -0
  241. package/src/composer/use-mention-search.ts +90 -0
  242. package/src/composer/use-skills.ts +45 -0
  243. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  244. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  245. package/src/composer/useVoiceDictation.spec.ts +45 -0
  246. package/src/composer/useVoiceDictation.ts +1191 -0
  247. package/src/context-ui/ContextMeter.tsx +101 -0
  248. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  249. package/src/context-ui/ContextTreemap.tsx +125 -0
  250. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  251. package/src/context-ui/format.ts +42 -0
  252. package/src/context-ui/index.ts +15 -0
  253. package/src/context-ui/types.ts +43 -0
  254. package/src/design-tweaks/index.ts +1 -0
  255. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  256. package/src/editor/BubbleToolbar.tsx +253 -0
  257. package/src/editor/CodeBlockNode.tsx +254 -0
  258. package/src/editor/DragHandle.spec.ts +588 -0
  259. package/src/editor/DragHandle.ts +1437 -0
  260. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  261. package/src/editor/ImageExtension.ts +306 -0
  262. package/src/editor/RegistryBlockContext.tsx +107 -0
  263. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  264. package/src/editor/RegistryBlockNode.tsx +652 -0
  265. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  266. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  267. package/src/editor/RichMarkdownEditor.tsx +81 -0
  268. package/src/editor/RunId.ts +61 -0
  269. package/src/editor/SharedRichEditor.tsx +321 -0
  270. package/src/editor/SlashCommandMenu.tsx +310 -0
  271. package/src/editor/disableHistory.spec.ts +85 -0
  272. package/src/editor/extensions.ts +402 -0
  273. package/src/editor/gfmDoc.spec.ts +230 -0
  274. package/src/editor/gfmDoc.ts +92 -0
  275. package/src/editor/index.ts +93 -0
  276. package/src/editor/registrySlashCommands.ts +131 -0
  277. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  278. package/src/editor/surgical-apply.spec.ts +279 -0
  279. package/src/editor/surgical-apply.ts +192 -0
  280. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  281. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  282. package/src/editor/useCollabReconcile.ts +589 -0
  283. package/src/editor.css +510 -0
  284. package/src/hooks/index.ts +2 -0
  285. package/src/hooks/use-mobile.tsx +21 -0
  286. package/src/hooks/use-toast.ts +188 -0
  287. package/src/index.ts +8 -0
  288. package/src/onboarding/index.ts +1 -0
  289. package/src/provider.tsx +47 -0
  290. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  291. package/src/sharing/VisibilityBadge.tsx +37 -0
  292. package/src/sharing/index.ts +4 -0
  293. package/src/styles.css +21 -0
  294. package/src/ui/accordion.tsx +56 -0
  295. package/src/ui/alert-dialog.tsx +141 -0
  296. package/src/ui/alert.tsx +59 -0
  297. package/src/ui/aspect-ratio.tsx +5 -0
  298. package/src/ui/avatar.tsx +48 -0
  299. package/src/ui/badge.tsx +37 -0
  300. package/src/ui/breadcrumb.tsx +115 -0
  301. package/src/ui/button.tsx +78 -0
  302. package/src/ui/calendar.tsx +214 -0
  303. package/src/ui/card.tsx +86 -0
  304. package/src/ui/carousel.tsx +260 -0
  305. package/src/ui/chart.tsx +375 -0
  306. package/src/ui/checkbox.tsx +28 -0
  307. package/src/ui/collapsible.tsx +9 -0
  308. package/src/ui/command.spec.tsx +51 -0
  309. package/src/ui/command.tsx +174 -0
  310. package/src/ui/context-menu.tsx +202 -0
  311. package/src/ui/date-picker.tsx +64 -0
  312. package/src/ui/dialog.spec.tsx +43 -0
  313. package/src/ui/dialog.tsx +146 -0
  314. package/src/ui/drawer.tsx +126 -0
  315. package/src/ui/dropdown-menu.tsx +218 -0
  316. package/src/ui/form.tsx +177 -0
  317. package/src/ui/hover-card.tsx +27 -0
  318. package/src/ui/index.ts +52 -0
  319. package/src/ui/input-otp.tsx +69 -0
  320. package/src/ui/input.tsx +22 -0
  321. package/src/ui/label.tsx +24 -0
  322. package/src/ui/logo.tsx +36 -0
  323. package/src/ui/menubar.tsx +235 -0
  324. package/src/ui/navigation-menu.tsx +128 -0
  325. package/src/ui/pagination.tsx +121 -0
  326. package/src/ui/popover.tsx +44 -0
  327. package/src/ui/progress.tsx +26 -0
  328. package/src/ui/radio-group.tsx +42 -0
  329. package/src/ui/resizable.tsx +43 -0
  330. package/src/ui/scroll-area.tsx +46 -0
  331. package/src/ui/select.tsx +158 -0
  332. package/src/ui/separator.tsx +29 -0
  333. package/src/ui/sheet.tsx +160 -0
  334. package/src/ui/sidebar.tsx +777 -0
  335. package/src/ui/skeleton.tsx +18 -0
  336. package/src/ui/slider.tsx +26 -0
  337. package/src/ui/sonner.tsx +58 -0
  338. package/src/ui/spinner.tsx +17 -0
  339. package/src/ui/switch.tsx +27 -0
  340. package/src/ui/table.tsx +117 -0
  341. package/src/ui/tabs.tsx +53 -0
  342. package/src/ui/textarea.tsx +23 -0
  343. package/src/ui/toast.tsx +127 -0
  344. package/src/ui/toaster.tsx +33 -0
  345. package/src/ui/toggle-group.tsx +59 -0
  346. package/src/ui/toggle.tsx +43 -0
  347. package/src/ui/tooltip.tsx +53 -0
  348. package/src/ui/use-toast.ts +3 -0
  349. package/src/utils.ts +8 -0
@@ -0,0 +1,321 @@
1
+ import type { Extension, Node, Mark } from "@tiptap/core";
2
+ import { EditorContent, useEditor } from "@tiptap/react";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import type { Awareness } from "y-protocols/awareness";
5
+ import type { Doc as YDoc } from "yjs";
6
+
7
+ import { cn } from "../utils.js";
8
+ import { BubbleToolbar, type BubbleToolbarItem } from "./BubbleToolbar.js";
9
+ import {
10
+ createSharedEditorExtensions,
11
+ type RichMarkdownDialect,
12
+ type RichMarkdownEditorPreset,
13
+ type RichMarkdownCollabUser,
14
+ type SharedEditorFeatures,
15
+ } from "./extensions.js";
16
+ import type { ImageUploadFn } from "./ImageExtension.js";
17
+ import { SlashCommandMenu, type SlashCommandItem } from "./SlashCommandMenu.js";
18
+ import {
19
+ useCollabReconcile,
20
+ getEditorMarkdown,
21
+ type UseCollabReconcileResult,
22
+ type UseCollabReconcileOptions,
23
+ } from "./useCollabReconcile.js";
24
+
25
+ export interface SharedRichEditorProps {
26
+ value: string;
27
+ onChange: (markdown: string) => void;
28
+ onBlur?: () => void;
29
+ contentUpdatedAt?: string | null;
30
+ editable?: boolean;
31
+ dialect?: RichMarkdownDialect;
32
+ preset?: RichMarkdownEditorPreset;
33
+ /** Toggle individual base extensions (tables/tasks/link/codeBlock/image). */
34
+ features?: SharedEditorFeatures;
35
+ /**
36
+ * Injectable image uploader for the shared image block. Used only when
37
+ * `features.image` is on. Pass `uploadEditorImage` (the framework
38
+ * `upload-image` action) for a real uploading image block.
39
+ */
40
+ onImageUpload?: ImageUploadFn | null;
41
+ /**
42
+ * App-specific extensions (Notion nodes, media, drag handles, comment
43
+ * anchors, …) appended after the shared base schema.
44
+ */
45
+ extraExtensions?: Array<Extension | Node | Mark>;
46
+ placeholder?: string;
47
+ className?: string;
48
+ editorClassName?: string;
49
+ ariaLabel?: string;
50
+ interactive?: boolean;
51
+ /**
52
+ * Yjs document for real-time multi-user editing. When provided, prose is
53
+ * authored against the shared Y.Doc and mirrored back to `value` as markdown
54
+ * (still the source of truth). When omitted, the editor is a plain controlled
55
+ * `value`/`onChange` editor — the existing, non-collaborative behavior.
56
+ */
57
+ ydoc?: YDoc | null;
58
+ /** True after the collab provider has loaded persisted Y.Doc state. */
59
+ collabSynced?: boolean;
60
+ /** Shared awareness instance for live cursors/presence. */
61
+ awareness?: Awareness | null;
62
+ /** Current user info for the collaborative cursor label. */
63
+ user?: RichMarkdownCollabUser | null;
64
+ /**
65
+ * Disable StarterKit's built-in undo/redo for a controlled (non-collab)
66
+ * editor whose host owns its own undo authority (see
67
+ * {@link CreateSharedEditorExtensionsOptions.disableHistory}). Ignored when a
68
+ * `ydoc` is present (Yjs always owns history then). Default `false`, so every
69
+ * existing embedder is unchanged.
70
+ */
71
+ disableHistory?: boolean;
72
+ /** Override the slash-menu block command list. */
73
+ slashItems?: SlashCommandItem[];
74
+ /** Override the bubble-toolbar item builder. */
75
+ buildBubbleItems?: (
76
+ editor: import("@tiptap/react").Editor,
77
+ toggleLink: () => void,
78
+ ) => BubbleToolbarItem[];
79
+ /**
80
+ * Override how the editor's content is read into the canonical `value`.
81
+ * Defaults to the tiptap-markdown storage reader. Apps with a custom on-disk
82
+ * format (Content's NFM, the plan's `blocks[]` JSON) pass their serializer so
83
+ * seed/reconcile/onChange all speak the same value space.
84
+ */
85
+ getMarkdown?: (editor: import("@tiptap/react").Editor) => string;
86
+ /** Override how the canonical `value` is applied into the editor (seed + reconcile). */
87
+ setContent?: (
88
+ editor: import("@tiptap/react").Editor,
89
+ value: string,
90
+ options: { emitUpdate?: boolean; addToHistory?: boolean },
91
+ ) => void;
92
+ /**
93
+ * Optional parser for surgical reconcile. Custom value formats that already
94
+ * handle surgical writes inside `setContent` can pass `false` so the default
95
+ * markdown parser never treats their source bytes as literal markdown.
96
+ */
97
+ parseValue?: UseCollabReconcileOptions["parseValue"];
98
+ /** Canonicalize `value` for the echo / already-in-sync equality checks. */
99
+ normalizeValue?: (value: string) => string;
100
+ /** Override the empty-doc seed predicate (see {@link useCollabReconcile}). */
101
+ shouldSeed?: (info: {
102
+ value: string;
103
+ currentMarkdown: string;
104
+ fragmentLength: number;
105
+ }) => boolean;
106
+ /** Initial "applied" watermark (see {@link useCollabReconcile}). */
107
+ initialAppliedUpdatedAt?: string | null;
108
+ /** Extra class on the editor wrapper (e.g. a drag-handle `wrapperSelector` hook). */
109
+ wrapperClassName?: string;
110
+ /**
111
+ * Fires once the editor instance exists. Lets a host capture the root
112
+ * `editor.view` — e.g. to repaint the WHOLE document after a structural block
113
+ * move (a column emptied and its container dissolved) that a surgical
114
+ * per-region patch can't express, since the change is in the root doc itself.
115
+ */
116
+ onEditorReady?: (editor: import("@tiptap/react").Editor) => void;
117
+ }
118
+
119
+ /**
120
+ * The single shared rich markdown editor surface. Combines
121
+ * {@link createSharedEditorExtensions} (schema + dialect-keyed markdown +
122
+ * optional collab + app extras), {@link useCollabReconcile} (seed / reconcile /
123
+ * lead-client logic), and the shared {@link SlashCommandMenu} +
124
+ * {@link BubbleToolbar}.
125
+ *
126
+ * With no `ydoc` it is a controlled `value`/`onChange` single-user editor.
127
+ * With a `ydoc` it binds the framework collaboration stack; markdown stays the
128
+ * canonical saved representation while the Y.Doc is transient live state.
129
+ */
130
+ export function SharedRichEditor({
131
+ value,
132
+ onChange,
133
+ onBlur,
134
+ contentUpdatedAt,
135
+ editable = true,
136
+ dialect = "gfm",
137
+ preset = "plan",
138
+ features,
139
+ onImageUpload = null,
140
+ extraExtensions,
141
+ placeholder = "Type '/' for commands...",
142
+ className,
143
+ editorClassName,
144
+ ariaLabel,
145
+ interactive = editable,
146
+ ydoc = null,
147
+ collabSynced = true,
148
+ awareness = null,
149
+ user = null,
150
+ disableHistory = false,
151
+ slashItems,
152
+ buildBubbleItems,
153
+ getMarkdown,
154
+ setContent,
155
+ parseValue,
156
+ normalizeValue,
157
+ shouldSeed,
158
+ initialAppliedUpdatedAt,
159
+ wrapperClassName,
160
+ onEditorReady,
161
+ }: SharedRichEditorProps) {
162
+ const readMarkdown = getMarkdown ?? getEditorMarkdown;
163
+ const onChangeRef = useRef(onChange);
164
+ const onBlurRef = useRef(onBlur);
165
+ onChangeRef.current = onChange;
166
+ onBlurRef.current = onBlur;
167
+
168
+ const extensions = useMemo(
169
+ () =>
170
+ createSharedEditorExtensions({
171
+ dialect,
172
+ preset,
173
+ placeholder,
174
+ features,
175
+ extraExtensions,
176
+ onImageUpload,
177
+ collab: ydoc ? { ydoc, awareness, user } : null,
178
+ disableHistory,
179
+ }),
180
+ // `preset` is retained in the dependency list so future preset-specific
181
+ // schema branches re-create the editor; it is currently schema-neutral. The
182
+ // collab inputs are identity-stable per block (one Y.Doc per docId), so they
183
+ // only change on a genuine doc switch — exactly when the editor must
184
+ // re-create to rebind Collaboration.
185
+ [
186
+ dialect,
187
+ placeholder,
188
+ preset,
189
+ features,
190
+ extraExtensions,
191
+ onImageUpload,
192
+ ydoc,
193
+ awareness,
194
+ user?.name,
195
+ user?.email,
196
+ user?.color,
197
+ disableHistory,
198
+ ],
199
+ );
200
+
201
+ const collab = !!ydoc;
202
+
203
+ // The collab hook needs the editor, but useEditor's `onUpdate` needs the
204
+ // hook's guards. Break the cycle with a ref: `onUpdate` reads the guards
205
+ // through `guardsRef`, which is populated right after the hook runs below.
206
+ // `onUpdate` only ever fires after the editor exists, by which point the ref
207
+ // holds the real guards.
208
+ const guardsRef = useRef<UseCollabReconcileResult | null>(null);
209
+
210
+ const editor = useEditor(
211
+ {
212
+ extensions,
213
+ // With Collaboration active the prose is owned by the shared Y.XmlFragment.
214
+ // Seeding `content` here too would make the editor initialize from BOTH the
215
+ // prop and the Y.Doc, firing a spurious initial update that could autosave a
216
+ // stale value over newer SQL. The lead-client seed effect populates an empty
217
+ // doc instead. Non-collab editors keep initializing from `value`.
218
+ // With Collaboration the Y.Doc owns the prose (seeded by the lead client).
219
+ // With a custom `setContent` the reconcile seeds the editor too (the raw
220
+ // `value` is not directly settable content — e.g. the plan's blocks JSON),
221
+ // so only the plain markdown path seeds from `value` here.
222
+ content: collab || setContent ? null : value,
223
+ editable,
224
+ editorProps: {
225
+ attributes: {
226
+ class: cn("an-rich-md-prose", editorClassName),
227
+ ...(ariaLabel ? { role: "textbox", "aria-label": ariaLabel } : {}),
228
+ },
229
+ },
230
+ onUpdate: ({ editor, transaction }) => {
231
+ const guards = guardsRef.current;
232
+ if (!guards || guards.shouldIgnoreUpdate(transaction)) return;
233
+ try {
234
+ const markdown = readMarkdown(editor);
235
+ if (!guards.registerEmitted(markdown)) return;
236
+ queueMicrotask(() => onChangeRef.current(markdown));
237
+ } catch (error) {
238
+ console.error("Markdown serialization error:", error);
239
+ }
240
+ },
241
+ onBlur: () => {
242
+ onBlurRef.current?.();
243
+ },
244
+ // Recreate the editor when the shared Y.Doc identity changes — including
245
+ // null → doc. Extensions are baked in at creation, so a ydoc that arrives
246
+ // AFTER the first mount (fresh page load: the session/user resolves after
247
+ // the editor mounts) would otherwise never bind Collaboration: the editor
248
+ // looks fine but produces no Yjs updates and never receives peers' edits.
249
+ },
250
+ [ydoc],
251
+ );
252
+
253
+ const collabState = useCollabReconcile({
254
+ editor,
255
+ ydoc,
256
+ collabSynced,
257
+ awareness,
258
+ value,
259
+ contentUpdatedAt,
260
+ editable,
261
+ getMarkdown: readMarkdown,
262
+ setContent,
263
+ parseValue,
264
+ normalizeValue,
265
+ shouldSeed,
266
+ initialAppliedUpdatedAt,
267
+ });
268
+ guardsRef.current = collabState;
269
+
270
+ useEffect(() => {
271
+ if (!editor || editor.isDestroyed) return;
272
+ editor.setEditable(editable);
273
+ }, [editable, editor]);
274
+
275
+ useEffect(() => {
276
+ if (editor) onEditorReady?.(editor);
277
+ }, [editor, onEditorReady]);
278
+
279
+ useEffect(() => () => editor?.destroy(), [editor]);
280
+
281
+ const handleWrapperClick = (event: React.MouseEvent<HTMLDivElement>) => {
282
+ if (!editable || !editor || editor.isDestroyed) return;
283
+ const target = event.target as HTMLElement;
284
+ if (
285
+ target.classList.contains("an-rich-md-wrapper") ||
286
+ target.classList.contains("an-rich-md-clickable")
287
+ ) {
288
+ editor.chain().focus("end").run();
289
+ }
290
+ };
291
+
292
+ if (!editor) {
293
+ return (
294
+ <div
295
+ className={cn("an-rich-md-wrapper an-rich-md-loading", className)}
296
+ data-plan-interactive={interactive ? true : undefined}
297
+ />
298
+ );
299
+ }
300
+
301
+ return (
302
+ <div
303
+ className={cn(
304
+ "an-rich-md-wrapper an-rich-md-clickable",
305
+ !editable && "an-rich-md-wrapper--readonly",
306
+ wrapperClassName,
307
+ className,
308
+ )}
309
+ onClick={handleWrapperClick}
310
+ data-plan-interactive={interactive ? true : undefined}
311
+ >
312
+ {editable ? (
313
+ <BubbleToolbar editor={editor} buildItems={buildBubbleItems} />
314
+ ) : null}
315
+ {editable ? (
316
+ <SlashCommandMenu editor={editor} items={slashItems} />
317
+ ) : null}
318
+ <EditorContent editor={editor} />
319
+ </div>
320
+ );
321
+ }
@@ -0,0 +1,310 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type CSSProperties,
9
+ } from "react";
10
+
11
+ import { cn } from "../utils.js";
12
+ import { pickAndInsertImage, type ImageUploadFn } from "./ImageExtension.js";
13
+
14
+ /** A single slash-menu block command. Apps can extend the default list. */
15
+ export interface SlashCommandItem {
16
+ title: string;
17
+ description: string;
18
+ /**
19
+ * Optional hidden search text. Use this for raw block types and aliases that
20
+ * should match slash queries without making the visible description verbose.
21
+ */
22
+ searchText?: string;
23
+ /** Short text glyph shown in the menu (T, H1, tbl, …). */
24
+ icon: string;
25
+ action: (editor: Editor) => void;
26
+ }
27
+
28
+ /**
29
+ * The default block commands — Plan's current set. Apps pass their own `items`
30
+ * (typically `[...DEFAULT_SLASH_COMMANDS, ...extra]`) to extend it.
31
+ */
32
+ export const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [
33
+ {
34
+ title: "Text",
35
+ description: "Plain text block",
36
+ icon: "T",
37
+ action: (editor) => editor.chain().focus().setParagraph().run(),
38
+ },
39
+ {
40
+ title: "Heading 1",
41
+ description: "Large heading",
42
+ icon: "H1",
43
+ action: (editor) =>
44
+ editor.chain().focus().toggleHeading({ level: 1 }).run(),
45
+ },
46
+ {
47
+ title: "Heading 2",
48
+ description: "Section heading",
49
+ icon: "H2",
50
+ action: (editor) =>
51
+ editor.chain().focus().toggleHeading({ level: 2 }).run(),
52
+ },
53
+ {
54
+ title: "Heading 3",
55
+ description: "Subheading",
56
+ icon: "H3",
57
+ action: (editor) =>
58
+ editor.chain().focus().toggleHeading({ level: 3 }).run(),
59
+ },
60
+ {
61
+ title: "Bulleted list",
62
+ description: "Unordered list",
63
+ icon: "-",
64
+ action: (editor) => editor.chain().focus().toggleBulletList().run(),
65
+ },
66
+ {
67
+ title: "Numbered list",
68
+ description: "Ordered list",
69
+ icon: "1.",
70
+ action: (editor) => editor.chain().focus().toggleOrderedList().run(),
71
+ },
72
+ {
73
+ title: "To-do list",
74
+ description: "Checklist items",
75
+ icon: "[]",
76
+ action: (editor) => editor.chain().focus().toggleTaskList().run(),
77
+ },
78
+ {
79
+ title: "Quote",
80
+ description: "Block quote",
81
+ icon: '"',
82
+ action: (editor) => editor.chain().focus().toggleBlockquote().run(),
83
+ },
84
+ {
85
+ title: "Code block",
86
+ description: "Code snippet",
87
+ icon: "<>",
88
+ action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
89
+ },
90
+ {
91
+ title: "Divider",
92
+ description: "Horizontal rule",
93
+ icon: "-",
94
+ action: (editor) => editor.chain().focus().setHorizontalRule().run(),
95
+ },
96
+ {
97
+ title: "Table",
98
+ description: "Three by three table",
99
+ icon: "tbl",
100
+ action: (editor) =>
101
+ editor
102
+ .chain()
103
+ .focus()
104
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
105
+ .run(),
106
+ },
107
+ ];
108
+
109
+ /**
110
+ * Build the `/image` slash command for the shared image block. Requires the
111
+ * editor to mount the shared image extension (`features.image`) and an
112
+ * {@link ImageUploadFn}; the command opens a native file picker and uploads +
113
+ * inserts the chosen image(s). Add it to the list an app passes to
114
+ * {@link SlashCommandMenu} (e.g. `[...DEFAULT_SLASH_COMMANDS, createImageSlashCommand(upload)]`).
115
+ */
116
+ export function createImageSlashCommand(
117
+ upload: ImageUploadFn,
118
+ ): SlashCommandItem {
119
+ return {
120
+ title: "Image",
121
+ description: "Upload an image",
122
+ icon: "img",
123
+ action: (editor) => pickAndInsertImage(editor.view, upload),
124
+ };
125
+ }
126
+
127
+ export interface SlashCommandMenuProps {
128
+ editor: Editor;
129
+ /** Block command list. Defaults to {@link DEFAULT_SLASH_COMMANDS}. */
130
+ items?: SlashCommandItem[];
131
+ }
132
+
133
+ /**
134
+ * The shared "/" block-insert menu. Detects a `/query` at the caret, filters the
135
+ * provided command list, and renders a fixed-position picker with keyboard
136
+ * navigation. Extracted from the inline plan menu so apps share one
137
+ * implementation and only swap the command list.
138
+ */
139
+ export function SlashCommandMenu({
140
+ editor,
141
+ items = DEFAULT_SLASH_COMMANDS,
142
+ }: SlashCommandMenuProps) {
143
+ const [isOpen, setIsOpen] = useState(false);
144
+ const [query, setQuery] = useState("");
145
+ const [selectedIndex, setSelectedIndex] = useState(0);
146
+ const [position, setPosition] = useState<{
147
+ top: number;
148
+ left: number;
149
+ flipUp: boolean;
150
+ } | null>(null);
151
+ const menuRef = useRef<HTMLDivElement>(null);
152
+ const selectedItemRef = useRef<HTMLButtonElement>(null);
153
+ const slashPosRef = useRef<number | null>(null);
154
+
155
+ const filteredCommands = useMemo(() => {
156
+ const normalizedQuery = query.toLowerCase();
157
+ return items.filter(
158
+ (cmd) =>
159
+ cmd.title.toLowerCase().includes(normalizedQuery) ||
160
+ cmd.description.toLowerCase().includes(normalizedQuery) ||
161
+ cmd.searchText?.toLowerCase().includes(normalizedQuery),
162
+ );
163
+ }, [items, query]);
164
+
165
+ const close = useCallback(() => {
166
+ setIsOpen(false);
167
+ setQuery("");
168
+ slashPosRef.current = null;
169
+ }, []);
170
+
171
+ const executeCommand = useCallback(
172
+ (command: SlashCommandItem) => {
173
+ if (slashPosRef.current !== null) {
174
+ const { from } = editor.state.selection;
175
+ editor
176
+ .chain()
177
+ .focus()
178
+ .deleteRange({ from: slashPosRef.current, to: from })
179
+ .run();
180
+ }
181
+ command.action(editor);
182
+ close();
183
+ },
184
+ [close, editor],
185
+ );
186
+
187
+ useEffect(() => {
188
+ const handleKeyDown = (event: KeyboardEvent) => {
189
+ if (!isOpen) return;
190
+ if (event.key === "ArrowDown") {
191
+ event.preventDefault();
192
+ if (filteredCommands.length === 0) return;
193
+ setSelectedIndex((index) => (index + 1) % filteredCommands.length);
194
+ } else if (event.key === "ArrowUp") {
195
+ event.preventDefault();
196
+ if (filteredCommands.length === 0) return;
197
+ setSelectedIndex(
198
+ (index) =>
199
+ (index - 1 + filteredCommands.length) % filteredCommands.length,
200
+ );
201
+ } else if (event.key === "Enter") {
202
+ event.preventDefault();
203
+ const command = filteredCommands[selectedIndex];
204
+ if (command) executeCommand(command);
205
+ } else if (event.key === "Escape") {
206
+ close();
207
+ }
208
+ };
209
+ document.addEventListener("keydown", handleKeyDown, true);
210
+ return () => document.removeEventListener("keydown", handleKeyDown, true);
211
+ }, [close, executeCommand, filteredCommands, isOpen, selectedIndex]);
212
+
213
+ useEffect(() => {
214
+ if (!isOpen) return;
215
+ const menu = menuRef.current;
216
+ const item = selectedItemRef.current;
217
+ if (!menu || !item) return;
218
+
219
+ const itemTop = item.offsetTop;
220
+ const itemBottom = itemTop + item.offsetHeight;
221
+ const visibleTop = menu.scrollTop;
222
+ const visibleBottom = visibleTop + menu.clientHeight;
223
+
224
+ if (itemTop < visibleTop) {
225
+ menu.scrollTop = itemTop;
226
+ } else if (itemBottom > visibleBottom) {
227
+ menu.scrollTop = itemBottom - menu.clientHeight;
228
+ }
229
+ }, [filteredCommands.length, isOpen, selectedIndex]);
230
+
231
+ useEffect(() => {
232
+ const handleTransaction = () => {
233
+ const { state } = editor;
234
+ const { from } = state.selection;
235
+ const textBefore = state.doc.textBetween(
236
+ Math.max(0, from - 32),
237
+ from,
238
+ "\n",
239
+ );
240
+ const slashMatch = textBefore.match(/\/([a-zA-Z0-9 ]*)$/);
241
+ if (!slashMatch) {
242
+ if (isOpen) close();
243
+ return;
244
+ }
245
+
246
+ const slashStart = from - slashMatch[0].length;
247
+ slashPosRef.current = slashStart;
248
+ setQuery(slashMatch[1]);
249
+ setSelectedIndex(0);
250
+ const coords = editor.view.coordsAtPos(from);
251
+ const menuHeight = 320;
252
+ const spaceBelow = window.innerHeight - coords.bottom;
253
+ const flipUp = spaceBelow < menuHeight && coords.top > menuHeight;
254
+ setPosition({
255
+ top: flipUp ? coords.top : coords.bottom + 4,
256
+ left: Math.min(coords.left, window.innerWidth - 250),
257
+ flipUp,
258
+ });
259
+ setIsOpen(true);
260
+ };
261
+
262
+ editor.on("transaction", handleTransaction);
263
+ return () => {
264
+ editor.off("transaction", handleTransaction);
265
+ };
266
+ }, [close, editor, isOpen]);
267
+
268
+ if (!isOpen || !position || filteredCommands.length === 0) return null;
269
+
270
+ return (
271
+ <div
272
+ ref={menuRef}
273
+ className="an-rich-md-slash-menu"
274
+ style={
275
+ {
276
+ position: "fixed",
277
+ ...(position.flipUp
278
+ ? { bottom: window.innerHeight - position.top + 4 }
279
+ : { top: position.top }),
280
+ left: position.left,
281
+ } as CSSProperties
282
+ }
283
+ data-plan-interactive
284
+ >
285
+ <div className="an-rich-md-slash-heading">Blocks</div>
286
+ {filteredCommands.map((command, index) => (
287
+ <button
288
+ key={command.title}
289
+ ref={index === selectedIndex ? selectedItemRef : undefined}
290
+ type="button"
291
+ className={cn(
292
+ "an-rich-md-slash-item",
293
+ index === selectedIndex && "an-rich-md-slash-item--active",
294
+ )}
295
+ onMouseEnter={() => setSelectedIndex(index)}
296
+ onMouseDown={(event) => event.preventDefault()}
297
+ onClick={() => executeCommand(command)}
298
+ >
299
+ <span className="an-rich-md-slash-icon">{command.icon}</span>
300
+ <span className="an-rich-md-slash-copy">
301
+ <span className="an-rich-md-slash-title">{command.title}</span>
302
+ <span className="an-rich-md-slash-description">
303
+ {command.description}
304
+ </span>
305
+ </span>
306
+ </button>
307
+ ))}
308
+ </div>
309
+ );
310
+ }