@openeditor/react 0.0.26 → 0.0.28

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
@@ -9,9 +9,11 @@ Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments a
9
9
  - `useOpenEditorController` for editor state, commands, document exports, and selection state
10
10
  - `OpenEditorContent` for the editable Tiptap-backed content surface
11
11
  - `OpenEditorViewer` for read-only React rendering
12
+ - `OpenEditorPageHeader` for host-backed page title and icon editing
12
13
  - `getDefaultBlockPickerItems` for host-owned block rails, pickers, and command palettes
13
14
  - `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
15
+ - stable-identity block targeting and transaction-backed copy, duplicate, move, and delete commands
16
+ - `OpenEditorBlockDragHandle` and `useOpenEditorBlockInteraction` for headless block-handle UIs
15
17
  - `defineOpenEditorReactNode` for consumer-owned React blocks
16
18
  - `defineOpenEditorReactExtension` for advanced integrations that contribute multiple Tiptap extensions
17
19
 
@@ -19,21 +21,52 @@ This package intentionally does not import CSS and does not render styled menus.
19
21
 
20
22
  ## Block actions
21
23
 
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:
24
+ Every addressable node has a persistent `openeditor-id`. The block interaction
25
+ snapshot publishes the stable block reference under the handle without
26
+ putting pointer-frequency state into the editor controller. Mutating commands
27
+ resolve that identity against the current ProseMirror document immediately
28
+ before dispatch, preserving history and avoiding stale positions:
25
29
 
26
30
  ```tsx
27
- controller.selectBlock(target);
28
- await controller.copyBlock(target);
29
- controller.duplicateBlock(target);
30
- controller.deleteBlock(target);
31
+ const { activeBlock } = useOpenEditorBlockInteraction(controller);
32
+ if (activeBlock) {
33
+ controller.selectBlock(activeBlock);
34
+ await controller.copyBlock(activeBlock);
35
+ controller.duplicateBlock(activeBlock);
36
+ controller.moveBlock(1, activeBlock);
37
+ controller.deleteBlock(activeBlock);
38
+ }
31
39
  ```
32
40
 
33
41
  Pass `blockActions` to contribute product-specific actions. The optional
34
42
  `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.
43
+ Move Up, Move Down, Copy, Duplicate, and Delete. Action contexts receive
44
+ `context.block`, never a ProseMirror position or DOM rectangle. Clipboard copies
45
+ use a versioned OpenEditor block envelope so blocks can move between independent
46
+ editor instances and documents while receiving fresh identities.
47
+
48
+ Handle ownership is structural, not inferred from pointer proximity. Callouts,
49
+ quotes, tables, and consumer blocks own a single handle for their content by
50
+ default. Lists, toggles, and columns expose their independent nested blocks.
51
+ Set `blockHandle: "nested"` on a consumer extension whose child blocks should
52
+ receive their own handles. Placement always follows the live DOM element for the
53
+ resolved ProseMirror node, including through scrolling, resizing, and reflow.
54
+
55
+ Hover targeting is owned by OpenEditor rather than the Tiptap drag-handle
56
+ extension. The editor's interaction surface is one continuous hover region:
57
+ content, the reserved left gutter, and the handle itself. Pointer coordinates
58
+ resolve to the deepest structurally addressable block, with collapsed outer
59
+ margins included so gaps between blocks do not become dead zones. A short hide
60
+ delay lets the pointer cross from content to the handle, while pressing,
61
+ dragging, or opening the block menu freezes the active target. Pointer movement
62
+ is coalesced to one resolution per animation frame and identity changes are the
63
+ only changes published to React.
64
+
65
+ The gutter is part of layout, not a collision workaround. Its reservation is
66
+ continuous rather than breakpoint-based: the editor borrows only the portion
67
+ of the 42px gutter that physically fits to its left and reserves the remainder
68
+ inside its width. Floating placement may correct vertical overflow but must
69
+ never shift horizontally into block content.
37
70
 
38
71
  ## Host-owned block pickers
39
72
 
