@openeditor/react 0.0.26 → 0.0.27

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.
package/README.md CHANGED
@@ -11,7 +11,8 @@ Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments a
11
11
  - `OpenEditorViewer` for read-only React rendering
12
12
  - `getDefaultBlockPickerItems` for host-owned block rails, pickers, and command palettes
13
13
  - `getDefaultSlashMenuItems` for UI packages that want the built-in insert actions
14
- - position-based block targeting and transaction-backed copy, duplicate, move, and delete commands
14
+ - stable-identity block targeting and transaction-backed copy, duplicate, move, and delete commands
15
+ - `OpenEditorBlockDragHandle` and `useOpenEditorBlockInteraction` for headless block-handle UIs
15
16
  - `defineOpenEditorReactNode` for consumer-owned React blocks
16
17
  - `defineOpenEditorReactExtension` for advanced integrations that contribute multiple Tiptap extensions
17
18
 
@@ -19,21 +20,52 @@ This package intentionally does not import CSS and does not render styled menus.
19
20
 
20
21
  ## Block actions
21
22
 
22
- `controller.blockTarget` identifies the top-level block under the pointer or the
23
- active node selection. Mutating commands operate directly on ProseMirror
24
- transactions, preserving history and avoiding whole-document replacement:
23
+ Every addressable node has a persistent `openeditor-id`. The block interaction
24
+ snapshot publishes the stable block reference under the handle without
25
+ putting pointer-frequency state into the editor controller. Mutating commands
26
+ resolve that identity against the current ProseMirror document immediately
27
+ before dispatch, preserving history and avoiding stale positions:
25
28
 
26
29
  ```tsx
27
- controller.selectBlock(target);
28
- await controller.copyBlock(target);
29
- controller.duplicateBlock(target);
30
- controller.deleteBlock(target);
30
+ const { activeBlock } = useOpenEditorBlockInteraction(controller);
31
+ if (activeBlock) {
32
+ controller.selectBlock(activeBlock);
33
+ await controller.copyBlock(activeBlock);
34
+ controller.duplicateBlock(activeBlock);
35
+ controller.moveBlock(1, activeBlock);
36
+ controller.deleteBlock(activeBlock);
37
+ }
31
38
  ```
32
39
 
33
40
  Pass `blockActions` to contribute product-specific actions. The optional
34
41
  `OpenEditorBlockMenu` from `@openeditor/ui` combines those contributions with
35
- Copy, Duplicate, and Delete. Clipboard copies use a versioned OpenEditor block
36
- envelope so blocks can move between independent editor instances and documents.
42
+ Move Up, Move Down, Copy, Duplicate, and Delete. Action contexts receive
43
+ `context.block`, never a ProseMirror position or DOM rectangle. Clipboard copies
44
+ use a versioned OpenEditor block envelope so blocks can move between independent
45
+ editor instances and documents while receiving fresh identities.
46
+
47
+ Handle ownership is structural, not inferred from pointer proximity. Callouts,
48
+ quotes, tables, and consumer blocks own a single handle for their content by
49
+ default. Lists, toggles, and columns expose their independent nested blocks.
50
+ Set `blockHandle: "nested"` on a consumer extension whose child blocks should
51
+ receive their own handles. Placement always follows the live DOM element for the
52
+ resolved ProseMirror node, including through scrolling, resizing, and reflow.
53
+
54
+ Hover targeting is owned by OpenEditor rather than the Tiptap drag-handle
55
+ extension. The editor's interaction surface is one continuous hover region:
56
+ content, the reserved left gutter, and the handle itself. Pointer coordinates
57
+ resolve to the deepest structurally addressable block, with collapsed outer
58
+ margins included so gaps between blocks do not become dead zones. A short hide
59
+ delay lets the pointer cross from content to the handle, while pressing,
60
+ dragging, or opening the block menu freezes the active target. Pointer movement
61
+ is coalesced to one resolution per animation frame and identity changes are the
62
+ only changes published to React.
63
+
64
+ The gutter is part of layout, not a collision workaround. Its reservation is
65
+ continuous rather than breakpoint-based: the editor borrows only the portion
66
+ of the 42px gutter that physically fits to its left and reserves the remainder
67
+ inside its width. Floating placement may correct vertical overflow but must
68
+ never shift horizontally into block content.
37
69
 
