@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
@@ -3,6 +3,7 @@ import { isChromeNode } from "../../../core/schema/node-flags.js";
3
3
  import { useUiKit } from "../../../vue/kit/use-ui-kit.js";
4
4
  import { useEditorSettings } from "../../../vue/settings.js";
5
5
  import { firstLineCenterShift, gutterShiftOf, handleLeftOf, hasKindIcon } from "../handle-geometry.js";
6
+ import { actionSurfacesOf, sameBlock, selectedBlockOf } from "../block-actions.js";
6
7
  import _sfc_main$1 from "./BlockActionsOverlay.js";
7
8
  import _sfc_main$2 from "./BlockHandles.js";
8
9
  import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, shallowRef, toRaw, unref, useTemplateRef, withCtx } from "vue";
@@ -10,10 +11,12 @@ import { DragHandle } from "@tiptap/extension-drag-handle-vue-3";
10
11
  //#region src/features/block-ui/components/BlockUi.vue
11
12
  /**
12
13
  * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the
13
- * hover actions (`BlockActionsOverlay`) at the editor's right edge, both targeting the same block. The actions are those
14
- * the surface registered for `editor` (features' block actions, then the host's). A block with a kind icon (a composite)
15
- * gets no handle: its icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent
16
- * handles on every eligible block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
14
+ * block actions (`BlockActionsOverlay`) at the editor's right edge. The actions are those the surface registered for
15
+ * `editor` (features' block actions, then the host's), shown per `BlockUiOptions.actions`: on a hover bar for the block
16
+ * the drag handle targets, on a persistent toolbar for the block holding the selection, both, or neither. When both
17
+ * would show a bar on the same block, only the toolbar does. A block with a kind icon (a composite) gets no handle: its
18
+ * icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent handles on every eligible
19
+ * block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
17
20
  */
