@ecsplico/editor 3.1.0 → 3.2.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 (270) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/adapters/direct-ai.d.ts +36 -0
  3. package/dist/adapters/direct-ai.js +169 -0
  4. package/dist/adapters/direct-ai.js.map +1 -0
  5. package/dist/adapters/memory.js.map +1 -1
  6. package/dist/code/CodeSurface.vue.d.ts +2 -2
  7. package/dist/core/block-drag.js +3 -0
  8. package/dist/core/block-drag.js.map +1 -1
  9. package/dist/core/block-transfer.d.ts +44 -0
  10. package/dist/core/block-transfer.js +68 -0
  11. package/dist/core/block-transfer.js.map +1 -0
  12. package/dist/core/commands.js +5 -0
  13. package/dist/core/commands.js.map +1 -1
  14. package/dist/core/core-feature.d.ts +4 -1
  15. package/dist/core/core-feature.js +8 -3
  16. package/dist/core/core-feature.js.map +1 -1
  17. package/dist/core/editor-ui.d.ts +3 -1
  18. package/dist/core/editor-ui.js.map +1 -1
  19. package/dist/core/extension-points.d.ts +17 -1
  20. package/dist/core/feature.d.ts +6 -1
  21. package/dist/core/feature.js.map +1 -1
  22. package/dist/core/index.d.ts +4 -2
  23. package/dist/core/index.js +3 -2
  24. package/dist/core/schema/index.d.ts +24 -3
  25. package/dist/core/schema/index.js +21 -4
  26. package/dist/core/schema/index.js.map +1 -1
  27. package/dist/core/surface-session.js +3 -1
  28. package/dist/core/surface-session.js.map +1 -1
  29. package/dist/core/toolbar-items.js +1 -1
  30. package/dist/core/toolbar-items.js.map +1 -1
  31. package/dist/document/DocumentSurface.vue.d.ts +2 -2
  32. package/dist/features/ai/agent-op-review.js +1 -1
  33. package/dist/features/ai/agent-ops.js +2 -2
  34. package/dist/features/ai/ai-apply.js +1 -1
  35. package/dist/features/ai/assistant.js +1 -1
  36. package/dist/features/ai/components/AiSelectionMenu.js +2 -2
  37. package/dist/features/ai/components/AiSidebar.js +2 -2
  38. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  39. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  40. package/dist/features/ai/document-chat-model.js +1 -1
  41. package/dist/features/ai/document-chat.js +2 -2
  42. package/dist/features/ai/ghost-autocomplete.js +1 -1
  43. package/dist/features/ai/index.d.ts +4 -3
  44. package/dist/features/ai/index.js +6 -5
  45. package/dist/features/ai/protocol/complete.d.ts +19 -0
  46. package/dist/features/ai/protocol/complete.js +27 -1
  47. package/dist/features/ai/protocol/complete.js.map +1 -1
  48. package/dist/features/ai/protocol/generate.d.ts +95 -0
  49. package/dist/features/ai/protocol/generate.js +58 -0
  50. package/dist/features/ai/protocol/generate.js.map +1 -0
  51. package/dist/features/ai/protocol/index.d.ts +4 -3
  52. package/dist/features/ai/protocol/index.js +6 -5
  53. package/dist/features/ai/protocol/tasks.d.ts +3 -1
  54. package/dist/features/ai/protocol/tasks.js +74 -2
  55. package/dist/features/ai/protocol/tasks.js.map +1 -1
  56. package/dist/features/ai/react/AiSelectionMenu.js +2 -2
  57. package/dist/features/ai/react/AiSidebar.js +2 -2
  58. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  59. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  60. package/dist/features/ai/selection-flow.js +1 -1
  61. package/dist/features/ai/sidebar-flow.js +1 -1
  62. package/dist/features/block-ui/block-actions.d.ts +19 -3
  63. package/dist/features/block-ui/block-actions.js +49 -3
  64. package/dist/features/block-ui/block-actions.js.map +1 -1
  65. package/dist/features/block-ui/components/BlockActionsOverlay.js +24 -13
  66. package/dist/features/block-ui/components/BlockActionsOverlay.js.map +1 -1
  67. package/dist/features/block-ui/components/BlockActionsOverlay.vue.d.ts +12 -5
  68. package/dist/features/block-ui/components/BlockUi.js +52 -20
  69. package/dist/features/block-ui/components/BlockUi.js.map +1 -1
  70. package/dist/features/block-ui/components/BlockUi.vue.d.ts +6 -4
  71. package/dist/features/block-ui/feature.d.ts +8 -2
  72. package/dist/features/block-ui/feature.js +7 -2
  73. package/dist/features/block-ui/feature.js.map +1 -1
  74. package/dist/features/block-ui/index.d.ts +3 -2
  75. package/dist/features/block-ui/index.js +2 -2
  76. package/dist/features/block-ui/react/BlockActionsOverlay.d.ts +14 -7
  77. package/dist/features/block-ui/react/BlockActionsOverlay.js +25 -13
  78. package/dist/features/block-ui/react/BlockActionsOverlay.js.map +1 -1
  79. package/dist/features/block-ui/react/BlockUi.d.ts +6 -4
  80. package/dist/features/block-ui/react/BlockUi.js +43 -15
  81. package/dist/features/block-ui/react/BlockUi.js.map +1 -1
  82. package/dist/features/block-ui/react.d.ts +4 -2
  83. package/dist/features/block-ui/react.js +3 -3
  84. package/dist/features/block-ui/react.js.map +1 -1
  85. package/dist/features/block-ui/vue.d.ts +4 -2
  86. package/dist/features/block-ui/vue.js +3 -3
  87. package/dist/features/block-ui/vue.js.map +1 -1
  88. package/dist/features/composites/baskets.d.ts +27 -0
  89. package/dist/features/composites/baskets.js +46 -0
  90. package/dist/features/composites/baskets.js.map +1 -0
  91. package/dist/features/composites/components/CompositeBasketMenu.js +97 -0
  92. package/dist/features/composites/components/CompositeBasketMenu.js.map +1 -0
  93. package/dist/features/composites/components/CompositeChrome.js +79 -23
  94. package/dist/features/composites/components/CompositeChrome.js.map +1 -1
  95. package/dist/features/composites/components/CompositeFieldEditor.js +1 -0
  96. package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
  97. package/dist/features/composites/components/CompositeView.js +17 -2
  98. package/dist/features/composites/components/CompositeView.js.map +1 -1
  99. package/dist/features/composites/components/DerivationBadge.js +12 -3
  100. package/dist/features/composites/components/DerivationBadge.js.map +1 -1
  101. package/dist/features/composites/components/FocusChip.js +53 -0
  102. package/dist/features/composites/components/FocusChip.js.map +1 -0
  103. package/dist/features/composites/components/FocusChip.vue.d.ts +14 -0
  104. package/dist/features/composites/components/SettingsEditingTab.js +40 -4
  105. package/dist/features/composites/components/SettingsEditingTab.js.map +1 -1
  106. package/dist/features/composites/derivation.d.ts +2 -1
  107. package/dist/features/composites/derivation.js +5 -1
  108. package/dist/features/composites/derivation.js.map +1 -1
  109. package/dist/features/composites/derived-chrome.js +13 -3
  110. package/dist/features/composites/derived-chrome.js.map +1 -1
  111. package/dist/features/composites/feature.d.ts +7 -1
  112. package/dist/features/composites/feature.js +96 -23
  113. package/dist/features/composites/feature.js.map +1 -1
  114. package/dist/features/composites/focus.d.ts +38 -0
  115. package/dist/features/composites/focus.js +86 -0
  116. package/dist/features/composites/focus.js.map +1 -0
  117. package/dist/features/composites/index.d.ts +6 -3
  118. package/dist/features/composites/index.js +6 -3
  119. package/dist/features/composites/meta-keys.d.ts +4 -0
  120. package/dist/features/composites/meta-keys.js +8 -4
  121. package/dist/features/composites/meta-keys.js.map +1 -1
  122. package/dist/features/composites/nodes.d.ts +14 -2
  123. package/dist/features/composites/nodes.js +61 -5
  124. package/dist/features/composites/nodes.js.map +1 -1
  125. package/dist/features/composites/outline.d.ts +4 -0
  126. package/dist/features/composites/outline.js +3 -0
  127. package/dist/features/composites/outline.js.map +1 -1
  128. package/dist/features/composites/react/CompositeBasketMenu.js +88 -0
  129. package/dist/features/composites/react/CompositeBasketMenu.js.map +1 -0
  130. package/dist/features/composites/react/CompositeChrome.js +52 -4
  131. package/dist/features/composites/react/CompositeChrome.js.map +1 -1
  132. package/dist/features/composites/react/CompositeView.js +12 -2
  133. package/dist/features/composites/react/CompositeView.js.map +1 -1
  134. package/dist/features/composites/react/DerivationBadge.js +10 -1
  135. package/dist/features/composites/react/DerivationBadge.js.map +1 -1
  136. package/dist/features/composites/react/FocusChip.d.ts +11 -0
  137. package/dist/features/composites/react/FocusChip.js +50 -0
  138. package/dist/features/composites/react/FocusChip.js.map +1 -0
  139. package/dist/features/composites/react/SettingsEditingTab.js +36 -1
  140. package/dist/features/composites/react/SettingsEditingTab.js.map +1 -1
  141. package/dist/features/composites/react.d.ts +2 -1
  142. package/dist/features/composites/react.js +3 -1
  143. package/dist/features/composites/react.js.map +1 -1
  144. package/dist/features/composites/schema.d.ts +5 -3
  145. package/dist/features/composites/schema.js +8 -4
  146. package/dist/features/composites/schema.js.map +1 -1
  147. package/dist/features/composites/split.d.ts +62 -0
  148. package/dist/features/composites/split.js +172 -0
  149. package/dist/features/composites/split.js.map +1 -0
  150. package/dist/features/composites/vue.d.ts +4 -3
  151. package/dist/features/composites/vue.js +7 -5
  152. package/dist/features/composites/vue.js.map +1 -1
  153. package/dist/features/media/index.js +1 -1
  154. package/dist/features/media/react/AudioRecorderModal.js +1 -1
  155. package/dist/features/media/react.js +1 -1
  156. package/dist/features/transfer/components/TransferMenu.js +93 -0
  157. package/dist/features/transfer/components/TransferMenu.js.map +1 -0
  158. package/dist/features/transfer/components/TransferMenu.vue.d.ts +15 -0
  159. package/dist/features/transfer/feature.d.ts +76 -0
  160. package/dist/features/transfer/feature.js +169 -0
  161. package/dist/features/transfer/feature.js.map +1 -0
  162. package/dist/features/transfer/index.d.ts +3 -0
  163. package/dist/features/transfer/index.js +3 -0
  164. package/dist/features/transfer/react/TransferMenu.d.ts +12 -0
  165. package/dist/features/transfer/react/TransferMenu.js +71 -0
  166. package/dist/features/transfer/react/TransferMenu.js.map +1 -0
  167. package/dist/features/transfer/react.d.ts +10 -0
  168. package/dist/features/transfer/react.js +11 -0
  169. package/dist/features/transfer/react.js.map +1 -0
  170. package/dist/features/transfer/transforms.d.ts +83 -0
  171. package/dist/features/transfer/transforms.js +137 -0
  172. package/dist/features/transfer/transforms.js.map +1 -0
  173. package/dist/features/transfer/vue.d.ts +9 -0
  174. package/dist/features/transfer/vue.js +11 -0
  175. package/dist/features/transfer/vue.js.map +1 -0
  176. package/dist/i18n/de/ai.js +4 -0
  177. package/dist/i18n/de/ai.js.map +1 -1
  178. package/dist/i18n/de/composites.js +20 -1
  179. package/dist/i18n/de/composites.js.map +1 -1
  180. package/dist/i18n/de/integrations.js +16 -0
  181. package/dist/i18n/de/integrations.js.map +1 -1
  182. package/dist/i18n/de/transfer.js +17 -0
  183. package/dist/i18n/de/transfer.js.map +1 -0
  184. package/dist/i18n/de/tree.js +6 -0
  185. package/dist/i18n/de/tree.js.map +1 -1
  186. package/dist/i18n/de.d.ts +55 -0
  187. package/dist/i18n/de.js +3 -1
  188. package/dist/i18n/de.js.map +1 -1
  189. package/dist/i18n/en/ai.js +4 -0
  190. package/dist/i18n/en/ai.js.map +1 -1
  191. package/dist/i18n/en/composites.js +20 -1
  192. package/dist/i18n/en/composites.js.map +1 -1
  193. package/dist/i18n/en/integrations.js +16 -0
  194. package/dist/i18n/en/integrations.js.map +1 -1
  195. package/dist/i18n/en/transfer.js +17 -0
  196. package/dist/i18n/en/transfer.js.map +1 -0
  197. package/dist/i18n/en/tree.js +6 -0
  198. package/dist/i18n/en/tree.js.map +1 -1
  199. package/dist/i18n/en.js +3 -1
  200. package/dist/i18n/en.js.map +1 -1
  201. package/dist/integrations/composite-ai/composite-fill.js +1 -1
  202. package/dist/integrations/composite-ai/composite-summary.js +1 -1
  203. package/dist/integrations/composite-ai/derive.d.ts +21 -0
  204. package/dist/integrations/composite-ai/derive.js +69 -47
  205. package/dist/integrations/composite-ai/derive.js.map +1 -1
  206. package/dist/integrations/composite-ai/index.d.ts +8 -3
  207. package/dist/integrations/composite-ai/index.js +50 -4
  208. package/dist/integrations/composite-ai/index.js.map +1 -1
  209. package/dist/integrations/composite-ai/outline-sanitize.d.ts +48 -0
  210. package/dist/integrations/composite-ai/outline-sanitize.js +175 -0
  211. package/dist/integrations/composite-ai/outline-sanitize.js.map +1 -0
  212. package/dist/integrations/composite-ai/split.d.ts +44 -0
  213. package/dist/integrations/composite-ai/split.js +187 -0
  214. package/dist/integrations/composite-ai/split.js.map +1 -0
  215. package/dist/integrations/composite-ai/transfer-transforms.d.ts +27 -0
  216. package/dist/integrations/composite-ai/transfer-transforms.js +153 -0
  217. package/dist/integrations/composite-ai/transfer-transforms.js.map +1 -0
  218. package/dist/integrations/transcript-ai/protocol.d.ts +13 -0
  219. package/dist/integrations/transcript-ai/protocol.js +49 -1
  220. package/dist/integrations/transcript-ai/protocol.js.map +1 -1
  221. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  222. package/dist/markdown/MarkdownSurface.vue.d.ts +6 -6
  223. package/dist/nuxt/module.d.ts +110 -0
  224. package/dist/presets/compose.d.ts +24 -8
  225. package/dist/presets/compose.js +117 -58
  226. package/dist/presets/compose.js.map +1 -1
  227. package/dist/react/compose.js +6 -2
  228. package/dist/react/compose.js.map +1 -1
  229. package/dist/react/composed-surface.d.ts +9 -0
  230. package/dist/react/composed-surface.js +4 -1
  231. package/dist/react/composed-surface.js.map +1 -1
  232. package/dist/server/ai.d.ts +2 -5
  233. package/dist/server/ai.js.map +1 -1
  234. package/dist/server/handlers/ai.d.ts +7 -78
  235. package/dist/server/handlers/ai.js +30 -94
  236. package/dist/server/handlers/ai.js.map +1 -1
  237. package/dist/server/handlers/editor.d.ts +1 -0
  238. package/dist/server/handlers/index.d.ts +3 -1
  239. package/dist/server/handlers/index.js +3 -1
  240. package/dist/server/index.d.ts +2 -1
  241. package/dist/settings/index.d.ts +8 -0
  242. package/dist/settings/index.js +16 -4
  243. package/dist/settings/index.js.map +1 -1
  244. package/dist/styles/composites.css +32 -0
  245. package/dist/tree/DocumentTree.js +229 -78
  246. package/dist/tree/DocumentTree.js.map +1 -1
  247. package/dist/tree/drop-rules.js +8 -0
  248. package/dist/tree/drop-rules.js.map +1 -1
  249. package/dist/tree/react/DocumentTree.js +152 -31
  250. package/dist/tree/react/DocumentTree.js.map +1 -1
  251. package/dist/tree/tree-host.js +30 -1
  252. package/dist/tree/tree-host.js.map +1 -1
  253. package/dist/tree/tree-items.js +52 -5
  254. package/dist/tree/tree-items.js.map +1 -1
  255. package/dist/tree/tree-model.js +113 -10
  256. package/dist/tree/tree-model.js.map +1 -1
  257. package/dist/tree/tree-rows.js +20 -7
  258. package/dist/tree/tree-rows.js.map +1 -1
  259. package/dist/tree/tree-view-model.d.ts +5 -0
  260. package/dist/tree/tree-view-model.js +13 -0
  261. package/dist/tree/tree-view-model.js.map +1 -1
  262. package/dist/vue/AdvancedEditor.js +6 -0
  263. package/dist/vue/AdvancedEditor.js.map +1 -1
  264. package/dist/vue/AdvancedEditor.vue.d.ts +13 -5
  265. package/dist/vue/SimpleEditor.vue.d.ts +4 -4
  266. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  267. package/dist/vue/compose.js +6 -2
  268. package/dist/vue/compose.js.map +1 -1
  269. package/dist/vue/kit/types.d.ts +2 -0
  270. package/package.json +21 -1
