@openeditor/react 0.0.27 → 0.0.29

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,6 +9,7 @@ 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
15
  - stable-identity block targeting and transaction-backed copy, duplicate, move, and delete commands
@@ -18,6 +19,40 @@ Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments a
18
19
 
19
20
  This package intentionally does not import CSS and does not render styled menus. Use `@openeditor/ui` for the optional styled components.
20
21
 
22
+ ## Read-only rendering and URL policy
23
+
24
+ `OpenEditorViewer` renders the portable document through an explicit registry of
25
+ built-in and structural renderers. Unknown nodes are preserved for forward
26
+ readability but carry `data-openeditor-unknown-node` so unsupported content never
27
+ silently appears to be a fully supported block. Built-in viewer output and safe
28
+ HTML export share stable `oe-*` classes and `data-openeditor-*` hooks for document
29
+ semantics such as columns, tasks, toggles, callouts, tables, pages, and files.
30
+
31
+ Viewer URLs use `openEditorPublicUrlPolicy` by default. The policy covers links,
32
+ images, pages, and attachments, including snapshots returned by page and
33
+ attachment runtimes. It accepts HTTP(S) and ordinary relative references, plus
34
+ `mailto:` and `tel:` for text links, while rejecting executable and local-preview
35
+ schemes. Owner previews can opt into a narrowly scoped policy:
36
+
37
+ ```tsx
38
+ import {
39
+ OpenEditorViewer,
40
+ openEditorPublicUrlPolicy,
41
+ type OpenEditorUrlPolicy,
42
+ } from "@openeditor/react";
43
+
44
+ const previewUrlPolicy: OpenEditorUrlPolicy = (value, context) => {
45
+ if (context === "attachment" && value.startsWith("blob:")) return value;
46
+ return openEditorPublicUrlPolicy(value, context);
47
+ };
48
+
49
+ <OpenEditorViewer document={document} urlPolicy={previewUrlPolicy} />;
50
+ ```
51
+
52
+ Custom viewer renderers receive both `urlPolicy` and a typed `resolveUrl` helper.
53
+ The host remains responsible for arbitrary custom renderer output and for actions
54
+ performed inside `openPage` or `openAttachment` callbacks.
55
+
21
56
  ## Block actions
22
57
 
23
58
  Every addressable node has a persistent `openeditor-id`. The block interaction
@@ -120,12 +155,24 @@ themeable SVG rendering and retain their canonical Mermaid source in the documen
120
155
  import { OpenEditorContent, useOpenEditorController } from "@openeditor/react";
121
156
 
122
157
  export function Example() {
123
- const controller = useOpenEditorController();
158
+ const controller = useOpenEditorController({ initialDocument });
124
159
 
125
160
  return <OpenEditorContent controller={controller} />;
126
161
  }
127
162
  ```
128
163
 
164
+ Editors are uncontrolled on both web and native. `initialDocument` is read once;
165
+ use `controller.setContent(document)` for an undoable programmatic replacement.
166
+ It emits `onChange` and preserves the current selection where possible. Runtime
167
+ changes to callbacks, `editable`, and `placeholder` are applied without remounting.
168
+
169
+ Exports are lazy. `controller.export("html")` returns publishing-safe built-in
170
+ HTML. `controller.exportUnsafe("html")` opts into trusted custom extension HTML.
171
+
172
+ Pass `enabledBlocks` to restrict authoring without removing nodes from the
173
+ schema. Existing disabled blocks remain readable. Runtime-dependent insertion
174
+ is automatically disabled unless page creation or attachment upload is wired.
175
+
129
176
  ## Custom blocks
130
177
 
131
178
  Custom blocks belong to the consuming product. OpenEditor hosts them without
@@ -195,7 +242,11 @@ const productCard = defineOpenEditorReactNode({
195
242
  keywords: ["product", "commerce", "card"],
196
243
  order: 250,
197
244
  },
198
- viewer: ({ node }) => <article>{String(node.attrs?.title ?? "")}</article>,
245
+ viewer: ({ node, resolveUrl }) => (
246
+ <article>
247
+ <a href={resolveUrl(node.attrs?.href, "link")}>{String(node.attrs?.title ?? "")}</a>
248
+ </article>
249
+ ),
199
250
  exporters: {
200
251
  html: {
201
252
  acmeProductCard: ({ node, escapeHtml }) =>
@@ -215,8 +266,9 @@ export function ProductEditor() {
215
266
  }
216
267
  ```
217
268
 
218
- Pass the same extension array to `OpenEditorViewer`. The controller automatically
219
- uses registered exporters for `controller.exports` and registered blocks appear
269
+ Pass the same extension array to `OpenEditorViewer`. The controller uses
270
+ registered exporters only through the explicit `controller.exportUnsafe(...)`
271
+ path, and registered blocks appear
220
272
  in `getDefaultSlashMenuItems`, which powers `@openeditor/ui`.