18
21
  const _sfc_main = /*@__PURE__*/ defineComponent({
19
22
  __name: "BlockUi",
@@ -24,6 +27,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
24
27
  const host = shallowRef(null);
25
28
  const hoverTarget = shallowRef(null);
26
29
  const actions = computed(() => editorEntryOf(toRaw(props.editor))?.blockActions ?? []);
30
+ const surfaces = computed(() => actionSurfacesOf(toRaw(props.editor)));
31
+ const selectedTarget = computed(() => {
32
+ props.editor.state;
33
+ return surfaces.value.toolbar ? selectedBlockOf(props.editor) : null;
34
+ });
35
+ const hoverBarTarget = computed(() => {
36
+ const hovered = hoverTarget.value;
37
+ const selected = selectedTarget.value;
38
+ return hovered && selected && sameBlock(props.editor, hovered, selected) ? null : hovered;
39
+ });
27
40
  let overlayHovered = false;
28
41
  let hoverClearTimer;
29
42
  onMounted(async () => {
@@ -141,22 +154,41 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
141
154
  return openBlock(), createElementBlock(Fragment, null, [host.value ? (openBlock(), createBlock(Teleport, {
142
155
  key: 0,
143
156
  to: host.value
144
- }, [createVNode(_sfc_main$1, {
145
- editor: __props.editor,
146
- target: hoverTarget.value,
147
- host: host.value,
148
- actions: actions.value,
149
- onHover: onOverlayHover
150
- }, null, 8, [
151
- "editor",
152
- "target",
153
- "host",
154
- "actions"
155
- ]), alwaysHandles.value ? (openBlock(), createBlock(_sfc_main$2, {
156
- key: 0,
157
- editor: __props.editor,
158
- host: host.value
159
- }, null, 8, ["editor", "host"])) : createCommentVNode("v-if", true)], 8, ["to"])) : createCommentVNode("v-if", true), createVNode(unref(DragHandle), {
157
+ }, [
158
+ surfaces.value.hover ? (openBlock(), createBlock(_sfc_main$1, {
159
+ key: 0,
160
+ editor: __props.editor,
161
+ target: hoverBarTarget.value,
162
+ host: host.value,
163
+ actions: actions.value,
164
+ surface: "hover",
165
+ onHover: onOverlayHover
166
+ }, null, 8, [
167
+ "editor",
168
+ "target",
169
+ "host",
170
+ "actions"
171
+ ])) : createCommentVNode("v-if", true),
172
+ surfaces.value.toolbar ? (openBlock(), createBlock(_sfc_main$1, {
173
+ key: 1,
174
+ editor: __props.editor,
175
+ target: selectedTarget.value,
176
+ host: host.value,
177
+ actions: actions.value,
178
+ surface: "toolbar",
179
+ persistent: ""
180
+ }, null, 8, [
181
+ "editor",
182
+ "target",
183
+ "host",
184
+ "actions"
185
+ ])) : createCommentVNode("v-if", true),
186
+ alwaysHandles.value ? (openBlock(), createBlock(_sfc_main$2, {
187
+ key: 2,
188
+ editor: __props.editor,
189
+ host: host.value
190
+ }, null, 8, ["editor", "host"])) : createCommentVNode("v-if", true)
191
+ ], 8, ["to"])) : createCommentVNode("v-if", true), createVNode(unref(DragHandle), {
160
192
  ref_key: "handle",
161
193
  ref: handle,
162
194
  editor: __props.editor,
@@ -1 +1 @@
1
- {"version":3,"file":"BlockUi.js","names":["_openBlock","_createElementBlock","_Fragment","_createBlock","_Teleport","_createVNode","BlockActionsOverlay","BlockHandles","_createCommentVNode","_unref","_normalizeClass","_withCtx"],"sources":["../../../../src/features/block-ui/components/BlockUi.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport { DragHandle, type DragHandleProps } from '@tiptap/extension-drag-handle-vue-3'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Editor } from '@tiptap/vue-3'\nimport { type ComponentPublicInstance, computed, nextTick, onBeforeUnmount, onMounted, shallowRef, toRaw, useTemplateRef } from 'vue'\nimport { type BlockTarget, editorEntryOf } from '../../../core'\nimport { useEditorSettings } from '../../../vue/settings'\nimport { isChromeNode } from '../../../core/schema'\nimport { firstLineCenterShift, gutterShiftOf, handleLeftOf, hasKindIcon } from '../handle-geometry'\nimport BlockActionsOverlay from './BlockActionsOverlay.vue'\nimport BlockHandles from './BlockHandles.vue'\n\n/**\n * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the\n * hover actions (`BlockActionsOverlay`) at the editor's right edge, both targeting the same block. The actions are those\n * the surface registered for `editor` (features' block actions, then the host's). A block with a kind icon (a composite)\n * gets no handle: its icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent\n * handles on every eligible block (`BlockHandles`). Mounted by the block-ui feature beside the editor.\n */\nconst props = defineProps<{ editor: Editor }>()\n\nconst { Icon: UIcon } = useUiKit()\n\n// Hover actions target the drag handle's block. The plugin drops its target when the pointer leaves the editor (also\n// onto the overlay, which sits outside it) and on keydown; a short grace keeps the overlay while the pointer moves\n// onto it, and typing hides it at once.\nconst host = shallowRef<HTMLElement | null>(null)\nconst hoverTarget = shallowRef<BlockTarget | null>(null)\nconst actions = computed(() => editorEntryOf(toRaw(props.editor))?.blockActions ?? [])\nlet overlayHovered = false\nlet hoverClearTimer: ReturnType<typeof setTimeout> | undefined\n\n// The overlay is positioned inside the element that holds the editor content (`relative`). Tiptap's content component\n// moves the editor DOM into it on the tick after it mounted, so look it up after that one.\nonMounted(async () => {\n await nextTick()\n const dom = props.editor.isDestroyed ? null : props.editor.view.dom\n host.value = dom?.isConnected ? dom.parentElement : null\n dom?.addEventListener('keydown', onEditorKeydown)\n dom?.addEventListener('mousemove', onEditorMousemove)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverClearTimer)\n if (props.editor.isDestroyed) return\n props.editor.view.dom.removeEventListener('keydown', onEditorKeydown)\n props.editor.view.dom.removeEventListener('mousemove', onEditorMousemove)\n})\n\n// Pointer position for the targeting rules below. The plugin evaluates them in a frame callback after the mousemove,\n// so this is the position they see.\nlet pointer: { x: number, y: number } | null = null\nfunction onEditorMousemove(e: MouseEvent) {\n pointer = { x: e.clientX, y: e.clientY }\n}\n\n// Like the drag-handle plugin: a key pressed in the focused editor hides the actions.\nfunction onEditorKeydown() {\n clearTimeout(hoverClearTimer)\n hoverTarget.value = null\n}\n\nfunction scheduleHoverClear() {\n clearTimeout(hoverClearTimer)\n hoverClearTimer = setTimeout(() => {\n if (!overlayHovered) hoverTarget.value = null\n }, 200)\n}\n\nfunction onOverlayHover(hovered: boolean) {\n overlayHovered = hovered\n if (!hovered) scheduleHoverClear()\n}\n\n// The drag-handle plugin hides the handle on dragend but keeps its current target. A dropped\n// nested node is the same node object, so hovering it again would not re-show the handle.\n// The plugin's `hideDragHandle` meta clears the target.\nfunction resetDragHandle() {\n const { editor } = props\n if (editor.isDestroyed) return\n editor.view.dispatch(editor.state.tr.setMeta('hideDragHandle', true).setMeta('addToHistory', false))\n}\n\n// The plugin puts the handle's right edge on the target block's left edge (`gutterShiftOf`: list items move further\n// left). The hover handle is hidden where the block has a handle of its own: the kind icon of a composite, or, with\n// `blocks.dragHandle: 'always'`, the permanent handles. Hiding keeps its layout, which the targeting rules below measure.\nconst { settings } = useEditorSettings()\nconst alwaysHandles = computed(() => settings.value.blocks.dragHandle === 'always')\nconst handle = useTemplateRef<ComponentPublicInstance>('handle')\nconst targetHasKindIcon = shallowRef(false)\nconst hoverHandleHidden = computed(() => alwaysHandles.value || targetHasKindIcon.value)\nlet handleGutterShift = 0\nlet dragTargetDom: HTMLElement | null = null\nlet dragTargetNode: Node | null = null\nfunction trackDragTarget({ node, pos }: { node: Node | null, pos: number }) {\n if (node) {\n clearTimeout(hoverClearTimer)\n hoverTarget.value = { node, pos }\n }\n else {\n scheduleHoverClear()\n }\n const dom = node ? props.editor.view.nodeDOM(pos) : null\n dragTargetDom = dom instanceof HTMLElement ? dom : null\n dragTargetNode = dragTargetDom ? node : null\n handleGutterShift = dragTargetDom && dragTargetNode ? gutterShiftOf(dragTargetNode, dragTargetDom) : 0\n targetHasKindIcon.value = dragTargetDom ? hasKindIcon(dragTargetDom) : false\n}\n\n// Left of a block, ProseMirror resolves the pointer to the position before the block, so the plugin only considers\n// the block's ancestors there: the target's own handle column would hand the handle to its parent before the pointer\n// reaches it. This keeps the target while the pointer is between its handle and its box, over its whole height\n// including the gaps to its neighbours, and in those gaps themselves (which resolve to the parent as well). The rule\n// then excludes every candidate, and the plugin keeps its target when the detection yields none. Inside the target's\n// own box the rule is off, so its descendants are still targeted.\nfunction pointerReachesTarget(): boolean {\n if (!pointer || !dragTargetNode || !dragTargetDom?.isConnected) return false\n const rect = dragTargetDom.getBoundingClientRect()\n const { x, y } = pointer\n if (x < handleLeftOf(dragTargetNode, dragTargetDom, handle.value?.$el.offsetWidth ?? 0) || x >= rect.right) return false\n if (y < (laidOutSibling(dragTargetDom, 'previous')?.bottom ?? rect.top)) return false\n if (y >= (laidOutSibling(dragTargetDom, 'next')?.top ?? rect.bottom)) return false\n return x < rect.left || y < rect.top || y >= rect.bottom\n}\n\n// Nearest sibling with a layout box (a composite's hidden description/tags rows have none).\nfunction laidOutSibling(dom: Element, side: 'previous' | 'next'): DOMRect | null {\n for (let el = dom[`${side}ElementSibling`]; el; el = el[`${side}ElementSibling`]) {\n const rect = el.getBoundingClientRect()\n if (rect.height > 0) return rect\n }\n return null\n}\n\n// The plugin aligns the handle's top with the block's top, which sits above the middle of the\n// block's first text line: `firstLineCenterShift` moves the handle's centre to that line's centre.\nconst dragHandlePosition: DragHandleProps['computePositionConfig'] = {\n middleware: [\n { name: 'gutterShift', fn: ({ x, y }) => ({ x: x - handleGutterShift, y }) },\n {\n name: 'firstLineCenter',\n fn: ({ x, y, elements, rects }) => ({\n x,\n y: dragTargetDom ? y + firstLineCenterShift(dragTargetDom, elements.reference.getBoundingClientRect().top, rects.floating.height) : y,\n }),\n },\n ],\n}\n\n// Targeting among nested blocks. `compositeChrome`: hovering a composite's title, description or tags row targets the\n// composite itself (a deduction of 1000, the base score, excludes a node). `reachTarget`: see `pointerReachesTarget`.\n// `ownGutter`: a nested block is targeted only from its handle column rightwards; further left its ancestors are, so\n// on any row the gutter is partitioned by the handle columns of the blocks nested there. The plugin's edge detection\n// is off: its left strip is this partition, its top strip would hand the handle to the parent on the block's first\n// line, where the handle sits.\nconst dragHandleNested: DragHandleProps['nested'] = {\n edgeDetection: 'none',\n rules: [\n { id: 'compositeChrome', evaluate: ({ node }) => isChromeNode(node) ? 1000 : 0 },\n { id: 'reachTarget', evaluate: () => pointerReachesTarget() ? 1000 : 0 },\n {\n id: 'ownGutter',\n evaluate: ({ node, pos, depth, view }) => {\n if (depth < 2 || !pointer) return 0\n const dom = view.nodeDOM(pos)\n return dom instanceof HTMLElement && pointer.x < handleLeftOf(node, dom, handle.value?.$el.offsetWidth ?? 0) ? 1000 : 0\n },\n },\n ],\n}\n</script>\n\n<template>\n <Teleport v-if=\"host\" :to=\"host\">\n <BlockActionsOverlay\n :editor=\"editor\"\n :target=\"hoverTarget\"\n :host=\"host\"\n :actions=\"actions\"\n @hover=\"onOverlayHover\"\n />\n <BlockHandles v-if=\"alwaysHandles\" :editor=\"editor\" :host=\"host\" />\n </Teleport>\n <DragHandle\n ref=\"handle\"\n :editor=\"editor\"\n :on-element-drag-end=\"resetDragHandle\"\n :on-node-change=\"trackDragTarget\"\n :compute-position-config=\"dragHandlePosition\"\n :nested=\"dragHandleNested\"\n :class=\"{ 'invisible! pointer-events-none!': hoverHandleHidden }\"\n class=\"flex size-6 cursor-grab items-center justify-center rounded text-[var(--sk-muted)] hover:bg-[var(--sk-bg-elevated)] hover:text-[var(--sk-fg-highlighted)] data-[dragging=true]:cursor-grabbing\"\n >\n <UIcon name=\"i-lucide-grip-vertical\" class=\"size-4\" />\n </DragHandle>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"BlockUi.js","names":["_openBlock","_createElementBlock","_Fragment","_createBlock","_Teleport","BlockActionsOverlay","_createCommentVNode","BlockHandles","_createVNode","_unref","_normalizeClass","_withCtx"],"sources":["../../../../src/features/block-ui/components/BlockUi.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport { DragHandle, type DragHandleProps } from '@tiptap/extension-drag-handle-vue-3'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Editor } from '@tiptap/vue-3'\nimport { type ComponentPublicInstance, computed, nextTick, onBeforeUnmount, onMounted, shallowRef, toRaw, useTemplateRef } from 'vue'\nimport { type BlockTarget, editorEntryOf } from '../../../core'\nimport { useEditorSettings } from '../../../vue/settings'\nimport { isChromeNode } from '../../../core/schema'\nimport { firstLineCenterShift, gutterShiftOf, handleLeftOf, hasKindIcon } from '../handle-geometry'\nimport { actionSurfacesOf, sameBlock, selectedBlockOf } from '../block-actions'\nimport BlockActionsOverlay from './BlockActionsOverlay.vue'\nimport BlockHandles from './BlockHandles.vue'\n\n/**\n * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the\n * block actions (`BlockActionsOverlay`) at the editor's right edge. The actions are those the surface registered for\n * `editor` (features' block actions, then the host's), shown per `BlockUiOptions.actions`: on a hover bar for the block\n * the drag handle targets, on a persistent toolbar for the block holding the selection, both, or neither. When both\n * would show a bar on the same block, only the toolbar does. A block with a kind icon (a composite) gets no handle: its\n * icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent handles on every eligible\n * block (`BlockHandles`). Mounted by the block-ui feature beside the editor.\n */\nconst props = defineProps<{ editor: Editor }>()\n\nconst { Icon: UIcon } = useUiKit()\n\n// Hover actions target the drag handle's block. The plugin drops its target when the pointer leaves the editor (also\n// onto the overlay, which sits outside it) and on keydown; a short grace keeps the overlay while the pointer moves\n// onto it, and typing hides it at once.\nconst host = shallowRef<HTMLElement | null>(null)\nconst hoverTarget = shallowRef<BlockTarget | null>(null)\nconst actions = computed(() => editorEntryOf(toRaw(props.editor))?.blockActions ?? [])\n// Read inside the teleport only: the surface registers its entry after its overlays have mounted.\nconst surfaces = computed(() => actionSurfacesOf(toRaw(props.editor)))\n// The block holding the selection follows every transaction (`editor.state` is reactive).\nconst selectedTarget = computed(() => {\n void props.editor.state\n return surfaces.value.toolbar ? selectedBlockOf(props.editor) : null\n})\n// One bar per block: the toolbar wins where the hover bar would sit on the same block.\nconst hoverBarTarget = computed(() => {\n const hovered = hoverTarget.value\n const selected = selectedTarget.value\n return hovered && selected && sameBlock(props.editor, hovered, selected) ? null : hovered\n})\nlet overlayHovered = false\nlet hoverClearTimer: ReturnType<typeof setTimeout> | undefined\n\n// The overlay is positioned inside the element that holds the editor content (`relative`). Tiptap's content component\n// moves the editor DOM into it on the tick after it mounted, so look it up after that one.\nonMounted(async () => {\n await nextTick()\n const dom = props.editor.isDestroyed ? null : props.editor.view.dom\n host.value = dom?.isConnected ? dom.parentElement : null\n dom?.addEventListener('keydown', onEditorKeydown)\n dom?.addEventListener('mousemove', onEditorMousemove)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(hoverClearTimer)\n if (props.editor.isDestroyed) return\n props.editor.view.dom.removeEventListener('keydown', onEditorKeydown)\n props.editor.view.dom.removeEventListener('mousemove', onEditorMousemove)\n})\n\n// Pointer position for the targeting rules below. The plugin evaluates them in a frame callback after the mousemove,\n// so this is the position they see.\nlet pointer: { x: number, y: number } | null = null\nfunction onEditorMousemove(e: MouseEvent) {\n pointer = { x: e.clientX, y: e.clientY }\n}\n\n// Like the drag-handle plugin: a key pressed in the focused editor hides the actions.\nfunction onEditorKeydown() {\n clearTimeout(hoverClearTimer)\n hoverTarget.value = null\n}\n\nfunction scheduleHoverClear() {\n clearTimeout(hoverClearTimer)\n hoverClearTimer = setTimeout(() => {\n if (!overlayHovered) hoverTarget.value = null\n }, 200)\n}\n\nfunction onOverlayHover(hovered: boolean) {\n overlayHovered = hovered\n if (!hovered) scheduleHoverClear()\n}\n\n// The drag-handle plugin hides the handle on dragend but keeps its current target. A dropped\n// nested node is the same node object, so hovering it again would not re-show the handle.\n// The plugin's `hideDragHandle` meta clears the target.\nfunction resetDragHandle() {\n const { editor } = props\n if (editor.isDestroyed) return\n editor.view.dispatch(editor.state.tr.setMeta('hideDragHandle', true).setMeta('addToHistory', false))\n}\n\n// The plugin puts the handle's right edge on the target block's left edge (`gutterShiftOf`: list items move further\n// left). The hover handle is hidden where the block has a handle of its own: the kind icon of a composite, or, with\n// `blocks.dragHandle: 'always'`, the permanent handles. Hiding keeps its layout, which the targeting rules below measure.\nconst { settings } = useEditorSettings()\nconst alwaysHandles = computed(() => settings.value.blocks.dragHandle === 'always')\nconst handle = useTemplateRef<ComponentPublicInstance>('handle')\nconst targetHasKindIcon = shallowRef(false)\nconst hoverHandleHidden = computed(() => alwaysHandles.value || targetHasKindIcon.value)\nlet handleGutterShift = 0\nlet dragTargetDom: HTMLElement | null = null\nlet dragTargetNode: Node | null = null\nfunction trackDragTarget({ node, pos }: { node: Node | null, pos: number }) {\n if (node) {\n clearTimeout(hoverClearTimer)\n hoverTarget.value = { node, pos }\n }\n else {\n scheduleHoverClear()\n }\n const dom = node ? props.editor.view.nodeDOM(pos) : null\n dragTargetDom = dom instanceof HTMLElement ? dom : null\n dragTargetNode = dragTargetDom ? node : null\n handleGutterShift = dragTargetDom && dragTargetNode ? gutterShiftOf(dragTargetNode, dragTargetDom) : 0\n targetHasKindIcon.value = dragTargetDom ? hasKindIcon(dragTargetDom) : false\n}\n\n// Left of a block, ProseMirror resolves the pointer to the position before the block, so the plugin only considers\n// the block's ancestors there: the target's own handle column would hand the handle to its parent before the pointer\n// reaches it. This keeps the target while the pointer is between its handle and its box, over its whole height\n// including the gaps to its neighbours, and in those gaps themselves (which resolve to the parent as well). The rule\n// then excludes every candidate, and the plugin keeps its target when the detection yields none. Inside the target's\n// own box the rule is off, so its descendants are still targeted.\nfunction pointerReachesTarget(): boolean {\n if (!pointer || !dragTargetNode || !dragTargetDom?.isConnected) return false\n const rect = dragTargetDom.getBoundingClientRect()\n const { x, y } = pointer\n if (x < handleLeftOf(dragTargetNode, dragTargetDom, handle.value?.$el.offsetWidth ?? 0) || x >= rect.right) return false\n if (y < (laidOutSibling(dragTargetDom, 'previous')?.bottom ?? rect.top)) return false\n if (y >= (laidOutSibling(dragTargetDom, 'next')?.top ?? rect.bottom)) return false\n return x < rect.left || y < rect.top || y >= rect.bottom\n}\n\n// Nearest sibling with a layout box (a composite's hidden description/tags rows have none).\nfunction laidOutSibling(dom: Element, side: 'previous' | 'next'): DOMRect | null {\n for (let el = dom[`${side}ElementSibling`]; el; el = el[`${side}ElementSibling`]) {\n const rect = el.getBoundingClientRect()\n if (rect.height > 0) return rect\n }\n return null\n}\n\n// The plugin aligns the handle's top with the block's top, which sits above the middle of the\n// block's first text line: `firstLineCenterShift` moves the handle's centre to that line's centre.\nconst dragHandlePosition: DragHandleProps['computePositionConfig'] = {\n middleware: [\n { name: 'gutterShift', fn: ({ x, y }) => ({ x: x - handleGutterShift, y }) },\n {\n name: 'firstLineCenter',\n fn: ({ x, y, elements, rects }) => ({\n x,\n y: dragTargetDom ? y + firstLineCenterShift(dragTargetDom, elements.reference.getBoundingClientRect().top, rects.floating.height) : y,\n }),\n },\n ],\n}\n\n// Targeting among nested blocks. `compositeChrome`: hovering a composite's title, description or tags row targets the\n// composite itself (a deduction of 1000, the base score, excludes a node). `reachTarget`: see `pointerReachesTarget`.\n// `ownGutter`: a nested block is targeted only from its handle column rightwards; further left its ancestors are, so\n// on any row the gutter is partitioned by the handle columns of the blocks nested there. The plugin's edge detection\n// is off: its left strip is this partition, its top strip would hand the handle to the parent on the block's first\n// line, where the handle sits.\nconst dragHandleNested: DragHandleProps['nested'] = {\n edgeDetection: 'none',\n rules: [\n { id: 'compositeChrome', evaluate: ({ node }) => isChromeNode(node) ? 1000 : 0 },\n { id: 'reachTarget', evaluate: () => pointerReachesTarget() ? 1000 : 0 },\n {\n id: 'ownGutter',\n evaluate: ({ node, pos, depth, view }) => {\n if (depth < 2 || !pointer) return 0\n const dom = view.nodeDOM(pos)\n return dom instanceof HTMLElement && pointer.x < handleLeftOf(node, dom, handle.value?.$el.offsetWidth ?? 0) ? 1000 : 0\n },\n },\n ],\n}\n</script>\n\n<template>\n <Teleport v-if=\"host\" :to=\"host\">\n <BlockActionsOverlay\n v-if=\"surfaces.hover\"\n :editor=\"editor\"\n :target=\"hoverBarTarget\"\n :host=\"host\"\n :actions=\"actions\"\n surface=\"hover\"\n @hover=\"onOverlayHover\"\n />\n <BlockActionsOverlay\n v-if=\"surfaces.toolbar\"\n :editor=\"editor\"\n :target=\"selectedTarget\"\n :host=\"host\"\n :actions=\"actions\"\n surface=\"toolbar\"\n persistent\n />\n <BlockHandles v-if=\"alwaysHandles\" :editor=\"editor\" :host=\"host\" />\n </Teleport>\n <DragHandle\n ref=\"handle\"\n :editor=\"editor\"\n :on-element-drag-end=\"resetDragHandle\"\n :on-node-change=\"trackDragTarget\"\n :compute-position-config=\"dragHandlePosition\"\n :nested=\"dragHandleNested\"\n :class=\"{ 'invisible! pointer-events-none!': hoverHandleHidden }\"\n class=\"flex size-6 cursor-grab items-center justify-center rounded text-[var(--sk-muted)] hover:bg-[var(--sk-bg-elevated)] hover:text-[var(--sk-fg-highlighted)] data-[dragging=true]:cursor-grabbing\"\n >\n <UIcon name=\"i-lucide-grip-vertical\" class=\"size-4\" />\n </DragHandle>\n</template>\n"],"mappings":"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"}
@@ -2,10 +2,12 @@ import { Editor } from "@tiptap/vue-3";
2
2
  //#region src/features/block-ui/components/BlockUi.vue.d.ts
3
3
  /**
4
4
  * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the
5
- * hover actions (`BlockActionsOverlay`) at the editor's right edge, both targeting the same block. The actions are those
6
- * the surface registered for `editor` (features' block actions, then the host's). A block with a kind icon (a composite)
7
- * gets no handle: its icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent
8
- * handles on every eligible block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
5
+ * block actions (`BlockActionsOverlay`) at the editor's right edge. The actions are those the surface registered for
6
+ * `editor` (features' block actions, then the host's), shown per `BlockUiOptions.actions`: on a hover bar for the block
7
+ * the drag handle targets, on a persistent toolbar for the block holding the selection, both, or neither. When both
8
+ * would show a bar on the same block, only the toolbar does. A block with a kind icon (a composite) gets no handle: its
9
+ * icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent handles on every eligible
10
+ * block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
9
11
  */
10
12
  type __VLS_Props = {
11
13
  editor: Editor;
@@ -1,3 +1,4 @@
1
+ import { BlockActionSurface, BlockUiOptions } from "../../core/extension-points.js";
1
2
  import { EditorFeature } from "../../core/feature.js";
2
3
  import "../../core/index.js";
3
4
  //#region src/features/block-ui/feature.d.ts
@@ -8,14 +9,19 @@ export interface BlockUiUi<C> {
8
9
  }
9
10
  /**
10
11
  * Block-level UI beside the editor content: the drag handle (upstream `@tiptap/extension-drag-handle`) with hover
11
- * targeting, and the hover actions of the targeted block (features' and host block actions, `BlockActionsOverlay`).
12
+ * targeting, and the actions of the targeted block (features' and host block actions, `BlockActionsOverlay`), on the
13
+ * hover bar, the toolbar of the block holding the selection, or both (`BlockUiOptions.actions`; hover by default).
12
14
  * Also adds block-range selection (`NodeRange`: Mod+drag, Shift+↑/↓). Composite chrome (`editorChrome` nodes) targets its
13
15
  * parent block.
14
16
  *
15
17
  * Opt-in: the simple editor has none of it. Installing a binding also bundles the drag handle's transitive collaboration
16
18
  * support code (its upstream package imports `@tiptap/extension-collaboration` and `y-tiptap`), even without a live
17
19
  * collaboration session.
20
+ *
21
+ * The overlay is a component type the surface mounts without props, so `options` reach it through `EditorFeature.blockUi`
22
+ * and the editor entry (`editorEntryOf(editor)?.blockUi`).
18
23
  */
19
- export declare function blockUiFeatureCore<C>(ui: BlockUiUi<C>): EditorFeature<C>;
24
+ export declare function blockUiFeatureCore<C>(ui: BlockUiUi<C>, options?: BlockUiOptions): EditorFeature<C>;
20
25
  //#endregion
26
+ export type { BlockActionSurface, BlockUiOptions };
21
27
  //# sourceMappingURL=feature.d.ts.map
@@ -2,19 +2,24 @@ import { NodeRange } from "@tiptap/extension-node-range";
2
2
  //#region src/features/block-ui/feature.ts
3
3
  /**
4
4
  * Block-level UI beside the editor content: the drag handle (upstream `@tiptap/extension-drag-handle`) with hover
5
- * targeting, and the hover actions of the targeted block (features' and host block actions, `BlockActionsOverlay`).
5
+ * targeting, and the actions of the targeted block (features' and host block actions, `BlockActionsOverlay`), on the
6
+ * hover bar, the toolbar of the block holding the selection, or both (`BlockUiOptions.actions`; hover by default).
6
7
  * Also adds block-range selection (`NodeRange`: Mod+drag, Shift+↑/↓). Composite chrome (`editorChrome` nodes) targets its
7
8
  * parent block.
8
9
  *
9
10
  * Opt-in: the simple editor has none of it. Installing a binding also bundles the drag handle's transitive collaboration
10
11
  * support code (its upstream package imports `@tiptap/extension-collaboration` and `y-tiptap`), even without a live
11
12
  * collaboration session.
13
+ *
14
+ * The overlay is a component type the surface mounts without props, so `options` reach it through `EditorFeature.blockUi`
15
+ * and the editor entry (`editorEntryOf(editor)?.blockUi`).
12
16
  */
13
- function blockUiFeatureCore(ui) {
17
+ function blockUiFeatureCore(ui, options = {}) {
14
18
  return {
15
19
  id: "block-ui",
16
20
  dimension: "interaction",
17
21
  extensions: () => [NodeRange],
22
+ blockUi: options,
18
23
  overlays: [ui.overlay]
19
24
  };
20
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"feature.js","names":[],"sources":["../../../src/features/block-ui/feature.ts"],"sourcesContent":["import { NodeRange } from '@tiptap/extension-node-range'\nimport type { EditorFeature } from '../../core'\n\n/** What a framework binding supplies to the block-ui feature. */\nexport interface BlockUiUi<C> {\n /** The block UI of an editor (drag handle, permanent handles, hover actions); mounted beside the editor content. */\n overlay: C\n}\n\n/**\n * Block-level UI beside the editor content: the drag handle (upstream `@tiptap/extension-drag-handle`) with hover\n * targeting, and the hover actions of the targeted block (features' and host block actions, `BlockActionsOverlay`).\n * Also adds block-range selection (`NodeRange`: Mod+drag, Shift+↑/↓). Composite chrome (`editorChrome` nodes) targets its\n * parent block.\n *\n * Opt-in: the simple editor has none of it. Installing a binding also bundles the drag handle's transitive collaboration\n * support code (its upstream package imports `@tiptap/extension-collaboration` and `y-tiptap`), even without a live\n * collaboration session.\n */\nexport function blockUiFeatureCore<C>(ui: BlockUiUi<C>): EditorFeature<C> {\n return {\n id: 'block-ui',\n dimension: 'interaction',\n extensions: () => [NodeRange],\n overlays: [ui.overlay],\n }\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,mBAAsB,IAAoC;CACxE,OAAO;EACL,IAAI;EACJ,WAAW;EACX,kBAAkB,CAAC,SAAS;EAC5B,UAAU,CAAC,GAAG,OAAO;CACvB;AACF"}
1
+ {"version":3,"file":"feature.js","names":[],"sources":["../../../src/features/block-ui/feature.ts"],"sourcesContent":["import { NodeRange } from '@tiptap/extension-node-range'\nimport type { BlockUiOptions, EditorFeature } from '../../core'\n\n// The option types live in `core` (the editor entry carries them to the overlay); this is their public home.\nexport type { BlockActionSurface, BlockUiOptions } from '../../core'\n\n/** What a framework binding supplies to the block-ui feature. */\nexport interface BlockUiUi<C> {\n /** The block UI of an editor (drag handle, permanent handles, hover actions); mounted beside the editor content. */\n overlay: C\n}\n\n/**\n * Block-level UI beside the editor content: the drag handle (upstream `@tiptap/extension-drag-handle`) with hover\n * targeting, and the actions of the targeted block (features' and host block actions, `BlockActionsOverlay`), on the\n * hover bar, the toolbar of the block holding the selection, or both (`BlockUiOptions.actions`; hover by default).\n * Also adds block-range selection (`NodeRange`: Mod+drag, Shift+↑/↓). Composite chrome (`editorChrome` nodes) targets its\n * parent block.\n *\n * Opt-in: the simple editor has none of it. Installing a binding also bundles the drag handle's transitive collaboration\n * support code (its upstream package imports `@tiptap/extension-collaboration` and `y-tiptap`), even without a live\n * collaboration session.\n *\n * The overlay is a component type the surface mounts without props, so `options` reach it through `EditorFeature.blockUi`\n * and the editor entry (`editorEntryOf(editor)?.blockUi`).\n */\nexport function blockUiFeatureCore<C>(ui: BlockUiUi<C>, options: BlockUiOptions = {}): EditorFeature<C> {\n return {\n id: 'block-ui',\n dimension: 'interaction',\n extensions: () => [NodeRange],\n blockUi: options,\n overlays: [ui.overlay],\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA0BA,SAAgB,mBAAsB,IAAkB,UAA0B,CAAC,GAAqB;CACtG,OAAO;EACL,IAAI;EACJ,WAAW;EACX,kBAAkB,CAAC,SAAS;EAC5B,SAAS;EACT,UAAU,CAAC,GAAG,OAAO;CACvB;AACF"}
@@ -1,3 +1,4 @@
1
- import { BLOCK_CORNER_ATTR, BlockActionEntry, BlockCorner, blockActionsFor, blockCornerOf, effectiveTarget } from "./block-actions.js";
1
+ import { BlockActionSurface, BlockUiOptions } from "../../core/extension-points.js";
2
+ import { BLOCK_CORNER_ATTR, BlockActionEntry, BlockCorner, actionSurfacesOf, blockActionsFor, blockCornerOf, effectiveTarget, sameBlock, selectedBlockOf } from "./block-actions.js";
2
3
  import { BlockUiUi, blockUiFeatureCore } from "./feature.js";
3
- export { BLOCK_CORNER_ATTR, type BlockActionEntry, type BlockCorner, type BlockUiUi, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget };
4
+ export { BLOCK_CORNER_ATTR, type BlockActionEntry, type BlockActionSurface, type BlockCorner, type BlockUiOptions, type BlockUiUi, actionSurfacesOf, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget, sameBlock, selectedBlockOf };
@@ -1,3 +1,3 @@
1
1
  import { blockUiFeatureCore } from "./feature.js";
2
- import { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, effectiveTarget } from "./block-actions.js";
3
- export { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget };
2
+ import { BLOCK_CORNER_ATTR, actionSurfacesOf, blockActionsFor, blockCornerOf, effectiveTarget, sameBlock, selectedBlockOf } from "./block-actions.js";
3
+ export { BLOCK_CORNER_ATTR, actionSurfacesOf, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget, sameBlock, selectedBlockOf };
@@ -1,23 +1,30 @@
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/core";
4
4
  //#region src/features/block-ui/react/BlockActionsOverlay.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, `onHover` keeps it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target` is
13
+ * the block holding the selection, the bar shows as long as it is set, reports no hover 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
13
18
  * it sits at the editor's right edge, aligned to the block's top.
14
19
  */
15
- export declare function BlockActionsOverlay({ editor, target, actions, host, onHover }: {
20
+ export declare function BlockActionsOverlay({ editor, target, actions, host, surface, persistent, onHover }: {
16
21
  editor: Editor;
17
22
  target: BlockTarget | null;
18
23
  actions: readonly BlockAction[];
19
24
  host: HTMLElement | null;
20
- onHover: (hovered: boolean) => void;
25
+ surface: BlockActionSurface;
26
+ persistent?: boolean;
27
+ onHover?: (hovered: boolean) => void;
21
28
  }): import("react").JSX.Element | null;
22
29
  //#endregion
23
30
  //# sourceMappingURL=BlockActionsOverlay.d.ts.map
@@ -1,8 +1,9 @@
1
+ import { cn } from "../../../react/ui/utils.js";
1
2
  import { Icon } from "../../../react/Icon.js";
2
3
  import { Button } from "../../../react/ui/button.js";
3
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../react/ui/tooltip.js";
4
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../../react/ui/dropdown-menu.js";
5
5
  import { blockActionsFor, blockCornerOf, effectiveTarget } from "../block-actions.js";
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../../react/ui/dropdown-menu.js";
6
7
  import { useEffect, useLayoutEffect, useReducer, useRef, useState } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
  //#region src/features/block-ui/react/BlockActionsOverlay.tsx
@@ -11,16 +12,21 @@ const EDGE_GUTTER = 8;
11
12
  /** Gap between the bar and the block's own corner elements it docks next to. */
12
13
  const CORNER_GAP = 4;
13
14
  /**
14
- * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and
15
- * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
16
- * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
17
- * read-only. `menu` actions open a dropdown of their items instead of running on click.
15
+ * The actions of a block on one surface, right-aligned at the editor's text column (the drag handle sits on the block's
16
+ * left and targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the
17
+ * composite chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the
18
+ * editor is read-only. `menu` actions open a dropdown of their items instead of running on click.
19
+ *
20
+ * `surface` selects the actions by their `placement`. The `hover` surface follows the pointer (`target` is the hovered
21
+ * block, `onHover` keeps it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target` is
22
+ * the block holding the selection, the bar shows as long as it is set, reports no hover and carries the class
23
+ * `block-actions-toolbar` for host styling.
18
24
  *
19
25
  * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
20
26
  * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
21
27
  * it sits at the editor's right edge, aligned to the block's top.
22
28
  */
23
- function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
29
+ function BlockActionsOverlay({ editor, target, actions, host, surface, persistent, onHover }) {
24
30
  const [, rerender] = useReducer((count) => count + 1, 0);
25
31
  useEffect(() => {
26
32
  editor.on("transaction", rerender);
@@ -39,7 +45,7 @@ function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
39
45
  pos: target.pos
40
46
  });
41
47
  })();
42
- const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : [];
48
+ const items = block && editor.isEditable ? blockActionsFor(editor, block, actions, surface) : [];
43
49
  const [place, setPlace] = useState(null);
44
50
  const resizeObserver = useRef(null);
45
51
  const watched = useRef([]);
@@ -94,28 +100,34 @@ function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
94
100
  const menuOpen = useRef(false);
95
101
  const onMenuOpenChange = (open) => {
96
102
  menuOpen.current = open;
97
- onHover(open || !!root.current?.matches(":hover"));
103
+ if (!persistent) onHover?.(open || !!root.current?.matches(":hover"));
98
104
  };
99
105
  const wasVisible = useRef(false);
100
106
  useEffect(() => {
101
107
  if (wasVisible.current && !visible) {
102
108
  menuOpen.current = false;
103
- onHover(false);
109
+ if (!persistent) onHover?.(false);
104
110
  }
105
111
  wasVisible.current = visible;
106
- }, [visible, onHover]);
112
+ }, [
113
+ visible,
114
+ persistent,
115
+ onHover
116
+ ]);
107
117
  if (!visible || !place) return null;
108
118
  return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx("div", {
109
119
  ref: root,
110
- className: "block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5",
120
+ className: cn("block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5", persistent && "block-actions-toolbar"),
111
121
  style: {
112
122
  top: `${place.top}px`,
113
123
  right: `${place.right}px`,
114
124
  transform: place.docked ? "translateY(-50%)" : void 0
115
125
  },
116
- onMouseEnter: () => onHover(true),
126
+ onMouseEnter: () => {
127
+ if (!persistent) onHover?.(true);
128
+ },
117
129
  onMouseLeave: () => {
118
- if (!menuOpen.current) onHover(false);
130
+ if (!persistent && !menuOpen.current) onHover?.(false);
119
131
  },
120
132
  children: items.map((entry) => {
121
133
  const button = /* @__PURE__ */ jsx(Button, {
@@ -1 +1 @@
1
- {"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\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 */\nexport function BlockActionsOverlay({ editor, target, actions, host, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n onHover: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\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 const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : []\n\n // `docked`: the bar sits next to the block's corner elements, and `top` is then their vertical centre.\n const [place, setPlace] = useState<{ top: number, right: number, docked: boolean } | null>(null)\n const resizeObserver = useRef<ResizeObserver | null>(null)\n const watched = useRef<HTMLElement[]>([])\n // The corner elements can change size (a badge appears) without the document changing: watch the current ones.\n const watchCorner = (elements: HTMLElement[]) => {\n const current = watched.current\n if (elements.length === current.length && elements.every((element, index) => element === current[index])) return\n for (const element of current) resizeObserver.current?.unobserve(element)\n watched.current = elements\n for (const element of elements) resizeObserver.current?.observe(element)\n }\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n const next = corner\n ? { top: corner.centerY - hostRect.top, right: hostRect.right - corner.left + CORNER_GAP, docked: true }\n : {\n top: dom.getBoundingClientRect().top - hostRect.top + 2,\n right: hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER,\n docked: false,\n }\n setPlace(previous => (previous && previous.top === next.top && previous.right === next.right && previous.docked === next.docked ? previous : next))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const observer = new ResizeObserver(run)\n resizeObserver.current = observer\n observer.observe(editor.view.dom)\n for (const element of watched.current) observer.observe(element)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n observer.disconnect()\n resizeObserver.current = null\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n onHover(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n onHover(false)\n }\n wasVisible.current = visible\n }, [visible, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5\"\n style={{ top: `${place.top}px`, right: `${place.right}px`, transform: place.docked ? 'translateY(-50%)' : undefined }}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => { if (!menuOpen.current) onHover(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockActionSurface, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\nimport { cn } from '../../../react/ui/utils'\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, `onHover` keeps it alive while the pointer is on the bar); the `toolbar` surface is `persistent`: `target` is\n * the block holding the selection, the bar shows as long as it is set, reports no hover 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 */\nexport function BlockActionsOverlay({ editor, target, actions, host, surface, persistent, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n surface: BlockActionSurface\n persistent?: boolean\n onHover?: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\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 const items = block && editor.isEditable ? blockActionsFor(editor, block, actions, surface) : []\n\n // `docked`: the bar sits next to the block's corner elements, and `top` is then their vertical centre.\n const [place, setPlace] = useState<{ top: number, right: number, docked: boolean } | null>(null)\n const resizeObserver = useRef<ResizeObserver | null>(null)\n const watched = useRef<HTMLElement[]>([])\n // The corner elements can change size (a badge appears) without the document changing: watch the current ones.\n const watchCorner = (elements: HTMLElement[]) => {\n const current = watched.current\n if (elements.length === current.length && elements.every((element, index) => element === current[index])) return\n for (const element of current) resizeObserver.current?.unobserve(element)\n watched.current = elements\n for (const element of elements) resizeObserver.current?.observe(element)\n }\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n const next = corner\n ? { top: corner.centerY - hostRect.top, right: hostRect.right - corner.left + CORNER_GAP, docked: true }\n : {\n top: dom.getBoundingClientRect().top - hostRect.top + 2,\n right: hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER,\n docked: false,\n }\n setPlace(previous => (previous && previous.top === next.top && previous.right === next.right && previous.docked === next.docked ? previous : next))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const observer = new ResizeObserver(run)\n resizeObserver.current = observer\n observer.observe(editor.view.dom)\n for (const element of watched.current) observer.observe(element)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n observer.disconnect()\n resizeObserver.current = null\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n if (!persistent) onHover?.(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n if (!persistent) onHover?.(false)\n }\n wasVisible.current = visible\n }, [visible, persistent, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className={cn('block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5', persistent && 'block-actions-toolbar')}\n style={{ top: `${place.top}px`, right: `${place.right}px`, transform: place.docked ? 'translateY(-50%)' : undefined }}\n onMouseEnter={() => { if (!persistent) onHover?.(true) }}\n onMouseLeave={() => { if (!persistent && !menuOpen.current) onHover?.(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
@@ -2,10 +2,12 @@ import { Editor } from "@tiptap/core";
2
2
  //#region src/features/block-ui/react/BlockUi.d.ts
3
3
  /**
4
4
  * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the
5
- * hover actions (`BlockActionsOverlay`) at the editor's right edge, both targeting the same block. The actions are those
6
- * the surface registered for `editor` (features' block actions, then the host's). A block with a kind icon (a composite)
7
- * gets no handle: its icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent
8
- * handles on every eligible block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
5
+ * block actions (`BlockActionsOverlay`) at the editor's right edge. The actions are those the surface registered for
6
+ * `editor` (features' block actions, then the host's), shown per `BlockUiOptions.actions`: on a hover bar for the block
7
+ * the drag handle targets, on a persistent toolbar for the block holding the selection, both, or neither. When both
8
+ * would show a bar on the same block, only the toolbar does. A block with a kind icon (a composite) gets no handle: its
9
+ * icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent handles on every eligible
10
+ * block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
9
11
  */
10
12
  export declare function BlockUi({ editor }: {
11
13
  editor: Editor;
@@ -4,19 +4,22 @@ import { cn } from "../../../react/ui/utils.js";
4
4
  import { Icon } from "../../../react/Icon.js";
5
5
  import { useEditorSettings } from "../../../react/settings.js";
6
6
  import { firstLineCenterShift, gutterShiftOf, handleLeftOf, hasKindIcon } from "../handle-geometry.js";
7
+ import { actionSurfacesOf, sameBlock, selectedBlockOf } from "../block-actions.js";
7
8
  import { BlockActionsOverlay } from "./BlockActionsOverlay.js";
8
9
  import { BlockHandles } from "./BlockHandles.js";
9
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
12
  import { DragHandle } from "@tiptap/extension-drag-handle-react";
12
13
  import { createPortal } from "react-dom";
13
14
  //#region src/features/block-ui/react/BlockUi.tsx
14
15
  /**
15
16
  * Block UI of an editor: the drag handle on the left of the hovered block (hidden while the editor is read-only) and the
16
- * hover actions (`BlockActionsOverlay`) at the editor's right edge, both targeting the same block. The actions are those
17
- * the surface registered for `editor` (features' block actions, then the host's). A block with a kind icon (a composite)
18
- * gets no handle: its icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent
19
- * handles on every eligible block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
17
+ * block actions (`BlockActionsOverlay`) at the editor's right edge. The actions are those the surface registered for
18
+ * `editor` (features' block actions, then the host's), shown per `BlockUiOptions.actions`: on a hover bar for the block
19
+ * the drag handle targets, on a persistent toolbar for the block holding the selection, both, or neither. When both
20
+ * would show a bar on the same block, only the toolbar does. A block with a kind icon (a composite) gets no handle: its
21
+ * icon drags it. With `blocks.dragHandle: 'always'` the hover handle is replaced by permanent handles on every eligible
22
+ * block (`BlockHandles`). Mounted by the block-ui feature beside the editor.
20
23
  */
21
24
  function BlockUi({ editor }) {
22
25
  const [host, setHost] = useState(null);
@@ -24,6 +27,19 @@ function BlockUi({ editor }) {
24
27
  const [targetHasKindIcon, setTargetHasKindIcon] = useState(false);
25
28
  const { settings } = useEditorSettings();
26
29
  const actions = editorEntryOf(editor)?.blockActions ?? [];
30
+ const surfaces = actionSurfacesOf(editor);
31
+ const [, rerender] = useReducer((count) => count + 1, 0);
32
+ useEffect(() => {
33
+ const onTransaction = () => {
34
+ if (actionSurfacesOf(editor).toolbar) rerender();
35
+ };
36
+ editor.on("transaction", onTransaction);
37
+ return () => {
38
+ editor.off("transaction", onTransaction);
39
+ };
40
+ }, [editor]);
41
+ const selectedTarget = surfaces.toolbar ? selectedBlockOf(editor) : null;
42
+ const hoverBarTarget = hoverTarget && selectedTarget && sameBlock(editor, hoverTarget, selectedTarget) ? null : hoverTarget;
27
43
  const track = useRef({
28
44
  overlayHovered: false,
29
45
  clearTimer: void 0,
@@ -138,16 +154,28 @@ function BlockUi({ editor }) {
138
154
  ]
139
155
  };
140
156
  }, [track]);
141
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [host && createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BlockActionsOverlay, {
142
- editor,
143
- target: hoverTarget,
144
- host,
145
- actions,
146
- onHover: onOverlayHover
147
- }), alwaysHandles && /* @__PURE__ */ jsx(BlockHandles, {
148
- editor,
149
- host
150
- })] }), host), /* @__PURE__ */ jsx(DragHandle, {
157
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [host && createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [
158
+ surfaces.hover && /* @__PURE__ */ jsx(BlockActionsOverlay, {
159
+ editor,
160
+ target: hoverBarTarget,
161
+ host,
162
+ actions,
163
+ surface: "hover",
164
+ onHover: onOverlayHover
165
+ }),
166
+ surfaces.toolbar && /* @__PURE__ */ jsx(BlockActionsOverlay, {
167
+ editor,
168
+ target: selectedTarget,
169
+ host,
170
+ actions,
171
+ surface: "toolbar",
172
+ persistent: true
173
+ }),
174
+ alwaysHandles && /* @__PURE__ */ jsx(BlockHandles, {
175
+ editor,
176
+ host
177
+ })
178
+ ] }), host), /* @__PURE__ */ jsx(DragHandle, {
151
179
  editor,
152
180
  onElementDragEnd: resetDragHandle,
153
181
  onNodeChange: trackDragTarget,