@@ -14,7 +14,11 @@ const AI_TASK_IDS = [
14
14
  "summarize",
15
15
  "custom",
16
16
  "continue",
17
- "fill-block"
17
+ "fill-block",
18
+ "split-block",
19
+ "outline-sanitize",
20
+ "transfer-summary",
21
+ "transfer-bullets"
18
22
  ];
19
23
  /** `markdown` under the `##` title `heading`; it replaces a top-level (#/##) heading the model wrote anyway. */
20
24
  function withHeading(heading, markdown) {
@@ -215,12 +219,80 @@ const AI_TASKS = {
215
219
  "Gib nur die Inhaltsblöcke des Bausteins als Markdown zurück: keine Titelzeile, keine Felder, keinen :::-Zaun."
216
220
  ].join("\n\n")
217
221
  }]
222
+ },
223
+ "split-block": {
224
+ id: "split-block",
225
+ labelKey: "ai.task.splitBlock.label",
226
+ icon: "i-lucide-sparkles",
227
+ needsSelection: true,
228
+ needsPrompt: true,
229
+ apply: "replace-selection",
230
+ slot: "generate",
231
+ programmatic: true,
232
+ messages: ({ prompt, selection, blocks }) => [{
233
+ role: "system",
234
+ content: aiSystem(blocks)
235
+ }, {
236
+ role: "user",
237
+ content: [`Gliedere den folgenden Text in Abschnitte von etwa ${prompt.trim()} Zeichen: füge an sinnvollen Stellen Überschriften der Ebene 2 („## “) mit knappen, treffenden Titeln ein. Verändere den Text selbst nicht: kein Wort hinzufügen, entfernen oder umstellen, Formatierung und Absätze beibehalten. Gib den vollständigen Text mit den eingefügten Überschriften als Markdown zurück, ohne weitere Überschrift.`, `Text:\n"""\n${selection}\n"""`].join("\n\n")
238
+ }]
239
+ },
240
+ "outline-sanitize": {
241
+ id: "outline-sanitize",
242
+ labelKey: "ai.task.outlineSanitize.label",
243
+ icon: "i-lucide-list-tree",
244
+ needsSelection: true,
245
+ needsPrompt: false,
246
+ apply: "replace-selection",
247
+ slot: "generate",
248
+ programmatic: true,
249
+ messages: ({ selection }) => [{
250
+ role: "system",
251
+ content: "Du bist ein Assistent für die Gliederung von Dokumenten. Antworte ausschließlich mit der geforderten JSON-Liste, ohne Erklärung und ohne Codezaun."
252
+ }, {
253
+ role: "user",
254
+ content: ["Hier ist die Gliederung eines Dokuments als JSON-Liste von Überschriften (i = Index, level = Ebene, text, chars = Zeichen des Textes darunter). Bereinige die Struktur: keine übersprungenen Ebenen; höchstens eine Überschrift der Ebene 1; Nummerierungen, Satzzeichen am Ende und doppelte Leerzeichen entfernen; einheitliche Schreibweise; eine Überschrift ohne Text darunter (chars = 0) mit der folgenden zusammenlegen, indem du ihren text leer lässt (sie wird entfernt). Ändere Bedeutung nicht. Antworte nur mit der JSON-Liste gleicher Länge und Reihenfolge: [{\"i\":0,\"level\":1,\"text\":\"…\"}, …]", `Gliederung:\n${selection}`].join("\n\n")
255
+ }]
256
+ },
257
+ "transfer-summary": {
258
+ id: "transfer-summary",
259
+ labelKey: "ai.task.transferSummary.label",
260
+ icon: "i-lucide-text-quote",
261
+ needsSelection: true,
262
+ needsPrompt: false,
263
+ apply: "insert-after-block",
264
+ slot: "generate",
265
+ programmatic: true,
266
+ messages: ({ selection, blocks }) => [{
267
+ role: "system",
268
+ content: aiSystem(blocks)
269
+ }, {
270
+ role: "user",
271
+ content: ["Fasse den folgenden Baustein knapp in ein bis drei Absätzen zusammen. Behalte Fachbegriffe bei. Gib nur die Zusammenfassung als Markdown zurück, ohne Überschrift.", `Baustein:\n"""\n${selection}\n"""`].join("\n\n")
272
+ }]
273
+ },
274
+ "transfer-bullets": {
275
+ id: "transfer-bullets",
276
+ labelKey: "ai.task.transferBullets.label",
277
+ icon: "i-lucide-list",
278
+ needsSelection: true,
279
+ needsPrompt: false,
280
+ apply: "insert-after-block",
281
+ slot: "generate",
282
+ programmatic: true,
283
+ messages: ({ selection, blocks }) => [{
284
+ role: "system",
285
+ content: aiSystem(blocks)
286
+ }, {
287
+ role: "user",
288
+ content: ["Gib die Kernaussagen des folgenden Bausteins als Aufzählung zurück: eine Markdown-Liste, ein Punkt je Aussage, knapp und vollständig. Gib nur die Liste zurück, ohne Überschrift.", `Baustein:\n"""\n${selection}\n"""`].join("\n\n")
289
+ }]
218
290
  }
219
291
  };
220
292
  /** In-block AI tasks (`rewrite` slot): rewrite text inside one block or a selection. */
221
293
  const inBlockTasks = () => Object.values(AI_TASKS).filter((task) => task.slot === "rewrite");
222
294
  /** Block generation tasks (`generate` slot): produce new blocks after the caret or at the end of the document. */
223
- const blockTasks = () => Object.values(AI_TASKS).filter((task) => task.slot === "generate" && !task.needsBlock);
295
+ const blockTasks = () => Object.values(AI_TASKS).filter((task) => task.slot === "generate" && !task.needsBlock && !task.programmatic);
224
296
  //#endregion
225
297
  export { AI_LIMITS, AI_TASKS, AI_TASK_IDS, blockTasks, inBlockTasks, withHeading, withTaskHeading };