221
273
 
222
274
  Insert-menu entries accept an optional React `icon` component and numeric `order`.
@@ -225,8 +277,7 @@ blocks can be placed between them (for example, `order: 250` appears between
225
277
  Heading 1 and Heading 2). Entries with the same order retain their declaration
226
278
  order, and entries without an order appear after ordered entries. This metadata
227
279
  is shared by slash menus and `getDefaultBlockPickerItems`; use `insertMenu: false`
228
- to keep a block out of both discovery surfaces. The legacy `slashMenu` extension
229
- field remains supported. The same fields are available on direct
280
+ to keep a block out of both discovery surfaces. The same fields are available on direct
230
281
  `slashMenuItems`; a slash menu's explicit `items` prop is also sorted by this rule.
231
282
 
232
283
  Block `name` is the stable public identifier. Namespace consumer blocks to avoid
@@ -238,19 +289,23 @@ Duplicate names and node types fail immediately during editor construction.
238
289
 
239
290
  The first-party `page` block models a Notion-style child page as a reference to
240
291
  a separately persisted document. Supply `pageRuntime` to connect creation,
241
- resolution, renaming, and navigation to your application:
292
+ resolution, metadata updates, and navigation to your application:
242
293
 
243
294
  ```tsx
244
295
  const controller = useOpenEditorController({
245
296
  pageRuntime: {
246
297
  createPage: async ({ title, icon }) => api.pages.create({ title, icon }),
247
298
  resolvePage: async (pageId) => api.pages.get(pageId),
248
- renamePage: async (pageId, title) => api.pages.rename(pageId, title),
299
+ updatePage: async (pageId, update) => api.pages.update(pageId, update),
249
300
  openPage: (page) => router.push(`/pages/${page.pageId}`),
250
301
  },
251
302
  });
252
303
  ```
253
304
 
305
+ Render `OpenEditorPageHeader` on the opened page surface to provide the canonical
306
+ editable title and icon. Both fields call `pageRuntime.updatePage`; page references
307
+ resolve the same page entity instead of owning independent metadata.
308
+
254
309
  The document stores only the stable reference and cached presentation. Page
255
310
  contents, hierarchy, permissions, routing, and deletion remain host-owned.