@@ -88,12 +121,24 @@ themeable SVG rendering and retain their canonical Mermaid source in the documen
88
121
  import { OpenEditorContent, useOpenEditorController } from "@openeditor/react";
89
122
 
90
123
  export function Example() {
91
- const controller = useOpenEditorController();
124
+ const controller = useOpenEditorController({ initialDocument });
92
125
 
93
126
  return <OpenEditorContent controller={controller} />;
94
127
  }
95
128
  ```
96
129
 
130
+ Editors are uncontrolled on both web and native. `initialDocument` is read once;
131
+ use `controller.setContent(document)` for an undoable programmatic replacement.
132
+ It emits `onChange` and preserves the current selection where possible. Runtime
133
+ changes to callbacks, `editable`, and `placeholder` are applied without remounting.
134
+
135
+ Exports are lazy. `controller.export("html")` returns publishing-safe built-in
136
+ HTML. `controller.exportUnsafe("html")` opts into trusted custom extension HTML.
137
+
138
+ Pass `enabledBlocks` to restrict authoring without removing nodes from the
139
+ schema. Existing disabled blocks remain readable. Runtime-dependent insertion
140
+ is automatically disabled unless page creation or attachment upload is wired.
141
+
97
142
  ## Custom blocks
98
143
 
99
144
  Custom blocks belong to the consuming product. OpenEditor hosts them without
@@ -183,8 +228,9 @@ export function ProductEditor() {
183
228
  }
184
229
  ```
185
230
 
186
- Pass the same extension array to `OpenEditorViewer`. The controller automatically
187
- uses registered exporters for `controller.exports` and registered blocks appear
231
+ Pass the same extension array to `OpenEditorViewer`. The controller uses
232
+ registered exporters only through the explicit `controller.exportUnsafe(...)`
233
+ path, and registered blocks appear
188
234
  in `getDefaultSlashMenuItems`, which powers `@openeditor/ui`.
189
235
 
190
236
  Insert-menu entries accept an optional React `icon` component and numeric `order`.
@@ -193,8 +239,7 @@ blocks can be placed between them (for example, `order: 250` appears between
193
239
  Heading 1 and Heading 2). Entries with the same order retain their declaration
194
240
  order, and entries without an order appear after ordered entries. This metadata
195
241
  is shared by slash menus and `getDefaultBlockPickerItems`; use `insertMenu: false`
196
- to keep a block out of both discovery surfaces. The legacy `slashMenu` extension
197
- field remains supported. The same fields are available on direct
242
+ to keep a block out of both discovery surfaces. The same fields are available on direct
198
243
  `slashMenuItems`; a slash menu's explicit `items` prop is also sorted by this rule.
199
244
 
200
245
  Block `name` is the stable public identifier. Namespace consumer blocks to avoid
@@ -206,19 +251,23 @@ Duplicate names and node types fail immediately during editor construction.
206
251
 
207
252
  The first-party `page` block models a Notion-style child page as a reference to
208
253
  a separately persisted document. Supply `pageRuntime` to connect creation,
209
- resolution, renaming, and navigation to your application:
254
+ resolution, metadata updates, and navigation to your application:
210
255
 
211
256
  ```tsx
212
257
  const controller = useOpenEditorController({
213
258
  pageRuntime: {
214
259
  createPage: async ({ title, icon }) => api.pages.create({ title, icon }),
215
260
  resolvePage: async (pageId) => api.pages.get(pageId),
216
- renamePage: async (pageId, title) => api.pages.rename(pageId, title),
261
+ updatePage: async (pageId, update) => api.pages.update(pageId, update),
217
262
  openPage: (page) => router.push(`/pages/${page.pageId}`),
218
263
  },
219
264
  });
220
265
  ```
221
266
 
267
+ Render `OpenEditorPageHeader` on the opened page surface to provide the canonical
268
+ editable title and icon. Both fields call `pageRuntime.updatePage`; page references
269
+ resolve the same page entity instead of owning independent metadata.
270
+
222
271
  The document stores only the stable reference and cached presentation. Page