226
298
 
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.js","names":[],"sources":["../../../../src/features/ai/protocol/tasks.ts"],"sourcesContent":["import type { AiBlock } from '../../../core/feature'\nimport type { MessageKey, Translate } from '../../../i18n'\nimport { de } from '../../../i18n/de'\nimport type { AiLanguage } from '../../../settings'\n\nexport const AI_TASK_IDS = ['reformulate', 'generate', 'transcript-summary', 'transcript-minutes', 'transcript-action-items', 'fix', 'shorter', 'formal', 'casual', 'translate', 'summarize', 'custom', 'continue', 'fill-block'] as const\nexport type AiTaskId = (typeof AI_TASK_IDS)[number]\n/**\n * How „Übernehmen“ applies the result: client-side over the captured selection, or server-side after the caret's block\n * or after the document's last block.\n */\nexport type AiApplyMode = 'replace-selection' | 'insert-after-block' | 'append-to-document'\n/** The block a `fill-block` run writes (the brief of `compositeAiBrief` plus the host's `aiBlockData`). */\nexport interface AiBlockInput {\n kind: { id: string, label: string, purpose?: string }\n path: readonly string[]\n title: string\n description: string\n fields: readonly { label: string, value: string }[]\n instructions: string\n skeleton: string\n body: string\n data: readonly { label: string, text: string }[]\n}\n/**\n * `users`: registered usernames, filled in by the generate route for tasks with `needsUsers` (never sent by the client).\n * `language`: target language of tasks with `needsLanguage`. `block`: the block of a `needsBlock` task.\n */\nexport interface AiInput { prompt: string, selection: string, context: string, users?: readonly string[], language?: AiLanguage, blocks?: readonly AiBlock[], block?: AiBlockInput }\nexport interface AiMessage { role: 'system' | 'user', content: string }\nexport interface AiTask {\n id: AiTaskId\n /** Catalog key of the task's name in the menus and the sidebar. */\n labelKey: MessageKey\n icon: string\n needsSelection: boolean\n needsPrompt: boolean\n /** Catalog key of the input placeholder of a task with `needsPrompt`; the other tasks name an optional hint. */\n promptLabelKey?: MessageKey\n apply: AiApplyMode\n /** Model slot the task runs on: `rewrite` (in-block AI, text inside one block) or `generate` (block generation). */\n slot: 'rewrite' | 'generate'\n /** The context is the whole document (up to `AI_LIMITS.transcript`) and must not be empty. */\n wholeDocument?: boolean\n /** The generate route passes the registered usernames as `users`, so the model can write `@username` mentions. */\n needsUsers?: boolean\n /**\n * Catalog key of the title of the result: its `##` heading as shown and applied (`withTaskHeading`, in the editor's\n * language). A feature may format the applied result differently through `formatAiResult` (composite AI wraps it in\n * a `section` composite). The model is told not to write one.\n */\n headingKey?: MessageKey\n /** The request must name a target `language` (see `AI_LANGUAGES`). */\n needsLanguage?: boolean\n /** Offered as quick action in the selection's AI menu (AiSelectionMenu). */\n selectionMenu?: boolean\n /** Offered as quick action in the AI menu at the caret (`/ai`, AiSelectionMenu), not in the AI sidebar. */\n caretMenu?: boolean\n /** The request must carry `block`; offered only through a block action, never in the sidebar or menus. */\n needsBlock?: boolean\n messages: (input: AiInput) => AiMessage[]\n}\n\n/** `markdown` under the `##` title `heading`; it replaces a top-level (#/##) heading the model wrote anyway. */\nexport function withHeading(heading: string, markdown: string): string {\n return `## ${heading}\\n\\n${markdown.replace(/^\\s*#{1,2}[ \\t][^\\n]*/, '').trimStart()}`\n}\n\n/** Result Markdown as shown: for tasks with a heading, that heading (in the language of `t`) replaces a top-level (#/##) heading the model wrote anyway. */\nexport const withTaskHeading = (task: AiTask, markdown: string, t: Translate): string => task.headingKey ? withHeading(t(task.headingKey), markdown) : markdown\n\n/** Maximum input lengths (characters), enforced by the generate route; the client truncates the context to fit. */\nexport const AI_LIMITS = { prompt: 4000, selection: 20000, context: 12000, transcript: 60000, block: { text: 4000, skeleton: 8000, body: 20000, data: 20000 } } as const\n\n// i18n-ignore-start: model-facing prompt\n/** The system prompt of all tasks; `blocks` (the editor's host blocks) are listed after the core Markdown subset. */\nfunction aiSystem(blocks: readonly AiBlock[] = []): string {\n return [\n 'Du bist ein Schreibassistent in einem Texteditor.',\n 'Antworte ausschließlich mit dem fertigen Text als Markdown.',\n 'Erlaubt sind: Absätze, Überschriften (#, ##, ###), **fett**, *kursiv*, ~~durchgestrichen~~, `Code`, Codeblöcke mit ```, Aufzählungen, nummerierte Listen, Zitate mit > und Trennlinien (---)'\n + (blocks.length\n ? ` sowie diese eigenen Blöcke, wenn sie inhaltlich passen: ${blocks.map(block => `${block.label} – ${block.description} Markdown-Form: „${block.syntax.replace(/\\n/g, '\\\\n')}“, Beispiel: „${block.example.replace(/\\n/g, '\\\\n')}“`).join('; ')}.`\n : '.'),\n 'Transkript-Passagen (Redebeiträge von Sprecher:innen) schreibst du als Sprecherzeilen: jeder Redebeitrag ist ein eigener Absatz, der mit dem fett gesetzten Namen und Doppelpunkt beginnt, also **Name:** Text – eine Zeile pro Redebeitrag, dazwischen eine Leerzeile. Vorhandene Sprechernamen übernimmst du unverändert.',\n 'Beginne andere Absätze nicht mit fett gesetztem Text, der auf einen Doppelpunkt endet.',\n 'Keine Einleitung, keine Erklärungen, keine Rückfragen.',\n 'Schreibe in der Sprache des Dokuments.',\n ].join('\\n')\n}\n\nconst contextBlock = (context: string) => `Dokument (Kontext):\\n\"\"\"\\n${context}\\n\"\"\"`\n// i18n-ignore-end\n\n/**\n * Whole-transcript task whose result is appended to the document under the title `headingKey`. The prompt names that\n * title in German (prompts stay German whatever the editor's language).\n */\nfunction transcriptTask(\n id: AiTaskId,\n headingKey: MessageKey,\n icon: string,\n instruction: (users: readonly string[]) => string[],\n needsUsers = false,\n): AiTask {\n return {\n id,\n labelKey: headingKey,\n icon,\n needsSelection: false,\n needsPrompt: false,\n promptLabelKey: 'ai.task.transcript.promptLabel',\n apply: 'append-to-document',\n slot: 'generate',\n wholeDocument: true,\n needsUsers,\n headingKey,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, users = [], blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n ...instruction(users),\n `Beginne ohne eigene Überschrift – die Überschrift „${de[headingKey]}“ wird automatisch vorangestellt. Zwischenüberschriften nur mit ### (z. B. „### Beschlüsse“), nie als fett gesetzte Zeile wie **Beschlüsse:**.`,\n 'Schreibe keine Sprecherzeilen (**Name:** …), sondern nenne Personen im Fließtext.',\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n `Transkript:\\n\"\"\"\\n${context}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n }\n}\n\n/** Quick action of the selection's AI menu: rewrites the selected excerpt according to `instruction`. */\nfunction selectionTask(\n id: AiTaskId,\n labelKey: MessageKey,\n icon: string,\n instruction: (input: AiInput) => string,\n extra: Partial<Pick<AiTask, 'needsPrompt' | 'promptLabelKey' | 'needsLanguage'>> = {},\n): AiTask {\n return {\n id,\n labelKey,\n icon,\n needsSelection: true,\n needsPrompt: false,\n apply: 'replace-selection',\n slot: 'rewrite',\n selectionMenu: true,\n ...extra,\n // i18n-ignore-start: model-facing prompt\n messages: input => [\n { role: 'system', content: aiSystem(input.blocks) },\n {\n role: 'user',\n content: [\n instruction(input),\n 'Gib nur den Text zurück, der den Textausschnitt ersetzen soll, ohne zusätzliche Überschrift. Behalte die Markdown-Formatierung und Sprecherzeilen bei.',\n contextBlock(input.context),\n `Textausschnitt:\\n\"\"\"\\n${input.selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n }\n}\n\nexport const AI_TASKS: Record<AiTaskId, AiTask> = {\n reformulate: {\n id: 'reformulate',\n labelKey: 'ai.task.reformulate.label',\n icon: 'i-lucide-refresh-cw',\n needsSelection: true,\n needsPrompt: false,\n promptLabelKey: 'ai.task.reformulate.promptLabel',\n apply: 'replace-selection',\n slot: 'rewrite',\n selectionMenu: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, selection, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Formuliere den folgenden Textausschnitt um. Behalte Bedeutung, Formatierung und ungefähre Länge bei.',\n 'Gib nur den Text zurück, der den Textausschnitt ersetzen soll, ohne zusätzliche Überschrift.',\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n contextBlock(context),\n `Textausschnitt:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n generate: {\n id: 'generate',\n labelKey: 'ai.task.generate.label',\n icon: 'i-lucide-sparkles',\n needsSelection: false,\n needsPrompt: true,\n promptLabelKey: 'ai.task.generate.promptLabel',\n apply: 'insert-after-block',\n slot: 'generate',\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Schreibe neuen Text für das Dokument nach folgender Anweisung. Gib nur den Text zurück, der eingefügt werden soll.',\n `Anweisung: ${prompt.trim()}`,\n contextBlock(context),\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n // i18n-ignore-start: model-facing prompt\n 'transcript-summary': transcriptTask('transcript-summary', 'ai.task.transcriptSummary.heading', 'i-lucide-text-quote', () => [\n 'Fasse das folgende Transkript zusammen: die wichtigsten Themen, Ergebnisse und offenen Punkte, knapp in wenigen Absätzen oder als Aufzählung.',\n ]),\n 'transcript-minutes': transcriptTask('transcript-minutes', 'ai.task.transcriptMinutes.heading', 'i-lucide-notebook-pen', () => [\n 'Erstelle ein Protokoll des folgenden Transkripts mit den Abschnitten „Teilnehmende“, „Themen“ (je Thema die wesentlichen Punkte), „Beschlüsse“ und „Nächste Schritte“ als Zwischenüberschriften und Aufzählungen darunter. Lass Abschnitte ohne Inhalt weg.',\n ]),\n 'transcript-action-items': transcriptTask('transcript-action-items', 'ai.task.transcriptActionItems.heading', 'i-lucide-list-checks', users => [\n 'Liste alle Aufgaben (Action Items) aus dem folgenden Transkript als Aufzählung auf: je Aufgabe ein Punkt der Form „Verantwortliche Person: Aufgabe (bis Frist)“; die Frist nur, wenn sie genannt wird. Ohne klar verantwortliche Person schreibe „Offen:“. Gibt es keine Aufgaben, antworte nur mit „Keine Aufgaben gefunden.“',\n users.length\n ? `Registrierte Benutzer:innen: ${users.map(u => `@${u}`).join(', ')}. Ist die verantwortliche Person eine davon (z. B. gleicher Vor- oder Nachname), schreibe sie genau als @benutzername; alle anderen Personen schreibe mit ihrem Namen ohne @.`\n : 'Schreibe verantwortliche Personen mit ihrem Namen, ohne @.',\n ], true),\n fix: selectionTask('fix', 'ai.task.fix.label', 'i-lucide-spell-check', () =>\n 'Korrigiere Rechtschreibung, Grammatik und Zeichensetzung im folgenden Textausschnitt. Ändere Wortwahl, Stil und Inhalt nicht. Ist nichts zu korrigieren, gib den Textausschnitt unverändert zurück.'),\n shorter: selectionTask('shorter', 'ai.task.shorter.label', 'i-lucide-shrink', () =>\n 'Kürze den folgenden Textausschnitt deutlich, etwa auf die Hälfte, ohne wesentliche Aussagen zu verlieren.'),\n formal: selectionTask('formal', 'ai.task.formal.label', 'i-lucide-briefcase', () =>\n 'Formuliere den folgenden Textausschnitt förmlicher und sachlicher. Behalte Bedeutung und ungefähre Länge bei.'),\n casual: selectionTask('casual', 'ai.task.casual.label', 'i-lucide-smile', () =>\n 'Formuliere den folgenden Textausschnitt lockerer und umgangssprachlicher. Behalte Bedeutung und ungefähre Länge bei.'),\n translate: selectionTask('translate', 'ai.task.translate.label', 'i-lucide-languages', ({ language }) =>\n `Übersetze den folgenden Textausschnitt in diese Sprache: ${language}. Schreibe abweichend von der Dokumentsprache auf ${language}; Sprechernamen bleiben unverändert.`, { needsLanguage: true }),\n summarize: selectionTask('summarize', 'ai.task.summarize.label', 'i-lucide-text-quote', () =>\n 'Fasse den folgenden Textausschnitt knapp zusammen.'),\n custom: selectionTask('custom', 'ai.task.custom.label', 'i-lucide-message-square-text', ({ prompt }) =>\n `Bearbeite den folgenden Textausschnitt nach dieser Anweisung: ${prompt.trim()}`, { needsPrompt: true, promptLabelKey: 'ai.task.custom.promptLabel' }),\n // i18n-ignore-end\n // `selection`: the document up to the caret line; `context`: the document after it (see AiSelectionMenu's caret mode).\n continue: {\n id: 'continue',\n labelKey: 'ai.task.continue.label',\n icon: 'i-lucide-pen-line',\n needsSelection: true,\n needsPrompt: false,\n apply: 'insert-after-block',\n slot: 'generate',\n caretMenu: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ selection, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Setze den folgenden Text an seinem Ende fort: schreibe die nächsten ein bis drei Absätze, passend zu Inhalt, Stil und Formatierung. Wiederhole nichts vom vorhandenen Text. Gib nur den neuen Text zurück, der am Ende eingefügt werden soll.',\n `Text bis zur Einfügestelle:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ...(context.trim() ? [`Danach folgt im Dokument (nur als Kontext, nicht fortsetzen oder wiederholen):\\n\"\"\"\\n${context}\\n\"\"\"`] : []),\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n 'fill-block': {\n id: 'fill-block',\n labelKey: 'ai.task.fillBlock.label',\n icon: 'i-lucide-wand-sparkles',\n needsSelection: false,\n needsPrompt: false,\n promptLabelKey: 'ai.task.fillBlock.promptLabel',\n apply: 'replace-selection',\n slot: 'generate',\n needsBlock: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, blocks, block }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n `Schreibe den Inhalt des Bausteins „${block!.title || block!.kind.label}“ (Art: ${block!.kind.label}${block!.kind.purpose ? ` – ${block!.kind.purpose}` : ''}).`,\n ...(block!.path.length ? [`Der Baustein steht unter: ${block!.path.join(' › ')}.`] : []),\n ...(block!.description.trim() ? [`Beschreibung des Bausteins: ${block!.description.trim()}`] : []),\n ...(block!.fields.length ? [`Angaben zum Baustein:\\n${block!.fields.map(f => `- ${f.label}: ${f.value}`).join('\\n')}`] : []),\n ...(block!.instructions.trim() ? [`Anweisung: ${block!.instructions.trim()}`] : []),\n ...(block!.skeleton.trim() ? [`Zieltext (Vorlage; Platzhalter in geschweiften Klammern ausfüllen, Struktur und Reihenfolge beibehalten):\\n\"\"\"\\n${block!.skeleton.trim()}\\n\"\"\"`] : []),\n ...block!.data.map(d => `${d.label}:\\n\"\"\"\\n${d.text}\\n\"\"\"`),\n ...(block!.body.trim() ? [`Bisheriger Inhalt des Bausteins (wird ersetzt; Brauchbares übernehmen):\\n\"\"\"\\n${block!.body.trim()}\\n\"\"\"`] : []),\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n contextBlock(context),\n 'Gib nur die Inhaltsblöcke des Bausteins als Markdown zurück: keine Titelzeile, keine Felder, keinen :::-Zaun.',\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n}\n\n/** In-block AI tasks (`rewrite` slot): rewrite text inside one block or a selection. */\nexport const inBlockTasks = (): AiTask[] => Object.values(AI_TASKS).filter(task => task.slot === 'rewrite')\n\n/** Block generation tasks (`generate` slot): produce new blocks after the caret or at the end of the document. */\nexport const blockTasks = (): AiTask[] => Object.values(AI_TASKS).filter(task => task.slot === 'generate' && !task.needsBlock)\n"],"mappings":"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"}
1
+ {"version":3,"file":"tasks.js","names":[],"sources":["../../../../src/features/ai/protocol/tasks.ts"],"sourcesContent":["import type { AiBlock } from '../../../core/feature'\nimport type { MessageKey, Translate } from '../../../i18n'\nimport { de } from '../../../i18n/de'\nimport type { AiLanguage } from '../../../settings'\n\nexport const AI_TASK_IDS = ['reformulate', 'generate', 'transcript-summary', 'transcript-minutes', 'transcript-action-items', 'fix', 'shorter', 'formal', 'casual', 'translate', 'summarize', 'custom', 'continue', 'fill-block', 'split-block', 'outline-sanitize', 'transfer-summary', 'transfer-bullets'] as const\nexport type AiTaskId = (typeof AI_TASK_IDS)[number]\n/**\n * How „Übernehmen“ applies the result: client-side over the captured selection, or server-side after the caret's block\n * or after the document's last block.\n */\nexport type AiApplyMode = 'replace-selection' | 'insert-after-block' | 'append-to-document'\n/** The block a `fill-block` run writes (the brief of `compositeAiBrief` plus the host's `aiBlockData`). */\nexport interface AiBlockInput {\n kind: { id: string, label: string, purpose?: string }\n path: readonly string[]\n title: string\n description: string\n fields: readonly { label: string, value: string }[]\n instructions: string\n skeleton: string\n body: string\n data: readonly { label: string, text: string }[]\n}\n/**\n * `users`: registered usernames, filled in by the generate route for tasks with `needsUsers` (never sent by the client).\n * `language`: target language of tasks with `needsLanguage`. `block`: the block of a `needsBlock` task.\n */\nexport interface AiInput { prompt: string, selection: string, context: string, users?: readonly string[], language?: AiLanguage, blocks?: readonly AiBlock[], block?: AiBlockInput }\nexport interface AiMessage { role: 'system' | 'user', content: string }\nexport interface AiTask {\n id: AiTaskId\n /** Catalog key of the task's name in the menus and the sidebar. */\n labelKey: MessageKey\n icon: string\n needsSelection: boolean\n needsPrompt: boolean\n /** Catalog key of the input placeholder of a task with `needsPrompt`; the other tasks name an optional hint. */\n promptLabelKey?: MessageKey\n apply: AiApplyMode\n /** Model slot the task runs on: `rewrite` (in-block AI, text inside one block) or `generate` (block generation). */\n slot: 'rewrite' | 'generate'\n /** The context is the whole document (up to `AI_LIMITS.transcript`) and must not be empty. */\n wholeDocument?: boolean\n /** The generate route passes the registered usernames as `users`, so the model can write `@username` mentions. */\n needsUsers?: boolean\n /**\n * Catalog key of the title of the result: its `##` heading as shown and applied (`withTaskHeading`, in the editor's\n * language). A feature may format the applied result differently through `formatAiResult` (composite AI wraps it in\n * a `section` composite). The model is told not to write one.\n */\n headingKey?: MessageKey\n /** The request must name a target `language` (see `AI_LANGUAGES`). */\n needsLanguage?: boolean\n /** Offered as quick action in the selection's AI menu (AiSelectionMenu). */\n selectionMenu?: boolean\n /** Offered as quick action in the AI menu at the caret (`/ai`, AiSelectionMenu), not in the AI sidebar. */\n caretMenu?: boolean\n /** The request must carry `block`; offered only through a block action, never in the sidebar or menus. */\n needsBlock?: boolean\n /** Run only by a block action or command that sends its own request; never listed in the sidebar or the menus. */\n programmatic?: boolean\n messages: (input: AiInput) => AiMessage[]\n}\n\n/** `markdown` under the `##` title `heading`; it replaces a top-level (#/##) heading the model wrote anyway. */\nexport function withHeading(heading: string, markdown: string): string {\n return `## ${heading}\\n\\n${markdown.replace(/^\\s*#{1,2}[ \\t][^\\n]*/, '').trimStart()}`\n}\n\n/** Result Markdown as shown: for tasks with a heading, that heading (in the language of `t`) replaces a top-level (#/##) heading the model wrote anyway. */\nexport const withTaskHeading = (task: AiTask, markdown: string, t: Translate): string => task.headingKey ? withHeading(t(task.headingKey), markdown) : markdown\n\n/** Maximum input lengths (characters), enforced by the generate route; the client truncates the context to fit. */\nexport const AI_LIMITS = { prompt: 4000, selection: 20000, context: 12000, transcript: 60000, block: { text: 4000, skeleton: 8000, body: 20000, data: 20000 } } as const\n\n// i18n-ignore-start: model-facing prompt\n/** The system prompt of all tasks; `blocks` (the editor's host blocks) are listed after the core Markdown subset. */\nfunction aiSystem(blocks: readonly AiBlock[] = []): string {\n return [\n 'Du bist ein Schreibassistent in einem Texteditor.',\n 'Antworte ausschließlich mit dem fertigen Text als Markdown.',\n 'Erlaubt sind: Absätze, Überschriften (#, ##, ###), **fett**, *kursiv*, ~~durchgestrichen~~, `Code`, Codeblöcke mit ```, Aufzählungen, nummerierte Listen, Zitate mit > und Trennlinien (---)'\n + (blocks.length\n ? ` sowie diese eigenen Blöcke, wenn sie inhaltlich passen: ${blocks.map(block => `${block.label} – ${block.description} Markdown-Form: „${block.syntax.replace(/\\n/g, '\\\\n')}“, Beispiel: „${block.example.replace(/\\n/g, '\\\\n')}“`).join('; ')}.`\n : '.'),\n 'Transkript-Passagen (Redebeiträge von Sprecher:innen) schreibst du als Sprecherzeilen: jeder Redebeitrag ist ein eigener Absatz, der mit dem fett gesetzten Namen und Doppelpunkt beginnt, also **Name:** Text – eine Zeile pro Redebeitrag, dazwischen eine Leerzeile. Vorhandene Sprechernamen übernimmst du unverändert.',\n 'Beginne andere Absätze nicht mit fett gesetztem Text, der auf einen Doppelpunkt endet.',\n 'Keine Einleitung, keine Erklärungen, keine Rückfragen.',\n 'Schreibe in der Sprache des Dokuments.',\n ].join('\\n')\n}\n\nconst contextBlock = (context: string) => `Dokument (Kontext):\\n\"\"\"\\n${context}\\n\"\"\"`\n// i18n-ignore-end\n\n/**\n * Whole-transcript task whose result is appended to the document under the title `headingKey`. The prompt names that\n * title in German (prompts stay German whatever the editor's language).\n */\nfunction transcriptTask(\n id: AiTaskId,\n headingKey: MessageKey,\n icon: string,\n instruction: (users: readonly string[]) => string[],\n needsUsers = false,\n): AiTask {\n return {\n id,\n labelKey: headingKey,\n icon,\n needsSelection: false,\n needsPrompt: false,\n promptLabelKey: 'ai.task.transcript.promptLabel',\n apply: 'append-to-document',\n slot: 'generate',\n wholeDocument: true,\n needsUsers,\n headingKey,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, users = [], blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n ...instruction(users),\n `Beginne ohne eigene Überschrift – die Überschrift „${de[headingKey]}“ wird automatisch vorangestellt. Zwischenüberschriften nur mit ### (z. B. „### Beschlüsse“), nie als fett gesetzte Zeile wie **Beschlüsse:**.`,\n 'Schreibe keine Sprecherzeilen (**Name:** …), sondern nenne Personen im Fließtext.',\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n `Transkript:\\n\"\"\"\\n${context}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n }\n}\n\n/** Quick action of the selection's AI menu: rewrites the selected excerpt according to `instruction`. */\nfunction selectionTask(\n id: AiTaskId,\n labelKey: MessageKey,\n icon: string,\n instruction: (input: AiInput) => string,\n extra: Partial<Pick<AiTask, 'needsPrompt' | 'promptLabelKey' | 'needsLanguage'>> = {},\n): AiTask {\n return {\n id,\n labelKey,\n icon,\n needsSelection: true,\n needsPrompt: false,\n apply: 'replace-selection',\n slot: 'rewrite',\n selectionMenu: true,\n ...extra,\n // i18n-ignore-start: model-facing prompt\n messages: input => [\n { role: 'system', content: aiSystem(input.blocks) },\n {\n role: 'user',\n content: [\n instruction(input),\n 'Gib nur den Text zurück, der den Textausschnitt ersetzen soll, ohne zusätzliche Überschrift. Behalte die Markdown-Formatierung und Sprecherzeilen bei.',\n contextBlock(input.context),\n `Textausschnitt:\\n\"\"\"\\n${input.selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n }\n}\n\nexport const AI_TASKS: Record<AiTaskId, AiTask> = {\n reformulate: {\n id: 'reformulate',\n labelKey: 'ai.task.reformulate.label',\n icon: 'i-lucide-refresh-cw',\n needsSelection: true,\n needsPrompt: false,\n promptLabelKey: 'ai.task.reformulate.promptLabel',\n apply: 'replace-selection',\n slot: 'rewrite',\n selectionMenu: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, selection, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Formuliere den folgenden Textausschnitt um. Behalte Bedeutung, Formatierung und ungefähre Länge bei.',\n 'Gib nur den Text zurück, der den Textausschnitt ersetzen soll, ohne zusätzliche Überschrift.',\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n contextBlock(context),\n `Textausschnitt:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n generate: {\n id: 'generate',\n labelKey: 'ai.task.generate.label',\n icon: 'i-lucide-sparkles',\n needsSelection: false,\n needsPrompt: true,\n promptLabelKey: 'ai.task.generate.promptLabel',\n apply: 'insert-after-block',\n slot: 'generate',\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Schreibe neuen Text für das Dokument nach folgender Anweisung. Gib nur den Text zurück, der eingefügt werden soll.',\n `Anweisung: ${prompt.trim()}`,\n contextBlock(context),\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n // i18n-ignore-start: model-facing prompt\n 'transcript-summary': transcriptTask('transcript-summary', 'ai.task.transcriptSummary.heading', 'i-lucide-text-quote', () => [\n 'Fasse das folgende Transkript zusammen: die wichtigsten Themen, Ergebnisse und offenen Punkte, knapp in wenigen Absätzen oder als Aufzählung.',\n ]),\n 'transcript-minutes': transcriptTask('transcript-minutes', 'ai.task.transcriptMinutes.heading', 'i-lucide-notebook-pen', () => [\n 'Erstelle ein Protokoll des folgenden Transkripts mit den Abschnitten „Teilnehmende“, „Themen“ (je Thema die wesentlichen Punkte), „Beschlüsse“ und „Nächste Schritte“ als Zwischenüberschriften und Aufzählungen darunter. Lass Abschnitte ohne Inhalt weg.',\n ]),\n 'transcript-action-items': transcriptTask('transcript-action-items', 'ai.task.transcriptActionItems.heading', 'i-lucide-list-checks', users => [\n 'Liste alle Aufgaben (Action Items) aus dem folgenden Transkript als Aufzählung auf: je Aufgabe ein Punkt der Form „Verantwortliche Person: Aufgabe (bis Frist)“; die Frist nur, wenn sie genannt wird. Ohne klar verantwortliche Person schreibe „Offen:“. Gibt es keine Aufgaben, antworte nur mit „Keine Aufgaben gefunden.“',\n users.length\n ? `Registrierte Benutzer:innen: ${users.map(u => `@${u}`).join(', ')}. Ist die verantwortliche Person eine davon (z. B. gleicher Vor- oder Nachname), schreibe sie genau als @benutzername; alle anderen Personen schreibe mit ihrem Namen ohne @.`\n : 'Schreibe verantwortliche Personen mit ihrem Namen, ohne @.',\n ], true),\n fix: selectionTask('fix', 'ai.task.fix.label', 'i-lucide-spell-check', () =>\n 'Korrigiere Rechtschreibung, Grammatik und Zeichensetzung im folgenden Textausschnitt. Ändere Wortwahl, Stil und Inhalt nicht. Ist nichts zu korrigieren, gib den Textausschnitt unverändert zurück.'),\n shorter: selectionTask('shorter', 'ai.task.shorter.label', 'i-lucide-shrink', () =>\n 'Kürze den folgenden Textausschnitt deutlich, etwa auf die Hälfte, ohne wesentliche Aussagen zu verlieren.'),\n formal: selectionTask('formal', 'ai.task.formal.label', 'i-lucide-briefcase', () =>\n 'Formuliere den folgenden Textausschnitt förmlicher und sachlicher. Behalte Bedeutung und ungefähre Länge bei.'),\n casual: selectionTask('casual', 'ai.task.casual.label', 'i-lucide-smile', () =>\n 'Formuliere den folgenden Textausschnitt lockerer und umgangssprachlicher. Behalte Bedeutung und ungefähre Länge bei.'),\n translate: selectionTask('translate', 'ai.task.translate.label', 'i-lucide-languages', ({ language }) =>\n `Übersetze den folgenden Textausschnitt in diese Sprache: ${language}. Schreibe abweichend von der Dokumentsprache auf ${language}; Sprechernamen bleiben unverändert.`, { needsLanguage: true }),\n summarize: selectionTask('summarize', 'ai.task.summarize.label', 'i-lucide-text-quote', () =>\n 'Fasse den folgenden Textausschnitt knapp zusammen.'),\n custom: selectionTask('custom', 'ai.task.custom.label', 'i-lucide-message-square-text', ({ prompt }) =>\n `Bearbeite den folgenden Textausschnitt nach dieser Anweisung: ${prompt.trim()}`, { needsPrompt: true, promptLabelKey: 'ai.task.custom.promptLabel' }),\n // i18n-ignore-end\n // `selection`: the document up to the caret line; `context`: the document after it (see AiSelectionMenu's caret mode).\n continue: {\n id: 'continue',\n labelKey: 'ai.task.continue.label',\n icon: 'i-lucide-pen-line',\n needsSelection: true,\n needsPrompt: false,\n apply: 'insert-after-block',\n slot: 'generate',\n caretMenu: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ selection, context, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Setze den folgenden Text an seinem Ende fort: schreibe die nächsten ein bis drei Absätze, passend zu Inhalt, Stil und Formatierung. Wiederhole nichts vom vorhandenen Text. Gib nur den neuen Text zurück, der am Ende eingefügt werden soll.',\n `Text bis zur Einfügestelle:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ...(context.trim() ? [`Danach folgt im Dokument (nur als Kontext, nicht fortsetzen oder wiederholen):\\n\"\"\"\\n${context}\\n\"\"\"`] : []),\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n 'fill-block': {\n id: 'fill-block',\n labelKey: 'ai.task.fillBlock.label',\n icon: 'i-lucide-wand-sparkles',\n needsSelection: false,\n needsPrompt: false,\n promptLabelKey: 'ai.task.fillBlock.promptLabel',\n apply: 'replace-selection',\n slot: 'generate',\n needsBlock: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, context, blocks, block }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n `Schreibe den Inhalt des Bausteins „${block!.title || block!.kind.label}“ (Art: ${block!.kind.label}${block!.kind.purpose ? ` – ${block!.kind.purpose}` : ''}).`,\n ...(block!.path.length ? [`Der Baustein steht unter: ${block!.path.join(' › ')}.`] : []),\n ...(block!.description.trim() ? [`Beschreibung des Bausteins: ${block!.description.trim()}`] : []),\n ...(block!.fields.length ? [`Angaben zum Baustein:\\n${block!.fields.map(f => `- ${f.label}: ${f.value}`).join('\\n')}`] : []),\n ...(block!.instructions.trim() ? [`Anweisung: ${block!.instructions.trim()}`] : []),\n ...(block!.skeleton.trim() ? [`Zieltext (Vorlage; Platzhalter in geschweiften Klammern ausfüllen, Struktur und Reihenfolge beibehalten):\\n\"\"\"\\n${block!.skeleton.trim()}\\n\"\"\"`] : []),\n ...block!.data.map(d => `${d.label}:\\n\"\"\"\\n${d.text}\\n\"\"\"`),\n ...(block!.body.trim() ? [`Bisheriger Inhalt des Bausteins (wird ersetzt; Brauchbares übernehmen):\\n\"\"\"\\n${block!.body.trim()}\\n\"\"\"`] : []),\n ...(prompt.trim() ? [`Hinweis: ${prompt.trim()}`] : []),\n contextBlock(context),\n 'Gib nur die Inhaltsblöcke des Bausteins als Markdown zurück: keine Titelzeile, keine Felder, keinen :::-Zaun.',\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n // `prompt`: the target size in characters, `selection`: the Baustein's body as Markdown (see `splitCompositeWithAi`).\n 'split-block': {\n id: 'split-block',\n labelKey: 'ai.task.splitBlock.label',\n icon: 'i-lucide-sparkles',\n needsSelection: true,\n needsPrompt: true,\n apply: 'replace-selection',\n slot: 'generate',\n programmatic: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ prompt, selection, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n `Gliedere den folgenden Text in Abschnitte von etwa ${prompt.trim()} Zeichen: füge an sinnvollen Stellen Überschriften der Ebene 2 („## “) mit knappen, treffenden Titeln ein. Verändere den Text selbst nicht: kein Wort hinzufügen, entfernen oder umstellen, Formatierung und Absätze beibehalten. Gib den vollständigen Text mit den eingefügten Überschriften als Markdown zurück, ohne weitere Überschrift.`,\n `Text:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n // `selection`: the document's headings as a JSON list (see `sanitizeOutline`); the answer is a JSON list, not Markdown.\n 'outline-sanitize': {\n id: 'outline-sanitize',\n labelKey: 'ai.task.outlineSanitize.label',\n icon: 'i-lucide-list-tree',\n needsSelection: true,\n needsPrompt: false,\n apply: 'replace-selection',\n slot: 'generate',\n programmatic: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ selection }) => [\n { role: 'system', content: 'Du bist ein Assistent für die Gliederung von Dokumenten. Antworte ausschließlich mit der geforderten JSON-Liste, ohne Erklärung und ohne Codezaun.' },\n {\n role: 'user',\n content: [\n 'Hier ist die Gliederung eines Dokuments als JSON-Liste von Überschriften (i = Index, level = Ebene, text, chars = Zeichen des Textes darunter). Bereinige die Struktur: keine übersprungenen Ebenen; höchstens eine Überschrift der Ebene 1; Nummerierungen, Satzzeichen am Ende und doppelte Leerzeichen entfernen; einheitliche Schreibweise; eine Überschrift ohne Text darunter (chars = 0) mit der folgenden zusammenlegen, indem du ihren text leer lässt (sie wird entfernt). Ändere Bedeutung nicht. Antworte nur mit der JSON-Liste gleicher Länge und Reihenfolge: [{\"i\":0,\"level\":1,\"text\":\"…\"}, …]',\n `Gliederung:\\n${selection}`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n // `selection`: the transferred Baustein as Markdown (see `transferRequest` of the transfer feature).\n 'transfer-summary': {\n id: 'transfer-summary',\n labelKey: 'ai.task.transferSummary.label',\n icon: 'i-lucide-text-quote',\n needsSelection: true,\n needsPrompt: false,\n apply: 'insert-after-block',\n slot: 'generate',\n programmatic: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ selection, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Fasse den folgenden Baustein knapp in ein bis drei Absätzen zusammen. Behalte Fachbegriffe bei. Gib nur die Zusammenfassung als Markdown zurück, ohne Überschrift.',\n `Baustein:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n 'transfer-bullets': {\n id: 'transfer-bullets',\n labelKey: 'ai.task.transferBullets.label',\n icon: 'i-lucide-list',\n needsSelection: true,\n needsPrompt: false,\n apply: 'insert-after-block',\n slot: 'generate',\n programmatic: true,\n // i18n-ignore-start: model-facing prompt\n messages: ({ selection, blocks }) => [\n { role: 'system', content: aiSystem(blocks) },\n {\n role: 'user',\n content: [\n 'Gib die Kernaussagen des folgenden Bausteins als Aufzählung zurück: eine Markdown-Liste, ein Punkt je Aussage, knapp und vollständig. Gib nur die Liste zurück, ohne Überschrift.',\n `Baustein:\\n\"\"\"\\n${selection}\\n\"\"\"`,\n ].join('\\n\\n'),\n },\n ],\n // i18n-ignore-end\n },\n}\n\n/** In-block AI tasks (`rewrite` slot): rewrite text inside one block or a selection. */\nexport const inBlockTasks = (): AiTask[] => Object.values(AI_TASKS).filter(task => task.slot === 'rewrite')\n\n/** Block generation tasks (`generate` slot): produce new blocks after the caret or at the end of the document. */\nexport const blockTasks = (): AiTask[] => Object.values(AI_TASKS).filter(task => task.slot === 'generate' && !task.needsBlock && !task.programmatic)\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { parseBlocks } from "../../../core/pipeline.js";
1
+ import { AI_LANGUAGE_KEYS } from "../protocol/models.js";
2
2
  import { AI_LANGUAGES } from "../../../settings/values.js";
3
+ import { parseBlocks } from "../../../core/pipeline.js";
3
4
  import { resolveRange } from "../../../core/doc-anchors.js";
4
- import { AI_LANGUAGE_KEYS } from "../protocol/models.js";
5
5
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
6
6
  import { Icon } from "../../../react/Icon.js";
7
7
  import { useStore } from "../../../react/use-store.js";
@@ -1,6 +1,6 @@
1
- import { conversionsOf } from "../../../core/pipeline.js";
2
- import { CHAT_LIMITS } from "../protocol/chat.js";
3
1
  import { AI_LIMITS, blockTasks, withTaskHeading } from "../protocol/tasks.js";
2
+ import { CHAT_LIMITS } from "../protocol/chat.js";
3
+ import { conversionsOf } from "../../../core/pipeline.js";
4
4
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
5
5
  import { cn } from "../../../react/ui/utils.js";
6
6
  import { Icon } from "../../../react/Icon.js";
@@ -1,5 +1,5 @@
1
- import { AI_LANGUAGES, MODEL_SLOTS } from "../../../settings/values.js";
2
1
  import { AI_LANGUAGE_KEYS, MODEL_SLOT_KEYS } from "../protocol/models.js";
2
+ import { AI_LANGUAGES, MODEL_SLOTS } from "../../../settings/values.js";
3
3
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
4
4
  import { useEditorSettings } from "../../../react/settings.js";
5
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../react/ui/select.js";
@@ -1,5 +1,5 @@
1
- import { fetchErrorMessage } from "../../../core/fetch-error.js";
2
1
  import { modelRefSchema } from "../protocol/models.js";
2
+ import { fetchErrorMessage } from "../../../core/fetch-error.js";
3
3
  import { useEditorContext } from "../../../react/context.js";
4
4
  import { useEditorI18n } from "../../../react/use-editor-i18n.js";
5
5
  import { Icon } from "../../../react/Icon.js";
@@ -1,10 +1,10 @@
1
+ import { AI_LIMITS, AI_TASKS } from "./protocol/tasks.js";
1
2
  import { ABILITY } from "../../core/schema/abilities.js";
2
3
  import { abilitiesAtSelection, hasAbility } from "../../core/abilities.js";
3
4
  import { aiContextOf } from "../../core/pipeline.js";
4
5
  import { createStore } from "../../core/store.js";
5
6
  import { captureRange, releaseRanges, resolveRange } from "../../core/doc-anchors.js";
6
7
  import { selectionRange } from "../../core/selection-range.js";
7
- import { AI_LIMITS, AI_TASKS } from "./protocol/tasks.js";
8
8
  import { aiApplyFailure, applyAiResult, documentMarkdown, insertAtCaretLine, rangeMarkdown } from "./ai-apply.js";
9
9
  import { hideAiSelectionHighlight, showAiSelectionHighlight } from "./ai-selection-highlight.js";
10
10
  import { aiMenuActionsOf } from "./ai-menu.js";
@@ -1,7 +1,7 @@
1
+ import { AI_LIMITS, withTaskHeading } from "./protocol/tasks.js";
1
2
  import { aiContextOf } from "../../core/pipeline.js";
2
3
  import { createStore } from "../../core/store.js";
3
4
  import { releaseRanges } from "../../core/doc-anchors.js";
4
- import { AI_LIMITS, withTaskHeading } from "./protocol/tasks.js";
5
5
  import { aiApplyFailure, applyAiResult, captureTarget, documentMarkdown, formatTaskResult, insertBlocksAtTarget } from "./ai-apply.js";
6
6
  import { selectionScope } from "./agent-ops.js";
7
7
  //#region src/features/ai/sidebar-flow.ts
@@ -1,5 +1,5 @@
1
1
  import { MenuItem } from "../../core/command-types.js";
2
- import { BlockAction, BlockActionContext, BlockTarget } from "../../core/extension-points.js";
2
+ import { BlockAction, BlockActionContext, BlockActionSurface, BlockTarget } from "../../core/extension-points.js";
3
3
  import "../../core/index.js";
4
4
  import { Editor } from "@tiptap/core";
5
5
  //#region src/features/block-ui/block-actions.d.ts
@@ -11,8 +11,24 @@ export interface BlockActionEntry {
11
11
  }
12
12
  /** Hovering a composite's title/description/tags row addresses the composite itself. */
13
13
  export declare function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget;
14
- /** The actions `target` offers; a `menu` action only when its menu has items. */
15
- export declare function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[]): BlockActionEntry[];
14
+ /**
15
+ * The surfaces that render block actions for `editor` (its surface's `BlockUiOptions.actions`, hover by default). Pass the
16
+ * raw editor instance (`editorEntryOf`).
17
+ */
18
+ export declare function actionSurfacesOf(editor: Editor): Record<BlockActionSurface, boolean>;
19
+ /** Whether `hovered` and `selected` address the same block (hovering composite chrome counts as hovering the composite). */
20
+ export declare function sameBlock(editor: Editor, hovered: BlockTarget, selected: BlockTarget): boolean;
21
+ /**
22
+ * The actions `target` offers on `surface`: those whose `placement` (default both) includes it, whose `ability` the
23
+ * block has and whose `isVisible` does not refuse. A `menu` action only when its menu has items.
24
+ */
25
+ export declare function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[], surface: BlockActionSurface): BlockActionEntry[];
26
+ /**
27
+ * The block holding the selection, for the toolbar surface: the selected node of a node selection, else the innermost
28
+ * block around the caret (composite chrome resolves to its composite). Null when the editor is not editable, when the
29
+ * selection spans several top-level blocks, or when it sits outside any block.
30
+ */
31
+ export declare function selectedBlockOf(editor: Editor): BlockTarget | null;
16
32
  /**
17
33
  * Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu
18
34
  * buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row
@@ -1,5 +1,7 @@
1
1
  import { abilitiesAt } from "../../core/abilities.js";
2
+ import { editorEntryOf } from "../../core/editor-ui.js";
2
3
  import { isChromeNode } from "../../core/schema/node-flags.js";
4
+ import { NodeSelection } from "@tiptap/pm/state";
3
5
  //#region src/features/block-ui/block-actions.ts
4
6
  /** Hovering a composite's title/description/tags row addresses the composite itself. */