256
311
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, RefObject, ComponentType } from 'react';
3
- import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorAttachmentRuntime } from '@openeditor/core';
4
- export { OpenEditorBlockRef } from '@openeditor/core';
5
- import { toExportBundle, OpenEditorExporters } from '@openeditor/exporters';
3
+ import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorUrlPolicy, OpenEditorUrlContext, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
+ export { OpenEditorBlockRef, OpenEditorPageRuntime, OpenEditorPageSnapshot, OpenEditorUrlContext, OpenEditorUrlPolicy, openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openeditor/core';
5
+ import { ExportFormat, OpenEditorExporters } from '@openeditor/exporters';
6
6
  export { seedDocument } from '@openeditor/extensions';
7
7
  import { AnyExtension, NodeConfig } from '@tiptap/core';
8
8
  import { ReactNodeViewProps } from '@tiptap/react';
@@ -17,7 +17,7 @@ type OpenEditorBlockInteractionStore = {
17
17
  subscribe: (listener: () => void) => () => void;
18
18
  };
19
19
 
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"];
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", "bodyFontSize", "bodyLineHeight", "headingFont", "headingLineHeight", "headingWeight", "heading1Size", "heading2Size", "heading3Size", "heading4Size", "heading5Size", "heading6Size"];
21
21
  type OpenEditorThemeToken = (typeof openEditorThemeTokenNames)[number];
22
22
  type OpenEditorTheme = Partial<Record<OpenEditorThemeToken, string>>;
23
23
  type OpenEditorThemeStyle = CSSProperties & Record<`--oe-${string}`, string | undefined>;
@@ -29,23 +29,7 @@ declare function OpenEditorThemeProvider({ children, className, theme }: {
29
29
  theme: OpenEditorTheme;
30
30
  }): react.JSX.Element;
31
31
 
32
- type OpenEditorPageSnapshot = {
33
- pageId: string;
34
- title: string;
35
- icon?: string | null;
36
- href?: string | null;
37
- };
38
- type OpenEditorPageRuntime = {
39
- createPage?: (input: {
40
- title: string;
41
- icon?: string | null;
42
- }) => Promise<OpenEditorPageSnapshot>;
43
- resolvePage?: (pageId: string) => Promise<OpenEditorPageSnapshot | null>;
44
- renamePage?: (pageId: string, title: string) => Promise<void> | void;
45
- updatePageIcon?: (pageId: string, icon: string) => Promise<void> | void;
46
- openPage?: (page: OpenEditorPageSnapshot) => Promise<void> | void;
47
- };
48
- type OpenEditorReactProps = {
32
+ type OpenEditorReactProps = OpenEditorAuthoringCapabilities & {
49
33
  initialDocument?: OpenEditorDocument;
50
34
  editable?: boolean;
51
35
  placeholder?: string;
@@ -72,7 +56,8 @@ type OpenEditorBlockAction = {
72
56
  type OpenEditorReactConfig = OpenEditorReactProps;
73
57
  type OpenEditorController = {
74
58
  document: OpenEditorDocument;
75
- exports: ReturnType<typeof toExportBundle>;
59
+ export: (format: ExportFormat) => string;
60
+ exportUnsafe: (format: ExportFormat) => string;
76
61
  ready: boolean;
77
62
  editable: boolean;
78
63
  placeholder: string;
@@ -95,6 +80,7 @@ type OpenEditorController = {
95
80
  undo: () => void;
96
81
  redo: () => void;
97
82
  insertBlock: (blockName?: string, range?: InsertRange) => boolean;
83
+ isBlockEnabled: (blockName: string) => boolean;
98
84
  setLink: (url?: string | null) => void;
99
85
  getActiveLink: () => string | undefined;
100
86
  selectBlock: (block: OpenEditorBlockRef) => boolean;
@@ -112,6 +98,7 @@ type OpenEditorController = {
112
98
  extensions: readonly OpenEditorReactExtension[];
113
99
  pageRuntime?: OpenEditorPageRuntime;
114
100
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
101
+ enabledBlocks?: readonly string[];
115
102
  slashState: SlashState | null;
116
103
  selectionBubbleState: SelectionBubbleState | null;
117
104
  };
@@ -182,6 +169,8 @@ type OpenEditorBlockPickerItem = {
182
169
  type OpenEditorViewerRenderer = (context: {
183
170
  node: ProseMirrorNode;
184
171
  children: ReactNode;
172
+ urlPolicy: OpenEditorUrlPolicy;
173
+ resolveUrl: (value: unknown, context: OpenEditorUrlContext) => string | undefined;
185
174
  }) => ReactNode;
186
175
  type OpenEditorExtensionMenu = {
187
176
  label?: string;
@@ -206,8 +195,6 @@ type OpenEditorReactExtension = {
206
195
  blockHandle?: "self" | "nested";
207
196
  /** Discovery metadata shared by slash menus and host-owned block pickers. */
208
197
  insertMenu?: false | OpenEditorExtensionMenu;
209
- /** @deprecated Use `insertMenu`. */
210
- slashMenu?: false | OpenEditorExtensionMenu;
211
198
  viewer: OpenEditorViewerRenderer;
212
199
  exporters?: OpenEditorExporters;
213
200
  };
@@ -225,6 +212,8 @@ type OpenEditorViewerProps = {
225
212
  extensions?: readonly OpenEditorReactExtension[];
226
213
  pageRuntime?: OpenEditorPageRuntime;
227
214
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
215
+ /** Safe by default. Supply an explicit host policy for trusted preview URLs such as `blob:`. */
216
+ urlPolicy?: OpenEditorUrlPolicy;
228
217
  };
229
218
  declare module "@tiptap/core" {
230
219
  interface Commands<ReturnType> {
@@ -234,7 +223,15 @@ declare module "@tiptap/core" {
234
223
  }
235
224
  }
236
225
  declare const formatAttachmentSize: (size: number | null) => string;
237
- declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
226
+ type OpenEditorPageHeaderProps = {
227
+ page: OpenEditorPageSnapshot;
228
+ runtime: OpenEditorPageRuntime;
229
+ className?: string;
230
+ onPageChange?: (page: OpenEditorPageSnapshot) => void;
231
+ };
232
+ /** Canonical Notion-style page metadata editor shared by host page surfaces. */
233
+ declare const OpenEditorPageHeader: ({ page, runtime, className, onPageChange, }: OpenEditorPageHeaderProps) => react.JSX.Element;
234
+ declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
238
235
  declare const useOpenEditorBlockInteraction: (controller: OpenEditorController) => OpenEditorBlockInteractionSnapshot;
239
236
  type OpenEditorBlockDragHandleProps = {
240
237
  controller: OpenEditorController;
@@ -257,7 +254,7 @@ declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]
257
254
  declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
258
255
  declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
259
256
  declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
260
- declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
261
- declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
257
+ declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, urlPolicy, }: OpenEditorViewerProps) => react.JSX.Element;
258
+ declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
262
259
 
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 };
260
+ 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 };