223
272
  contents, hierarchy, permissions, routing, and deletion remain host-owned.
224
273
 
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';
4
- import { toExportBundle, OpenEditorExporters } from '@openeditor/exporters';
2
+ import { ReactNode, CSSProperties, RefObject, ComponentType } from 'react';
3
+ import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
+ export { OpenEditorBlockRef, OpenEditorPageRuntime, OpenEditorPageSnapshot } from '@openeditor/core';
5
+ import { ExportFormat, 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>>;
@@ -20,23 +29,7 @@ declare function OpenEditorThemeProvider({ children, className, theme }: {
20
29
  theme: OpenEditorTheme;
21
30
  }): react.JSX.Element;
22
31
 
23
- type OpenEditorPageSnapshot = {
24
- pageId: string;
25
- title: string;
26
- icon?: string | null;
27
- href?: string | null;
28
- };
29
- type OpenEditorPageRuntime = {
30
- createPage?: (input: {
31
- title: string;
32
- icon?: string | null;
33
- }) => Promise<OpenEditorPageSnapshot>;
34
- resolvePage?: (pageId: string) => Promise<OpenEditorPageSnapshot | null>;
35
- renamePage?: (pageId: string, title: string) => Promise<void> | void;
36
- updatePageIcon?: (pageId: string, icon: string) => Promise<void> | void;
37
- openPage?: (page: OpenEditorPageSnapshot) => Promise<void> | void;
38
- };
39
- type OpenEditorReactProps = {
32
+ type OpenEditorReactProps = OpenEditorAuthoringCapabilities & {
40
33
  initialDocument?: OpenEditorDocument;
41
34
  editable?: boolean;
42
35
  placeholder?: string;
@@ -49,16 +42,9 @@ type OpenEditorReactProps = {
49
42
  extensions?: readonly OpenEditorReactExtension[];
50
43
  blockActions?: readonly OpenEditorBlockAction[];
51
44
  };
52
- type OpenEditorBlockTarget = {
53
- pos: number;
54
- depth: number;
55
- nodeType: string;
56
- blockName?: string;
57
- rect: OpenEditorOverlayRect;
58
- };
59
45
  type OpenEditorBlockActionContext = {
60
46
  controller: OpenEditorController;
61
- target: OpenEditorBlockTarget;
47
+ block: OpenEditorBlockRef;
62
48
  };
63
49
  type OpenEditorBlockAction = {
64
50
  id: string;
@@ -69,9 +55,10 @@ type OpenEditorBlockAction = {
69
55
  };
70
56
  type OpenEditorReactConfig = OpenEditorReactProps;
71
57
  type OpenEditorController = {
72
- editor: Editor | null;
73
58
  document: OpenEditorDocument;
74
- exports: ReturnType<typeof toExportBundle>;
59
+ export: (format: ExportFormat) => string;
60
+ exportUnsafe: (format: ExportFormat) => string;
61
+ ready: boolean;
75
62
  editable: boolean;
76
63
  placeholder: string;
77
64
  selectedBlock: string;
@@ -93,23 +80,25 @@ type OpenEditorController = {
93
80
  undo: () => void;
94
81
  redo: () => void;
95
82
  insertBlock: (blockName?: string, range?: InsertRange) => boolean;
83
+ isBlockEnabled: (blockName: string) => boolean;
96
84
  setLink: (url?: string | null) => void;
97
85
  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;
86
+ selectBlock: (block: OpenEditorBlockRef) => boolean;
87
+ copyBlock: (block?: OpenEditorBlockRef) => Promise<boolean>;
88
+ duplicateBlock: (block?: OpenEditorBlockRef) => boolean;
89
+ deleteBlock: (block?: OpenEditorBlockRef) => boolean;
90
+ moveBlock: (direction: -1 | 1, block?: OpenEditorBlockRef) => boolean;
91
+ canMoveBlock: (direction: -1 | 1, block?: OpenEditorBlockRef) => boolean;
106
92
  blockActions: readonly OpenEditorBlockAction[];
107
- blockTarget: OpenEditorBlockTarget | null;
108
- setBlockTargetAt: (pos: number | null) => void;
93
+ blockInteraction: OpenEditorBlockInteractionStore;
94
+ getRootElement: () => HTMLElement | null;
95
+ setBlockHandleLocked: (locked: boolean) => void;
96
+ isActive: (name: string, attributes?: Record<string, unknown>) => boolean;
109
97
  slashMenuItems: readonly OpenEditorSlashMenuItem[];
110
98
  extensions: readonly OpenEditorReactExtension[];
111
99
  pageRuntime?: OpenEditorPageRuntime;
112
100
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
101
+ enabledBlocks?: readonly string[];
113
102
  slashState: SlashState | null;
114
103
  selectionBubbleState: SelectionBubbleState | null;
115
104
  };
@@ -196,10 +185,14 @@ type OpenEditorExtensionMenu = {
196
185
  type OpenEditorReactExtension = {
197
186
  block: BlockSpec;
198
187
  tiptapExtensions: readonly AnyExtension[];
188
+ /**
189
+ * Controls ownership when this block contains other block nodes. Containers
190
+ * own one handle by default; use `nested` when their children are independent
191
+ * draggable blocks, as in column layouts.
192
+ */
193
+ blockHandle?: "self" | "nested";
199
194
  /** Discovery metadata shared by slash menus and host-owned block pickers. */
200
195
  insertMenu?: false | OpenEditorExtensionMenu;
201
- /** @deprecated Use `insertMenu`. */
202
- slashMenu?: false | OpenEditorExtensionMenu;
203
196
  viewer: OpenEditorViewerRenderer;
204
197
  exporters?: OpenEditorExporters;
205
198
  };
@@ -226,13 +219,38 @@ declare module "@tiptap/core" {
226
219
  }
227
220
  }
228
221
  declare const formatAttachmentSize: (size: number | null) => string;
229
- declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
222
+ type OpenEditorPageHeaderProps = {
223
+ page: OpenEditorPageSnapshot;
224
+ runtime: OpenEditorPageRuntime;
225
+ className?: string;
226
+ onPageChange?: (page: OpenEditorPageSnapshot) => void;
227
+ };
228
+ /** Canonical Notion-style page metadata editor shared by host page surfaces. */
229
+ declare const OpenEditorPageHeader: ({ page, runtime, className, onPageChange, }: OpenEditorPageHeaderProps) => react.JSX.Element;
230
+ declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
231
+ declare const useOpenEditorBlockInteraction: (controller: OpenEditorController) => OpenEditorBlockInteractionSnapshot;
232
+ type OpenEditorBlockDragHandleProps = {
233
+ controller: OpenEditorController;
234
+ children: ReactNode;
235
+ className?: string;
236
+ ariaLabel?: string;
237
+ elementRef?: RefObject<HTMLDivElement | null>;
238
+ expanded?: boolean;
239
+ onActivate?: () => void;
240
+ onDraggingChange?: (dragging: boolean) => void;
241
+ };
242
+ /**
243
+ * Headless block handle whose outer DOM element owns both native dragging and
244
+ * activation. Keep visual children pointer-transparent so nested controls do
245
+ * not intercept the dragstart gesture.
246
+ */
247
+ declare const OpenEditorBlockDragHandle: ({ controller, children, className, ariaLabel, elementRef, expanded, onActivate, onDraggingChange, }: OpenEditorBlockDragHandleProps) => react.ReactPortal | null;
230
248
  declare const sortSlashMenuItems: (items: readonly OpenEditorSlashMenuItem[]) => OpenEditorSlashMenuItem[];
231
249
  declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]) => OpenEditorBlockPickerItem[];
232
250
  declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
233
251
  declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
234
252
  declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
235
253
  declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
236
- declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
254
+ declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
237
255
 
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 };
256
+ export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, OpenEditorBlockDragHandle, type OpenEditorBlockDragHandleProps, type OpenEditorBlockPickerItem, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, OpenEditorPageHeader, type OpenEditorPageHeaderProps, 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 };