5
7
  function effectiveTarget(editor, target) {
@@ -10,14 +12,35 @@ function effectiveTarget(editor, target) {
10
12
  pos: $pos.before()
11
13
  };
12
14
  }
13
- /** The actions `target` offers; a `menu` action only when its menu has items. */
14
- function blockActionsFor(editor, target, actions) {
15
+ /**
16
+ * The surfaces that render block actions for `editor` (its surface's `BlockUiOptions.actions`, hover by default). Pass the
17
+ * raw editor instance (`editorEntryOf`).
18
+ */
19
+ function actionSurfacesOf(editor) {
20
+ const mode = editorEntryOf(editor)?.blockUi?.actions ?? "hover";
21
+ return {
22
+ hover: mode === "hover" || mode === "both",
23
+ toolbar: mode === "toolbar" || mode === "both"
24
+ };
25
+ }
26
+ /** Whether `hovered` and `selected` address the same block (hovering composite chrome counts as hovering the composite). */
27
+ function sameBlock(editor, hovered, selected) {
28
+ if (hovered.pos < 0 || hovered.pos > editor.state.doc.content.size) return false;
29
+ return effectiveTarget(editor, hovered).pos === selected.pos;
30
+ }
31
+ /**
32
+ * The actions `target` offers on `surface`: those whose `placement` (default both) includes it, whose `ability` the
33
+ * block has and whose `isVisible` does not refuse. A `menu` action only when its menu has items.
34
+ */
35
+ function blockActionsFor(editor, target, actions, surface) {
15
36
  const ctx = {
16
37
  ...target,
17
38
  editor,
18
39
  abilities: abilitiesAt(editor, target.pos, target.node)
19
40
  };
20
41
  return actions.flatMap((action) => {
42
+ const placement = action.placement ?? "both";
43
+ if (placement !== "both" && placement !== surface) return [];
21
44
  if (action.ability && !ctx.abilities.has(action.ability) || action.isVisible?.(ctx) === false) return [];
22
45
  if (!action.menu) return [{
23
46
  action,
@@ -32,6 +55,29 @@ function blockActionsFor(editor, target, actions) {
32
55
  });
33
56
  }
34
57
  /**
58
+ * The block holding the selection, for the toolbar surface: the selected node of a node selection, else the innermost
59
+ * block around the caret (composite chrome resolves to its composite). Null when the editor is not editable, when the
60
+ * selection spans several top-level blocks, or when it sits outside any block.
61
+ */
62
+ function selectedBlockOf(editor) {
63
+ if (editor.isDestroyed || !editor.isEditable) return null;
64
+ const { selection } = editor.state;
65
+ if (selection instanceof NodeSelection && selection.node.isBlock) return effectiveTarget(editor, {
66
+ node: selection.node,
67
+ pos: selection.from
68
+ });
69
+ const { $from } = selection;
70
+ if ($from.sharedDepth(selection.to) < 1) return null;
71
+ for (let depth = $from.depth; depth >= 1; depth--) {
72
+ const node = $from.node(depth);
73
+ if (node.isBlock && !isChromeNode(node)) return {
74
+ node,
75
+ pos: $from.before(depth)
76
+ };
77
+ }
78
+ return null;
79
+ }
80
+ /**
35
81
  * Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu
36
82
  * buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row
37
83
  * instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.
@@ -56,6 +102,6 @@ function blockCornerOf(dom) {
56
102
  };
57
103
  }
58
104
  //#endregion
59
- export { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, effectiveTarget };
105
+ export { BLOCK_CORNER_ATTR, actionSurfacesOf, blockActionsFor, blockCornerOf, effectiveTarget, sameBlock, selectedBlockOf };
60
106
 
61
107
  //# sourceMappingURL=block-actions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"block-actions.js","names":[],"sources":["../../../src/features/block-ui/block-actions.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { abilitiesAt, type BlockAction, type BlockActionContext, type BlockTarget, type MenuItem } from '../../core'\nimport { isChromeNode } from '../../core/schema'\n\n/** An action `target` offers, with the context it runs with and, for a `menu` action, its items. */\nexport interface BlockActionEntry {\n action: BlockAction\n ctx: BlockActionContext\n menu?: MenuItem[]\n}\n\n/** Hovering a composite's title/description/tags row addresses the composite itself. */\nexport function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget {\n if (!isChromeNode(target.node)) return target\n const $pos = editor.state.doc.resolve(target.pos)\n return { node: $pos.parent, pos: $pos.before() }\n}\n\n/** The actions `target` offers; a `menu` action only when its menu has items. */\nexport function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[]): BlockActionEntry[] {\n const ctx: BlockActionContext = { ...target, editor, abilities: abilitiesAt(editor, target.pos, target.node) }\n return actions.flatMap((action): BlockActionEntry[] => {\n if ((action.ability && !ctx.abilities.has(action.ability)) || action.isVisible?.(ctx) === false) return []\n if (!action.menu) return [{ action, ctx }]\n const menu = action.menu(ctx)\n return menu.length ? [{ action, ctx, menu }] : []\n })\n}\n\n/**\n * Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu\n * buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row\n * instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.\n */\nexport const BLOCK_CORNER_ATTR = 'data-block-corner'\n\n/** Where a block's corner elements sit, in viewport coordinates. */\nexport interface BlockCorner {\n /** Left edge of the leftmost corner element. */\n left: number\n /** Vertical centre of the first corner element. */\n centerY: number\n /** The marked elements, for a host that wants to watch them for size changes. */\n elements: HTMLElement[]\n}\n\n/**\n * The corner elements `dom` (a block's node view root) marks with `BLOCK_CORNER_ATTR`, or null when none is shown.\n * Elements inside a nested block's own node view belong to that block and do not count; neither do hidden ones (no\n * layout box).\n */\nexport function blockCornerOf(dom: HTMLElement): BlockCorner | null {\n const elements = [...dom.querySelectorAll<HTMLElement>(`[${BLOCK_CORNER_ATTR}]`)].filter((element) => {\n const owner = element.closest('[data-node-view-wrapper]')\n return !(owner && owner !== dom && dom.contains(owner))\n })\n const rects = elements.map(element => element.getBoundingClientRect()).filter(rect => rect.width > 0 && rect.height > 0)\n if (!rects.length) return null\n return {\n left: Math.min(...rects.map(rect => rect.left)),\n centerY: rects[0]!.top + rects[0]!.height / 2,\n elements,\n }\n}\n"],"mappings":";;;;AAYA,SAAgB,gBAAgB,QAAgB,QAAkC;CAChF,IAAI,CAAC,aAAa,OAAO,IAAI,GAAG,OAAO;CACvC,MAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,OAAO,GAAG;CAChD,OAAO;EAAE,MAAM,KAAK;EAAQ,KAAK,KAAK,OAAO;CAAE;AACjD;;AAGA,SAAgB,gBAAgB,QAAgB,QAAqB,SAAqD;CACxH,MAAM,MAA0B;EAAE,GAAG;EAAQ;EAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,OAAO,IAAI;CAAE;CAC7G,OAAO,QAAQ,SAAS,WAA+B;EACrD,IAAK,OAAO,WAAW,CAAC,IAAI,UAAU,IAAI,OAAO,OAAO,KAAM,OAAO,YAAY,GAAG,MAAM,OAAO,OAAO,CAAC;EACzG,IAAI,CAAC,OAAO,MAAM,OAAO,CAAC;GAAE;GAAQ;EAAI,CAAC;EACzC,MAAM,OAAO,OAAO,KAAK,GAAG;EAC5B,OAAO,KAAK,SAAS,CAAC;GAAE;GAAQ;GAAK;EAAK,CAAC,IAAI,CAAC;CAClD,CAAC;AACH;;;;;;AAOA,MAAa,oBAAoB;;;;;;AAiBjC,SAAgB,cAAc,KAAsC;CAClE,MAAM,WAAW,CAAC,GAAG,IAAI,iBAA8B,IAAI,kBAAkB,EAAE,CAAC,CAAC,CAAC,QAAQ,YAAY;EACpG,MAAM,QAAQ,QAAQ,QAAQ,0BAA0B;EACxD,OAAO,EAAE,SAAS,UAAU,OAAO,IAAI,SAAS,KAAK;CACvD,CAAC;CACD,MAAM,QAAQ,SAAS,KAAI,YAAW,QAAQ,sBAAsB,CAAC,CAAC,CAAC,QAAO,SAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,CAAC;CACvH,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,OAAO;EACL,MAAM,KAAK,IAAI,GAAG,MAAM,KAAI,SAAQ,KAAK,IAAI,CAAC;EAC9C,SAAS,MAAM,EAAE,CAAE,MAAM,MAAM,EAAE,CAAE,SAAS;EAC5C;CACF;AACF"}
1
+ {"version":3,"file":"block-actions.js","names":[],"sources":["../../../src/features/block-ui/block-actions.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { NodeSelection } from '@tiptap/pm/state'\nimport { abilitiesAt, type BlockAction, type BlockActionContext, type BlockActionSurface, type BlockTarget, editorEntryOf, type MenuItem } from '../../core'\nimport { isChromeNode } from '../../core/schema'\n\n/** An action `target` offers, with the context it runs with and, for a `menu` action, its items. */\nexport interface BlockActionEntry {\n action: BlockAction\n ctx: BlockActionContext\n menu?: MenuItem[]\n}\n\n/** Hovering a composite's title/description/tags row addresses the composite itself. */\nexport function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget {\n if (!isChromeNode(target.node)) return target\n const $pos = editor.state.doc.resolve(target.pos)\n return { node: $pos.parent, pos: $pos.before() }\n}\n\n/**\n * The surfaces that render block actions for `editor` (its surface's `BlockUiOptions.actions`, hover by default). Pass the\n * raw editor instance (`editorEntryOf`).\n */\nexport function actionSurfacesOf(editor: Editor): Record<BlockActionSurface, boolean> {\n const mode = editorEntryOf(editor)?.blockUi?.actions ?? 'hover'\n return { hover: mode === 'hover' || mode === 'both', toolbar: mode === 'toolbar' || mode === 'both' }\n}\n\n/** Whether `hovered` and `selected` address the same block (hovering composite chrome counts as hovering the composite). */\nexport function sameBlock(editor: Editor, hovered: BlockTarget, selected: BlockTarget): boolean {\n if (hovered.pos < 0 || hovered.pos > editor.state.doc.content.size) return false\n return effectiveTarget(editor, hovered).pos === selected.pos\n}\n\n/**\n * The actions `target` offers on `surface`: those whose `placement` (default both) includes it, whose `ability` the\n * block has and whose `isVisible` does not refuse. A `menu` action only when its menu has items.\n */\nexport function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[], surface: BlockActionSurface): BlockActionEntry[] {\n const ctx: BlockActionContext = { ...target, editor, abilities: abilitiesAt(editor, target.pos, target.node) }\n return actions.flatMap((action): BlockActionEntry[] => {\n const placement = action.placement ?? 'both'\n if (placement !== 'both' && placement !== surface) return []\n if ((action.ability && !ctx.abilities.has(action.ability)) || action.isVisible?.(ctx) === false) return []\n if (!action.menu) return [{ action, ctx }]\n const menu = action.menu(ctx)\n return menu.length ? [{ action, ctx, menu }] : []\n })\n}\n\n/**\n * The block holding the selection, for the toolbar surface: the selected node of a node selection, else the innermost\n * block around the caret (composite chrome resolves to its composite). Null when the editor is not editable, when the\n * selection spans several top-level blocks, or when it sits outside any block.\n */\nexport function selectedBlockOf(editor: Editor): BlockTarget | null {\n if (editor.isDestroyed || !editor.isEditable) return null\n const { selection } = editor.state\n if (selection instanceof NodeSelection && selection.node.isBlock) {\n return effectiveTarget(editor, { node: selection.node, pos: selection.from })\n }\n const { $from } = selection\n if ($from.sharedDepth(selection.to) < 1) return null\n for (let depth = $from.depth; depth >= 1; depth--) {\n const node = $from.node(depth)\n if (node.isBlock && !isChromeNode(node)) return { node, pos: $from.before(depth) }\n }\n return null\n}\n\n/**\n * Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu\n * buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row\n * instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.\n */\nexport const BLOCK_CORNER_ATTR = 'data-block-corner'\n\n/** Where a block's corner elements sit, in viewport coordinates. */\nexport interface BlockCorner {\n /** Left edge of the leftmost corner element. */\n left: number\n /** Vertical centre of the first corner element. */\n centerY: number\n /** The marked elements, for a host that wants to watch them for size changes. */\n elements: HTMLElement[]\n}\n\n/**\n * The corner elements `dom` (a block's node view root) marks with `BLOCK_CORNER_ATTR`, or null when none is shown.\n * Elements inside a nested block's own node view belong to that block and do not count; neither do hidden ones (no\n * layout box).\n */\nexport function blockCornerOf(dom: HTMLElement): BlockCorner | null {\n const elements = [...dom.querySelectorAll<HTMLElement>(`[${BLOCK_CORNER_ATTR}]`)].filter((element) => {\n const owner = element.closest('[data-node-view-wrapper]')\n return !(owner && owner !== dom && dom.contains(owner))\n })\n const rects = elements.map(element => element.getBoundingClientRect()).filter(rect => rect.width > 0 && rect.height > 0)\n if (!rects.length) return null\n return {\n left: Math.min(...rects.map(rect => rect.left)),\n centerY: rects[0]!.top + rects[0]!.height / 2,\n elements,\n }\n}\n"],"mappings":";;;;;;AAaA,SAAgB,gBAAgB,QAAgB,QAAkC;CAChF,IAAI,CAAC,aAAa,OAAO,IAAI,GAAG,OAAO;CACvC,MAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,OAAO,GAAG;CAChD,OAAO;EAAE,MAAM,KAAK;EAAQ,KAAK,KAAK,OAAO;CAAE;AACjD;;;;;AAMA,SAAgB,iBAAiB,QAAqD;CACpF,MAAM,OAAO,cAAc,MAAM,CAAC,EAAE,SAAS,WAAW;CACxD,OAAO;EAAE,OAAO,SAAS,WAAW,SAAS;EAAQ,SAAS,SAAS,aAAa,SAAS;CAAO;AACtG;;AAGA,SAAgB,UAAU,QAAgB,SAAsB,UAAgC;CAC9F,IAAI,QAAQ,MAAM,KAAK,QAAQ,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,OAAO;CAC3E,OAAO,gBAAgB,QAAQ,OAAO,CAAC,CAAC,QAAQ,SAAS;AAC3D;;;;;AAMA,SAAgB,gBAAgB,QAAgB,QAAqB,SAAiC,SAAiD;CACrJ,MAAM,MAA0B;EAAE,GAAG;EAAQ;EAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,OAAO,IAAI;CAAE;CAC7G,OAAO,QAAQ,SAAS,WAA+B;EACrD,MAAM,YAAY,OAAO,aAAa;EACtC,IAAI,cAAc,UAAU,cAAc,SAAS,OAAO,CAAC;EAC3D,IAAK,OAAO,WAAW,CAAC,IAAI,UAAU,IAAI,OAAO,OAAO,KAAM,OAAO,YAAY,GAAG,MAAM,OAAO,OAAO,CAAC;EACzG,IAAI,CAAC,OAAO,MAAM,OAAO,CAAC;GAAE;GAAQ;EAAI,CAAC;EACzC,MAAM,OAAO,OAAO,KAAK,GAAG;EAC5B,OAAO,KAAK,SAAS,CAAC;GAAE;GAAQ;GAAK;EAAK,CAAC,IAAI,CAAC;CAClD,CAAC;AACH;;;;;;AAOA,SAAgB,gBAAgB,QAAoC;CAClE,IAAI,OAAO,eAAe,CAAC,OAAO,YAAY,OAAO;CACrD,MAAM,EAAE,cAAc,OAAO;CAC7B,IAAI,qBAAqB,iBAAiB,UAAU,KAAK,SACvD,OAAO,gBAAgB,QAAQ;EAAE,MAAM,UAAU;EAAM,KAAK,UAAU;CAAK,CAAC;CAE9E,MAAM,EAAE,UAAU;CAClB,IAAI,MAAM,YAAY,UAAU,EAAE,IAAI,GAAG,OAAO;CAChD,KAAK,IAAI,QAAQ,MAAM,OAAO,SAAS,GAAG,SAAS;EACjD,MAAM,OAAO,MAAM,KAAK,KAAK;EAC7B,IAAI,KAAK,WAAW,CAAC,aAAa,IAAI,GAAG,OAAO;GAAE;GAAM,KAAK,MAAM,OAAO,KAAK;EAAE;CACnF;CACA,OAAO;AACT;;;;;;AAOA,MAAa,oBAAoB;;;;;;AAiBjC,SAAgB,cAAc,KAAsC;CAClE,MAAM,WAAW,CAAC,GAAG,IAAI,iBAA8B,IAAI,kBAAkB,EAAE,CAAC,CAAC,CAAC,QAAQ,YAAY;EACpG,MAAM,QAAQ,QAAQ,QAAQ,0BAA0B;EACxD,OAAO,EAAE,SAAS,UAAU,OAAO,IAAI,SAAS,KAAK;CACvD,CAAC;CACD,MAAM,QAAQ,SAAS,KAAI,YAAW,QAAQ,sBAAsB,CAAC,CAAC,CAAC,QAAO,SAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,CAAC;CACvH,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,OAAO;EACL,MAAM,KAAK,IAAI,GAAG,MAAM,KAAI,SAAQ,KAAK,IAAI,CAAC;EAC9C,SAAS,MAAM,EAAE,CAAE,MAAM,MAAM,EAAE,CAAE,SAAS;EAC5C;CACF;AACF"}
@@ -1,16 +1,21 @@
1
1
  import { useUiKit } from "../../../vue/kit/use-ui-kit.js";
2
2
  import { blockActionsFor, blockCornerOf, effectiveTarget } from "../block-actions.js";
3
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, unref, useTemplateRef, watch, withCtx, withModifiers } from "vue";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, unref, useTemplateRef, watch, withCtx, withModifiers } from "vue";
4
4
  //#region src/features/block-ui/components/BlockActionsOverlay.vue
5
5
  /** Gap between the bar and the editor's right edge. */
6
6
  const EDGE_GUTTER = 8;
7
7
  /** Gap between the bar and the block's own corner elements it docks next to. */
8
8
  const CORNER_GAP = 4;
9
9
  /**
10
- * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and
11
- * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
12
- * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
13
- * read-only. `menu` actions open a dropdown of their items instead of running on click.
10
+ * The actions of a block on one surface, right-aligned at the editor's text column (the drag handle sits on the block's
11
+ * left and targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the
12
+ * composite chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the
13
+ * editor is read-only. `menu` actions open a dropdown of their items instead of running on click.
14
+ *
15
+ * `surface` selects the actions by their `placement`. The `hover` surface follows the pointer (`target` is the hovered
16
+ * block, `hover` events keep it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target`
17
+ * is the block holding the selection, the bar shows as long as it is set, sends no `hover` events and carries the class
18
+ * `block-actions-toolbar` for host styling.
14
19
  *
15
20
  * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
16
21
  * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
@@ -22,13 +27,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
22
27
  editor: {},
23
28
  target: {},
24
29
  actions: {},
25
- host: {}
30
+ host: {},
31
+ surface: {},
32
+ persistent: { type: Boolean }
26
33
  },
27
34
  emits: ["hover"],
28
35
  setup(__props, { emit: __emit }) {
29
36
  const props = __props;
30
37
  const { Button: UButton, DropdownMenu: UDropdownMenu, Tooltip: UTooltip } = useUiKit();
31
38
  const emit = __emit;
39
+ /** Reports pointer presence to the host; a persistent bar has none to report. */
40
+ function reportHover(hovered) {
41
+ if (!props.persistent) emit("hover", hovered);
42
+ }
32
43
  /** The target in the current document; null once its position holds something else (deleted, replaced). */
33
44
  const block = computed(() => {
34
45
  const { editor, target } = props;
@@ -46,7 +57,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
46
57
  props.editor.state;
47
58
  return props.editor.isEditable;
48
59
  });
49
- const items = computed(() => block.value ? blockActionsFor(props.editor, block.value, props.actions) : []);
60
+ const items = computed(() => block.value ? blockActionsFor(props.editor, block.value, props.actions, props.surface) : []);
50
61
  const top = ref(0);
51
62
  const right = ref(0);
52
63
  /** Whether the bar is docked next to the block's corner elements: `top` is then their vertical centre. */
@@ -86,15 +97,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
86
97
  const menuOpen = ref(false);
87
98
  function onMenuOpen(open) {
88
99
  menuOpen.value = open;
89
- emit("hover", open || !!root.value?.matches(":hover"));
100
+ reportHover(open || !!root.value?.matches(":hover"));
90
101
  }
91
102
  function onMouseLeave() {
92
- if (!menuOpen.value) emit("hover", false);
103
+ if (!menuOpen.value) reportHover(false);
93
104
  }
94
105
  watch(visible, (value) => {
95
106
  if (value) return;
96
107
  menuOpen.value = false;
97
- emit("hover", false);
108
+ reportHover(false);
98
109
  });
99
110
  let resizeObserver;
100
111
  onMounted(() => {
@@ -115,13 +126,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
115
126
  key: 0,
116
127
  ref_key: "root",
117
128
  ref: root,
118
- class: "block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5",
129
+ class: normalizeClass(["block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5", { "block-actions-toolbar": __props.persistent }]),
119
130
  style: normalizeStyle({
120
131
  top: `${top.value}px`,
121
132
  right: `${right.value}px`,
122
133
  transform: docked.value ? "translateY(-50%)" : void 0
123
134
  }),
124
- onMouseenter: _cache[2] || (_cache[2] = ($event) => emit("hover", true)),
135
+ onMouseenter: _cache[2] || (_cache[2] = ($event) => reportHover(true)),
125
136
  onMouseleave: onMouseLeave
126
137
  }, [(openBlock(true), createElementBlock(Fragment, null, renderList(items.value, (entry) => {
127
138
  return openBlock(), createElementBlock(Fragment, { key: entry.action.id }, [entry.menu ? (openBlock(), createBlock(unref(UDropdownMenu), {
@@ -161,7 +172,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
161
172
  ])]),
162
173
  _: 2
163
174
  }, 1032, ["text"]))], 64);
164
- }), 128))], 36)) : createCommentVNode("v-if", true);
175
+ }), 128))], 38)) : createCommentVNode("v-if", true);
165
176
  };