38
70
  ## Host-owned block pickers
39
71
 
package/dist/index.d.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, CSSProperties, ComponentType } from 'react';
3
- import { OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorAttachmentRuntime } from '@openeditor/core';
2
+ import { ReactNode, CSSProperties, RefObject, ComponentType } from 'react';
3
+ import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorAttachmentRuntime } from '@openeditor/core';
4
+ export { OpenEditorBlockRef } from '@openeditor/core';
4
5
  import { toExportBundle, OpenEditorExporters } from '@openeditor/exporters';
5
6
  export { seedDocument } from '@openeditor/extensions';
6
- import { Editor, AnyExtension, NodeConfig } from '@tiptap/core';
7
- export { DragHandle as OpenEditorDragHandle } from '@tiptap/extension-drag-handle-react';
7
+ import { AnyExtension, NodeConfig } from '@tiptap/core';
8
8
  import { ReactNodeViewProps } from '@tiptap/react';
9
9
  export { NodeViewContent, NodeViewWrapper, ReactNodeViewProps as OpenEditorNodeViewProps } from '@tiptap/react';
10
10
 
11
+ type OpenEditorBlockInteractionSnapshot = {
12
+ activeBlock: OpenEditorBlockRef | null;
13
+ dragging: boolean;
14
+ };
15
+ type OpenEditorBlockInteractionStore = {
16
+ getSnapshot: () => OpenEditorBlockInteractionSnapshot;
17
+ subscribe: (listener: () => void) => () => void;
18
+ };
19
+
11
20
  declare const openEditorThemeTokenNames: readonly ["surface", "surfaceRaised", "surfaceMuted", "blockSurface", "text", "textSoft", "heading", "muted", "placeholder", "border", "borderStrong", "structuralLine", "accent", "accentText", "accentStrong", "buttonBackground", "buttonText", "codeBackground", "codeText", "link", "linkHover", "shadow", "fontSans", "fontMono", "radiusSmall", "radiusMedium", "radiusLarge", "spaceBlock", "spaceInline"];
12
21
  type OpenEditorThemeToken = (typeof openEditorThemeTokenNames)[number];
13
22
  type OpenEditorTheme = Partial<Record<OpenEditorThemeToken, string>>;
@@ -49,16 +58,9 @@ type OpenEditorReactProps = {
49
58
  extensions?: readonly OpenEditorReactExtension[];
50
59
  blockActions?: readonly OpenEditorBlockAction[];
51
60
  };
52
- type OpenEditorBlockTarget = {
53
- pos: number;
54
- depth: number;
55
- nodeType: string;
56
- blockName?: string;
57
- rect: OpenEditorOverlayRect;
58
- };
59
61
  type OpenEditorBlockActionContext = {
60
62
  controller: OpenEditorController;
61
- target: OpenEditorBlockTarget;
63
+ block: OpenEditorBlockRef;
62
64
  };
63
65
  type OpenEditorBlockAction = {
64
66
  id: string;
@@ -69,9 +71,9 @@ type OpenEditorBlockAction = {
69
71
  };
70
72
  type OpenEditorReactConfig = OpenEditorReactProps;
71
73
  type OpenEditorController = {
72
- editor: Editor | null;
73
74
  document: OpenEditorDocument;
74
75
  exports: ReturnType<typeof toExportBundle>;
76
+ ready: boolean;
75
77
  editable: boolean;
76
78
  placeholder: string;
77
79
  selectedBlock: string;
@@ -95,17 +97,17 @@ type OpenEditorController = {
95
97
  insertBlock: (blockName?: string, range?: InsertRange) => boolean;
96
98
  setLink: (url?: string | null) => void;
97
99
  getActiveLink: () => string | undefined;
98
- moveSelectedBlock: (direction: -1 | 1) => void;
99
- duplicateSelectedBlock: () => void;
100
- deleteSelectedBlock: () => void;
101
- selectBlock: (target: OpenEditorBlockTarget) => void;
102
- copyBlock: (target?: OpenEditorBlockTarget) => Promise<boolean>;
103
- duplicateBlock: (target?: OpenEditorBlockTarget) => boolean;
104
- deleteBlock: (target?: OpenEditorBlockTarget) => boolean;
105
- moveBlock: (direction: -1 | 1, target?: OpenEditorBlockTarget) => boolean;
100
+ selectBlock: (block: OpenEditorBlockRef) => boolean;
101
+ copyBlock: (block?: OpenEditorBlockRef) => Promise<boolean>;
102
+ duplicateBlock: (block?: OpenEditorBlockRef) => boolean;
103
+ deleteBlock: (block?: OpenEditorBlockRef) => boolean;
104
+ moveBlock: (direction: -1 | 1, block?: OpenEditorBlockRef) => boolean;
105
+ canMoveBlock: (direction: -1 | 1, block?: OpenEditorBlockRef) => boolean;
106
106
  blockActions: readonly OpenEditorBlockAction[];
107
- blockTarget: OpenEditorBlockTarget | null;
108
- setBlockTargetAt: (pos: number | null) => void;
107
+ blockInteraction: OpenEditorBlockInteractionStore;
108
+ getRootElement: () => HTMLElement | null;
109
+ setBlockHandleLocked: (locked: boolean) => void;
110
+ isActive: (name: string, attributes?: Record<string, unknown>) => boolean;
109
111
  slashMenuItems: readonly OpenEditorSlashMenuItem[];
110
112
  extensions: readonly OpenEditorReactExtension[];
111
113
  pageRuntime?: OpenEditorPageRuntime;
@@ -196,6 +198,12 @@ type OpenEditorExtensionMenu = {
196
198
  type OpenEditorReactExtension = {
197
199
  block: BlockSpec;
198
200
  tiptapExtensions: readonly AnyExtension[];
201
+ /**
202
+ * Controls ownership when this block contains other block nodes. Containers
203
+ * own one handle by default; use `nested` when their children are independent
204
+ * draggable blocks, as in column layouts.
205
+ */
206
+ blockHandle?: "self" | "nested";
199
207
  /** Discovery metadata shared by slash menus and host-owned block pickers. */
200
208
  insertMenu?: false | OpenEditorExtensionMenu;
201
209
  /** @deprecated Use `insertMenu`. */
@@ -227,6 +235,23 @@ declare module "@tiptap/core" {
227
235
  }
228
236
  declare const formatAttachmentSize: (size: number | null) => string;
229
237
  declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
238
+ declare const useOpenEditorBlockInteraction: (controller: OpenEditorController) => OpenEditorBlockInteractionSnapshot;
239
+ type OpenEditorBlockDragHandleProps = {
240
+ controller: OpenEditorController;
241
+ children: ReactNode;
242
+ className?: string;
243
+ ariaLabel?: string;
244
+ elementRef?: RefObject<HTMLDivElement | null>;
245
+ expanded?: boolean;
246
+ onActivate?: () => void;
247
+ onDraggingChange?: (dragging: boolean) => void;
248
+ };
249
+ /**
250
+ * Headless block handle whose outer DOM element owns both native dragging and
251
+ * activation. Keep visual children pointer-transparent so nested controls do
252
+ * not intercept the dragstart gesture.
253
+ */
254
+ declare const OpenEditorBlockDragHandle: ({ controller, children, className, ariaLabel, elementRef, expanded, onActivate, onDraggingChange, }: OpenEditorBlockDragHandleProps) => react.ReactPortal | null;
230
255
  declare const sortSlashMenuItems: (items: readonly OpenEditorSlashMenuItem[]) => OpenEditorSlashMenuItem[];
231
256
  declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]) => OpenEditorBlockPickerItem[];
232
257
  declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
@@ -235,4 +260,4 @@ declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentP
235
260
  declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
236
261
  declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
237
262
 
238
- export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, type OpenEditorBlockPickerItem, type OpenEditorBlockTarget, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SelectionBubbleState, type SlashState, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
263
+ export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, OpenEditorBlockDragHandle, type OpenEditorBlockDragHandleProps, type OpenEditorBlockPickerItem, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SelectionBubbleState, type SlashState, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorBlockInteraction, useOpenEditorController, useOpenEditorThemeStyle };