166
177
  }
167
178
  });
@@ -1 +1 @@
1
- {"version":3,"file":"BlockActionsOverlay.js","names":["_openBlock","_createElementBlock","_normalizeStyle","_Fragment","_renderList","_createBlock","_unref","_withCtx","_createVNode","_withModifiers","_createCommentVNode"],"sources":["../../../../src/features/block-ui/components/BlockActionsOverlay.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport type { Editor } from '@tiptap/vue-3'\nimport { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport type { BlockAction, BlockTarget } from '../../../core'\nimport { blockActionsFor, blockCornerOf, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n/** Gap between the bar and the block's own corner elements it docks next to. */\nconst CORNER_GAP = 4\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n *\n * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):\n * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements\n * it sits at the editor's right edge, aligned to the block's top.\n */\nconst props = defineProps<{\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n}>()\n\nconst { Button: UButton, DropdownMenu: UDropdownMenu, Tooltip: UTooltip } = useUiKit()\n\nconst emit = defineEmits<{ hover: [hovered: boolean] }>()\n\n/** The target in the current document; null once its position holds something else (deleted, replaced). */\nconst block = computed<BlockTarget | null>(() => {\n const { editor, target } = props\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n})\n\n// Reading `state` makes the flag follow `setEditable` (which dispatches a transaction).\nconst editable = computed(() => {\n void props.editor.state\n return props.editor.isEditable\n})\n\nconst items = computed(() => (block.value ? blockActionsFor(props.editor, block.value, props.actions) : []))\n\nconst top = ref(0)\nconst right = ref(0)\n/** Whether the bar is docked next to the block's corner elements: `top` is then their vertical centre. */\nconst docked = ref(false)\nconst measured = ref(false)\n\nlet watched: HTMLElement[] = []\n// The corner elements can change size (a badge appears) without the document changing: watch the current ones.\nfunction watchCorner(elements: HTMLElement[]) {\n if (elements.length === watched.length && elements.every((element, index) => element === watched[index])) return\n for (const element of watched) resizeObserver?.unobserve(element)\n watched = elements\n for (const element of watched) resizeObserver?.observe(element)\n}\n\nfunction measure() {\n const { editor, host } = props\n const dom = block.value && host && !editor.isDestroyed ? editor.view.nodeDOM(block.value.pos) : null\n measured.value = dom instanceof HTMLElement\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n docked.value = corner !== null\n if (corner) {\n top.value = corner.centerY - hostRect.top\n right.value = hostRect.right - corner.left + CORNER_GAP\n return\n }\n const rect = dom.getBoundingClientRect()\n top.value = rect.top - hostRect.top + 2\n right.value = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n}\n\nwatch([block, () => props.editor.state], measure, { flush: 'post' })\n\nconst visible = computed(() => editable.value && items.value.length > 0 && measured.value)\n\n// An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\nconst root = useTemplateRef<HTMLElement>('root')\nconst menuOpen = ref(false)\nfunction onMenuOpen(open: boolean) {\n menuOpen.value = open\n emit('hover', open || !!root.value?.matches(':hover'))\n}\nfunction onMouseLeave() {\n if (!menuOpen.value) emit('hover', false)\n}\n\n// A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\nwatch(visible, (value) => {\n if (value) return\n menuOpen.value = false\n emit('hover', false)\n})\n\n// The bar follows the editor's right edge when the column resizes without the window doing so.\nlet resizeObserver: ResizeObserver | undefined\nonMounted(() => {\n window.addEventListener('resize', measure)\n window.addEventListener('scroll', measure, true)\n resizeObserver = new ResizeObserver(measure)\n resizeObserver.observe(props.editor.view.dom)\n for (const element of watched) resizeObserver.observe(element)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', measure)\n window.removeEventListener('scroll', measure, true)\n resizeObserver?.disconnect()\n watched = []\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n ref=\"root\"\n class=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5\"\n :style=\"{ top: `${top}px`, right: `${right}px`, transform: docked ? 'translateY(-50%)' : undefined }\"\n @mouseenter=\"emit('hover', true)\"\n @mouseleave=\"onMouseLeave\"\n >\n <template v-for=\"entry in items\" :key=\"entry.action.id\">\n <UDropdownMenu v-if=\"entry.menu\" :items=\"entry.menu\" :content=\"{ align: 'end' }\" @update:open=\"onMenuOpen\">\n <UTooltip :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n />\n </UTooltip>\n </UDropdownMenu>\n <UTooltip v-else :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n @click=\"entry.action.run?.(entry.ctx)\"\n />\n </UTooltip>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"BlockActionsOverlay.js","names":["_openBlock","_createElementBlock","_normalizeClass","_normalizeStyle","_Fragment","_renderList","_createBlock","_unref","_withCtx","_createVNode","_withModifiers","_createCommentVNode"],"sources":["../../../../src/features/block-ui/components/BlockActionsOverlay.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport type { Editor } from '@tiptap/vue-3'\nimport { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport type { BlockAction, BlockActionSurface, BlockTarget } from '../../../core'\nimport { blockActionsFor, blockCornerOf, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n/** Gap between the bar and the block's own corner elements it docks next to. */\nconst CORNER_GAP = 4\n\n/**\n * The actions of a block on one surface, right-aligned at the editor's text column (the drag handle sits on the block's\n * left and targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the\n * composite chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the\n * editor is read-only. `menu` actions open a dropdown of their items instead of running on click.\n *\n * `surface` selects the actions by their `placement`. The `hover` surface follows the pointer (`target` is the hovered\n * block, `hover` events keep it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target`\n * is the block holding the selection, the bar shows as long as it is set, sends no `hover` events and carries the class\n * `block-actions-toolbar` for host styling.\n *\n * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):\n * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements\n * it sits at the editor's right edge, aligned to the block's top.\n */\nconst props = defineProps<{\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n surface: BlockActionSurface\n persistent?: boolean\n}>()\n\nconst { Button: UButton, DropdownMenu: UDropdownMenu, Tooltip: UTooltip } = useUiKit()\n\nconst emit = defineEmits<{ hover: [hovered: boolean] }>()\n\n/** Reports pointer presence to the host; a persistent bar has none to report. */\nfunction reportHover(hovered: boolean) {\n if (!props.persistent) emit('hover', hovered)\n}\n\n/** The target in the current document; null once its position holds something else (deleted, replaced). */\nconst block = computed<BlockTarget | null>(() => {\n const { editor, target } = props\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n})\n\n// Reading `state` makes the flag follow `setEditable` (which dispatches a transaction).\nconst editable = computed(() => {\n void props.editor.state\n return props.editor.isEditable\n})\n\nconst items = computed(() => (block.value ? blockActionsFor(props.editor, block.value, props.actions, props.surface) : []))\n\nconst top = ref(0)\nconst right = ref(0)\n/** Whether the bar is docked next to the block's corner elements: `top` is then their vertical centre. */\nconst docked = ref(false)\nconst measured = ref(false)\n\nlet watched: HTMLElement[] = []\n// The corner elements can change size (a badge appears) without the document changing: watch the current ones.\nfunction watchCorner(elements: HTMLElement[]) {\n if (elements.length === watched.length && elements.every((element, index) => element === watched[index])) return\n for (const element of watched) resizeObserver?.unobserve(element)\n watched = elements\n for (const element of watched) resizeObserver?.observe(element)\n}\n\nfunction measure() {\n const { editor, host } = props\n const dom = block.value && host && !editor.isDestroyed ? editor.view.nodeDOM(block.value.pos) : null\n measured.value = dom instanceof HTMLElement\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n docked.value = corner !== null\n if (corner) {\n top.value = corner.centerY - hostRect.top\n right.value = hostRect.right - corner.left + CORNER_GAP\n return\n }\n const rect = dom.getBoundingClientRect()\n top.value = rect.top - hostRect.top + 2\n right.value = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n}\n\nwatch([block, () => props.editor.state], measure, { flush: 'post' })\n\nconst visible = computed(() => editable.value && items.value.length > 0 && measured.value)\n\n// An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\nconst root = useTemplateRef<HTMLElement>('root')\nconst menuOpen = ref(false)\nfunction onMenuOpen(open: boolean) {\n menuOpen.value = open\n reportHover(open || !!root.value?.matches(':hover'))\n}\nfunction onMouseLeave() {\n if (!menuOpen.value) reportHover(false)\n}\n\n// A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\nwatch(visible, (value) => {\n if (value) return\n menuOpen.value = false\n reportHover(false)\n})\n\n// The bar follows the editor's right edge when the column resizes without the window doing so.\nlet resizeObserver: ResizeObserver | undefined\nonMounted(() => {\n window.addEventListener('resize', measure)\n window.addEventListener('scroll', measure, true)\n resizeObserver = new ResizeObserver(measure)\n resizeObserver.observe(props.editor.view.dom)\n for (const element of watched) resizeObserver.observe(element)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', measure)\n window.removeEventListener('scroll', measure, true)\n resizeObserver?.disconnect()\n watched = []\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n ref=\"root\"\n class=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5\"\n :class=\"{ 'block-actions-toolbar': persistent }\"\n :style=\"{ top: `${top}px`, right: `${right}px`, transform: docked ? 'translateY(-50%)' : undefined }\"\n @mouseenter=\"reportHover(true)\"\n @mouseleave=\"onMouseLeave\"\n >\n <template v-for=\"entry in items\" :key=\"entry.action.id\">\n <UDropdownMenu v-if=\"entry.menu\" :items=\"entry.menu\" :content=\"{ align: 'end' }\" @update:open=\"onMenuOpen\">\n <UTooltip :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n />\n </UTooltip>\n </UDropdownMenu>\n <UTooltip v-else :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n @click=\"entry.action.run?.(entry.ctx)\"\n />\n </UTooltip>\n </template>\n </div>\n</template>\n"],"mappings":";;;;;AAQA,MAAM,cAAc;;AAEpB,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBnB,MAAM,QAAQ;EASd,MAAM,EAAE,QAAQ,SAAS,cAAc,eAAe,SAAS,aAAa,SAAS;EAErF,MAAM,OAAO;;EAGb,SAAS,YAAY,SAAkB;GACrC,IAAI,CAAC,MAAM,YAAY,KAAK,SAAS,OAAO;EAC9C;;EAGA,MAAM,QAAQ,eAAmC;GAC/C,MAAM,EAAE,QAAQ,WAAW;GAC3B,IAAI,CAAC,UAAU,OAAO,aAAa,OAAO;GAC1C,MAAM,EAAE,QAAQ,OAAO;GACvB,IAAI,OAAO,MAAM,KAAK,OAAO,OAAO,IAAI,QAAQ,MAAM,OAAO;GAC7D,MAAM,OAAO,IAAI,OAAO,OAAO,GAAG;GAClC,IAAI,CAAC,QAAQ,KAAK,SAAS,OAAO,KAAK,MAAM,OAAO;GACpD,OAAO,gBAAgB,QAAQ;IAAE;IAAM,KAAK,OAAO;GAAI,CAAC;EAC1D,CAAC;EAGD,MAAM,WAAW,eAAe;GAC9B,MAAW,OAAO;GAClB,OAAO,MAAM,OAAO;EACtB,CAAC;EAED,MAAM,QAAQ,eAAgB,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,MAAM,OAAO,MAAM,SAAS,MAAM,OAAO,IAAI,CAAC,CAAE;EAE1H,MAAM,MAAM,IAAI,CAAC;EACjB,MAAM,QAAQ,IAAI,CAAC;;EAEnB,MAAM,SAAS,IAAI,KAAK;EACxB,MAAM,WAAW,IAAI,KAAK;EAE1B,IAAI,UAAyB,CAAC;EAE9B,SAAS,YAAY,UAAyB;GAC5C,IAAI,SAAS,WAAW,QAAQ,UAAU,SAAS,OAAO,SAAS,UAAU,YAAY,QAAQ,MAAM,GAAG;GAC1G,KAAK,MAAM,WAAW,SAAS,gBAAgB,UAAU,OAAO;GAChE,UAAU;GACV,KAAK,MAAM,WAAW,SAAS,gBAAgB,QAAQ,OAAO;EAChE;EAEA,SAAS,UAAU;GACjB,MAAM,EAAE,QAAQ,SAAS;GACzB,MAAM,MAAM,MAAM,SAAS,QAAQ,CAAC,OAAO,cAAc,OAAO,KAAK,QAAQ,MAAM,MAAM,GAAG,IAAI;GAChG,SAAS,QAAQ,eAAe;GAChC,IAAI,EAAE,eAAe,gBAAgB,CAAC,MAAM;IAC1C,YAAY,CAAC,CAAC;IACd;GACF;GACA,MAAM,WAAW,KAAK,sBAAsB;GAC5C,MAAM,SAAS,cAAc,GAAG;GAChC,YAAY,QAAQ,YAAY,CAAC,CAAC;GAClC,OAAO,QAAQ,WAAW;GAC1B,IAAI,QAAQ;IACV,IAAI,QAAQ,OAAO,UAAU,SAAS;IACtC,MAAM,QAAQ,SAAS,QAAQ,OAAO,OAAO;IAC7C;GACF;GACA,MAAM,OAAO,IAAI,sBAAsB;GACvC,IAAI,QAAQ,KAAK,MAAM,SAAS,MAAM;GACtC,MAAM,QAAQ,SAAS,QAAQ,OAAO,KAAK,IAAI,sBAAsB,CAAC,CAAC,QAAQ;EACjF;EAEA,MAAM,CAAC,aAAa,MAAM,OAAO,KAAK,GAAG,SAAS,EAAE,OAAO,OAAO,CAAC;EAEnE,MAAM,UAAU,eAAe,SAAS,SAAS,MAAM,MAAM,SAAS,KAAK,SAAS,KAAK;EAGzF,MAAM,OAAO,eAA4B,MAAM;EAC/C,MAAM,WAAW,IAAI,KAAK;EAC1B,SAAS,WAAW,MAAe;GACjC,SAAS,QAAQ;GACjB,YAAY,QAAQ,CAAC,CAAC,KAAK,OAAO,QAAQ,QAAQ,CAAC;EACrD;EACA,SAAS,eAAe;GACtB,IAAI,CAAC,SAAS,OAAO,YAAY,KAAK;EACxC;EAGA,MAAM,UAAU,UAAU;GACxB,IAAI,OAAO;GACX,SAAS,QAAQ;GACjB,YAAY,KAAK;EACnB,CAAC;EAGD,IAAI;EACJ,gBAAgB;GACd,OAAO,iBAAiB,UAAU,OAAO;GACzC,OAAO,iBAAiB,UAAU,SAAS,IAAI;GAC/C,iBAAiB,IAAI,eAAe,OAAO;GAC3C,eAAe,QAAQ,MAAM,OAAO,KAAK,GAAG;GAC5C,KAAK,MAAM,WAAW,SAAS,eAAe,QAAQ,OAAO;EAC/D,CAAC;EAED,sBAAsB;GACpB,OAAO,oBAAoB,UAAU,OAAO;GAC5C,OAAO,oBAAoB,UAAU,SAAS,IAAI;GAClD,gBAAgB,WAAW;GAC3B,UAAU,CAAC;EACb,CAAC;;GAKS,OAAA,QAAA,SADRA,UAAA,GAAAC,mBAkCM,OAAA;;IAhCA,SAAA;IAAJ,KAAI;IACJ,OAAKC,eAAA,CAAC,+EAA6E,EAAA,yBAChD,QAAA,WAAU,CAAA,CAAA;IAC5C,OAAKC,eAAA;KAAA,KAAA,GAAY,IAAA,MAAG;KAAA,OAAA,GAAgB,MAAA,MAAK;KAAA,WAAiB,OAAA,QAAM,qBAAwB,KAAA;IAAS,CAAA;IACjG,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,YAAW,IAAA;IACvB,cAAY;GAEb,GAAA,EAAAH,UAAA,IAAA,GAAAC,mBAwBWG,UAAA,MAAAC,WAxBe,MAAA,QAAT,UAAK;IAAiB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,MAAM,OAAO,GAAA,GAC7B,CAAA,MAAM,QAA3BL,UAAA,GAAAM,YAWgBC,MAAA,aAAA,GAAA;;KAXkB,OAAO,MAAM;KAAO,SAAS,EAAA,OAAA,MAAA;KAAmB,iBAAa;;KAC7F,SAAAC,cASW,CATXC,YASWF,MAAA,QAAA,GAAA,EATA,MAAM,MAAM,OAAO,MAAA,GAAA;MAC5B,SAAAC,cAOE,CAPFC,YAOEF,MAAA,OAAA,GAAA;OANA,MAAK;OACL,OAAM;OACN,SAAQ;OACP,MAAM,MAAM,OAAO;OACnB,cAAY,MAAM,OAAO;OACzB,aAAS,OAAA,OAAA,OAAA,KAAAG,oBAAV,CAAA,GAAkB,CAAA,SAAA,CAAA;;;;;IAIxB,GAAA,MAAA,CAAA,OAAA,CAAA,MAAAV,UAAA,GAAAM,YAUWC,MAAA,QAAA,GAAA;;KAVO,MAAM,MAAM,OAAO;;KACnC,SAAAC,cAQE,CARFC,YAQEF,MAAA,OAAA,GAAA;MAPA,MAAK;MACL,OAAM;MACN,SAAQ;MACP,MAAM,MAAM,OAAO;MACnB,cAAY,MAAM,OAAO;MACzB,aAAS,OAAA,OAAA,OAAA,KAAAG,oBAAV,CAAA,GAAkB,CAAA,SAAA,CAAA;MACjB,UAAK,WAAE,MAAM,OAAO,MAAM,MAAM,GAAG"}
@@ -1,12 +1,17 @@
1
- import { BlockAction, BlockTarget } from "../../../core/extension-points.js";
1
+ import { BlockAction, BlockActionSurface, BlockTarget } from "../../../core/extension-points.js";
2
2
  import "../../../core/index.js";
3
3
  import { Editor } from "@tiptap/vue-3";
4
4
  //#region src/features/block-ui/components/BlockActionsOverlay.vue.d.ts
5
5
  /**
6
- * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and
7
- * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
8
- * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
9
- * read-only. `menu` actions open a dropdown of their items instead of running on click.
6
+ * The actions of a block on one surface, right-aligned at the editor's text column (the drag handle sits on the block's
7
+ * left and targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the
8
+ * composite chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the
9
+ * editor is read-only. `menu` actions open a dropdown of their items instead of running on click.
10
+ *
11
+ * `surface` selects the actions by their `placement`. The `hover` surface follows the pointer (`target` is the hovered
12
+ * block, `hover` events keep it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target`
13
+ * is the block holding the selection, the bar shows as long as it is set, sends no `hover` events and carries the class
14
+ * `block-actions-toolbar` for host styling.
10
15
  *
11
16
  * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
12
17
  * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
@@ -17,6 +22,8 @@ type __VLS_Props = {
17
22
  target: BlockTarget | null;
18
23
  actions: readonly BlockAction[];
19
24
  host: HTMLElement | null;
25
+ surface: BlockActionSurface;
26
+ persistent?: boolean;
20
27
  };
21
28
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
22
29
  hover: (hovered: boolean) => any;