@openeditor/react 0.0.22 → 0.0.24

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
+ - `getDefaultBlockPickerItems` for host-owned block rails, pickers, and command palettes
12
13
  - `getDefaultSlashMenuItems` for UI packages that want the built-in insert actions
13
14
  - position-based block targeting and transaction-backed copy, duplicate, move, and delete commands
14
15
  - `defineOpenEditorReactNode` for consumer-owned React blocks
@@ -34,6 +35,49 @@ Pass `blockActions` to contribute product-specific actions. The optional
34
35
  Copy, Duplicate, and Delete. Clipboard copies use a versioned OpenEditor block
35
36
  envelope so blocks can move between independent editor instances and documents.
36
37
 
38
+ ## Host-owned block pickers
39
+
40
+ Use `getDefaultBlockPickerItems` when blocks need to be discoverable outside the
41
+ slash menu. The catalog contains built-in insert presets and registered custom
42
+ blocks, but deliberately excludes slash-only actions such as moving a block or
43
+ converting a whole document. Use `sortBlockPickerItems` when merging additional
44
+ host-owned entries into the catalog.
45
+
46
+ ```tsx
47
+ import {
48
+ getDefaultBlockPickerItems,
49
+ OpenEditorContent,
50
+ useOpenEditorController,
51
+ } from "@openeditor/react";
52
+
53
+ export function EditorWithBlockRail() {
54
+ const controller = useOpenEditorController({ extensions });
55
+ const items = getDefaultBlockPickerItems(controller);
56
+
57
+ return (
58
+ <>
59
+ <aside aria-label="Blocks">
60
+ {items.map((item) => (
61
+ <button
62
+ key={item.key}
63
+ onMouseDown={(event) => event.preventDefault()}
64
+ onClick={item.insert}
65
+ type="button"
66
+ >
67
+ {item.label}
68
+ </button>
69
+ ))}
70
+ </aside>
71
+ <OpenEditorContent controller={controller} />
72
+ </>
73
+ );
74
+ }
75
+ ```
76
+
77
+ `item.insert()` restores focus and inserts at the controller's current editor
78
+ selection. Preventing the button's mouse-down default keeps the caret visibly
79
+ anchored while the user moves from the editor to a host-owned picker.
80
+
37
81
  The built-in web catalog includes editable Toggle Lists, rich-content Callouts,
38
82
  and Mermaid-powered Diagrams. Diagrams use `beautiful-mermaid` for synchronous,
39
83
  themeable SVG rendering and retain their canonical Mermaid source in the document.
@@ -114,7 +158,7 @@ const productCard = defineOpenEditorReactNode({
114
158
  ],
115
159
  },
116
160
  component: ProductCardNode,
117
- slashMenu: {
161
+ insertMenu: {
118
162
  icon: PackageOpen,
119
163
  keywords: ["product", "commerce", "card"],
120
164
  order: 250,
@@ -143,13 +187,15 @@ Pass the same extension array to `OpenEditorViewer`. The controller automaticall
143
187
  uses registered exporters for `controller.exports` and registered blocks appear
144
188
  in `getDefaultSlashMenuItems`, which powers `@openeditor/ui`.
145
189
 
146
- Slash-menu entries accept an optional React `icon` component and numeric `order`.
190
+ Insert-menu entries accept an optional React `icon` component and numeric `order`.
147
191
  OpenEditor assigns built-in entries orders in increments of 100, so consumer
148
192
  blocks can be placed between them (for example, `order: 250` appears between
149
193
  Heading 1 and Heading 2). Entries with the same order retain their declaration
150
- order, and entries without an order appear after ordered entries. The same fields
151
- are available on direct `slashMenuItems`; a menu's explicit `items` prop is also
152
- sorted by this rule.
194
+ order, and entries without an order appear after ordered entries. This metadata
195
+ 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
198
+ `slashMenuItems`; a slash menu's explicit `items` prop is also sorted by this rule.
153
199
 
154
200
  Block `name` is the stable public identifier. Namespace consumer blocks to avoid
155
201
  collisions. `nodeType` is the serialized ProseMirror node type and defaults to
package/dist/index.d.ts CHANGED
@@ -161,6 +161,22 @@ type OpenEditorSlashMenuItem = {
161
161
  range: InsertRange;
162
162
  }) => boolean;
163
163
  };
164
+ /**
165
+ * An insertable block exposed to host-owned pickers and command palettes.
166
+ *
167
+ * Unlike `OpenEditorSlashMenuItem`, this contract contains only block insertion
168
+ * and is not coupled to a slash query range. Calling `insert` restores editor
169
+ * focus and inserts at the controller's current selection.
170
+ */
171
+ type OpenEditorBlockPickerItem = {
172
+ key: string;
173
+ label: string;
174
+ group: string;
175
+ icon?: OpenEditorSlashMenuIcon;
176
+ keywords?: readonly string[];
177
+ order?: number;
178
+ insert: () => boolean;
179
+ };
164
180
  type OpenEditorViewerRenderer = (context: {
165
181
  node: ProseMirrorNode;
166
182
  children: ReactNode;
@@ -180,6 +196,9 @@ type OpenEditorExtensionMenu = {
180
196
  type OpenEditorReactExtension = {
181
197
  block: BlockSpec;
182
198
  tiptapExtensions: readonly AnyExtension[];
199
+ /** Discovery metadata shared by slash menus and host-owned block pickers. */
200
+ insertMenu?: false | OpenEditorExtensionMenu;
201
+ /** @deprecated Use `insertMenu`. */
183
202
  slashMenu?: false | OpenEditorExtensionMenu;
184
203
  viewer: OpenEditorViewerRenderer;
185
204
  exporters?: OpenEditorExporters;
@@ -209,9 +228,11 @@ declare module "@tiptap/core" {
209
228
  declare const formatAttachmentSize: (size: number | null) => string;
210
229
  declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
211
230
  declare const sortSlashMenuItems: (items: readonly OpenEditorSlashMenuItem[]) => OpenEditorSlashMenuItem[];
231
+ declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]) => OpenEditorBlockPickerItem[];
232
+ declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
212
233
  declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
213
234
  declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
214
235
  declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
215
236
  declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
216
237
 
217
- export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, 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, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
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 };
package/dist/index.js CHANGED
@@ -32904,15 +32904,34 @@ var pageSnapshotFromNode = (node) => {
32904
32904
  href: typeof node.attrs?.href === "string" ? node.attrs.href : null
32905
32905
  };
32906
32906
  };
32907
+ var useResolvedPageSnapshot = (cached, runtime) => {
32908
+ const [resolved, setResolved] = useState(null);
32909
+ useEffect(() => {
32910
+ let active = true;
32911
+ setResolved(null);
32912
+ if (cached?.pageId && runtime?.resolvePage) {
32913
+ void runtime.resolvePage(cached.pageId).then((page) => {
32914
+ if (active && page) setResolved(page);
32915
+ }).catch(() => {
32916
+ if (active) setResolved(null);
32917
+ });
32918
+ }
32919
+ return () => {
32920
+ active = false;
32921
+ };
32922
+ }, [cached?.pageId, runtime]);
32923
+ return [resolved ?? cached, setResolved];
32924
+ };
32907
32925
  var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes2 }) => {
32908
32926
  const runtime = use(OpenEditorPageRuntimeContext);
32909
32927
  const themeStyle = useOpenEditorThemeStyle();
32910
32928
  const [creating, setCreating] = useState(false);
32911
32929
  const [error, setError] = useState(null);
32912
- const snapshot = pageSnapshotFromNode(node);
32913
- const icon = normalizeEmoji(node.attrs.icon, DEFAULT_PAGE_EMOJI);
32930
+ const cached = pageSnapshotFromNode(node);
32931
+ const [page, setResolvedPage] = useResolvedPageSnapshot(cached, runtime);
32932
+ const icon = normalizeEmoji(page?.icon, DEFAULT_PAGE_EMOJI);
32914
32933
  const openPage = () => {
32915
- if (snapshot) void runtime?.openPage?.(snapshot);
32934
+ if (page) void runtime?.openPage?.(page);
32916
32935
  };
32917
32936
  const createPage = async () => {
32918
32937
  if (!runtime?.createPage || creating) return;
@@ -32929,10 +32948,11 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
32929
32948
  };
32930
32949
  const updateIcon = async (nextIcon) => {
32931
32950
  updateAttributes2({ icon: nextIcon });
32932
- if (!snapshot?.pageId || !runtime?.updatePageIcon) return;
32951
+ if (!page?.pageId || !runtime?.updatePageIcon) return;
32952
+ setResolvedPage({ ...page, icon: nextIcon });
32933
32953
  setError(null);
32934
32954
  try {
32935
- await runtime.updatePageIcon(snapshot.pageId, nextIcon);
32955
+ await runtime.updatePageIcon(page.pageId, nextIcon);
32936
32956
  } catch (cause) {
32937
32957
  setError(cause instanceof Error ? cause.message : "Could not update the page icon.");
32938
32958
  }
@@ -32940,22 +32960,22 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
32940
32960
  return /* @__PURE__ */ jsxs(
32941
32961
  NodeViewWrapper,
32942
32962
  {
32943
- "aria-label": snapshot ? `Open ${snapshot.title}` : void 0,
32963
+ "aria-label": page ? `Open ${page.title}` : void 0,
32944
32964
  className: "oe-page",
32945
- "data-page-id": snapshot?.pageId ?? "",
32965
+ "data-page-id": page?.pageId ?? "",
32946
32966
  onClick: (event) => {
32947
32967
  if (event.target.closest(".oe-emoji-trigger, .oe-emoji-popover")) return;
32948
32968
  openPage();
32949
32969
  },
32950
32970
  onKeyDown: (event) => {
32951
32971
  if (event.target.closest(".oe-emoji-trigger, .oe-emoji-popover")) return;
32952
- if (snapshot && (event.key === "Enter" || event.key === " ")) {
32972
+ if (page && (event.key === "Enter" || event.key === " ")) {
32953
32973
  event.preventDefault();
32954
32974
  openPage();
32955
32975
  }
32956
32976
  },
32957
- role: snapshot ? "button" : void 0,
32958
- tabIndex: snapshot ? 0 : void 0,
32977
+ role: page ? "button" : void 0,
32978
+ tabIndex: page ? 0 : void 0,
32959
32979
  children: [
32960
32980
  /* @__PURE__ */ jsx("span", { contentEditable: false, children: /* @__PURE__ */ jsx(
32961
32981
  OpenEditorEmojiPicker,
@@ -32968,7 +32988,7 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
32968
32988
  onEmojiSelect: (nextIcon) => void updateIcon(nextIcon)
32969
32989
  }
32970
32990
  ) }),
32971
- /* @__PURE__ */ jsx(
32991
+ page && page !== cached ? /* @__PURE__ */ jsx("div", { className: "oe-page-title", contentEditable: false, children: page.title }) : /* @__PURE__ */ jsx(
32972
32992
  NodeViewContent,
32973
32993
  {
32974
32994
  as: "div",
@@ -32976,7 +32996,7 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
32976
32996
  contentEditable: false
32977
32997
  }
32978
32998
  ),
32979
- !snapshot && runtime?.createPage && editor.isEditable ? /* @__PURE__ */ jsx(
32999
+ !page && runtime?.createPage && editor.isEditable ? /* @__PURE__ */ jsx(
32980
33000
  "button",
32981
33001
  {
32982
33002
  className: "oe-page-action",
@@ -33015,20 +33035,7 @@ var Page = Node3.create({
33015
33035
  var OpenEditorPageViewer = ({ node }) => {
33016
33036
  const runtime = use(OpenEditorPageRuntimeContext);
33017
33037
  const cached = pageSnapshotFromNode(node);
33018
- const [resolved, setResolved] = useState(null);
33019
- useEffect(() => {
33020
- let active = true;
33021
- setResolved(null);
33022
- if (cached?.pageId && runtime?.resolvePage) {
33023
- void runtime.resolvePage(cached.pageId).then((page2) => {
33024
- if (active && page2) setResolved(page2);
33025
- });
33026
- }
33027
- return () => {
33028
- active = false;
33029
- };
33030
- }, [cached?.pageId, runtime]);
33031
- const page = resolved ?? cached;
33038
+ const [page] = useResolvedPageSnapshot(cached, runtime);
33032
33039
  const title = page?.title ?? (getNodeText(node) || "Untitled");
33033
33040
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
33034
33041
  /* @__PURE__ */ jsx("span", { className: "oe-page-icon", "aria-hidden": "true", children: normalizeEmoji(page?.icon, DEFAULT_PAGE_EMOJI) }),
@@ -33651,12 +33658,37 @@ var useOpenEditorController = ({
33651
33658
  };
33652
33659
  return controller;
33653
33660
  };
33654
- var sortSlashMenuItems = (items) => [...items].sort((left, right) => {
33661
+ var sortMenuItems = (items) => [...items].sort((left, right) => {
33655
33662
  const leftOrder = typeof left.order === "number" && Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
33656
33663
  const rightOrder = typeof right.order === "number" && Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
33657
33664
  if (leftOrder === rightOrder) return 0;
33658
33665
  return leftOrder - rightOrder;
33659
33666
  });
33667
+ var sortSlashMenuItems = (items) => sortMenuItems(items);
33668
+ var sortBlockPickerItems = (items) => sortMenuItems(items);
33669
+ var getExtensionInsertMenu = (extension) => extension.insertMenu ?? extension.slashMenu;
33670
+ var getDefaultBlockPickerItems = (controller) => sortBlockPickerItems([
33671
+ ...openEditorInsertPresets.map((preset, index) => ({
33672
+ key: preset.key,
33673
+ label: preset.label,
33674
+ group: preset.group,
33675
+ keywords: [preset.key],
33676
+ order: (index + 1) * 100,
33677
+ insert: () => controller.insertBlock(preset.key)
33678
+ })),
33679
+ ...controller.extensions.filter((extension) => getExtensionInsertMenu(extension) !== false).map((extension) => {
33680
+ const menu = getExtensionInsertMenu(extension) || void 0;
33681
+ return {
33682
+ key: extension.block.name,
33683
+ label: menu?.label ?? extension.block.label,
33684
+ group: menu?.group ?? extension.block.group,
33685
+ icon: menu?.icon,
33686
+ keywords: menu?.keywords ?? [extension.block.name],
33687
+ order: menu?.order,
33688
+ insert: () => controller.insertBlock(extension.block.name)
33689
+ };
33690
+ })
33691
+ ]);
33660
33692
  var getDefaultSlashMenuItems = (controller) => {
33661
33693
  const internalController = controller;
33662
33694
  return sortSlashMenuItems([
@@ -33692,8 +33724,8 @@ var getDefaultSlashMenuItems = (controller) => {
33692
33724
  return true;
33693
33725
  }
33694
33726
  },
33695
- ...controller.extensions.filter((extension) => extension.slashMenu !== false).map((extension) => {
33696
- const menu = extension.slashMenu || void 0;
33727
+ ...controller.extensions.filter((extension) => getExtensionInsertMenu(extension) !== false).map((extension) => {
33728
+ const menu = getExtensionInsertMenu(extension) || void 0;
33697
33729
  return {
33698
33730
  key: extension.block.name,
33699
33731
  label: menu?.label ?? extension.block.label,
@@ -33847,6 +33879,6 @@ var OpenEditorViewer = ({
33847
33879
  };
33848
33880
  var useOpenEditor = useOpenEditorController;
33849
33881
 
33850
- export { NodeViewContent, NodeViewWrapper, OpenEditorContent, DragHandle2 as OpenEditorDragHandle, OpenEditorThemeProvider, OpenEditorViewer, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
33882
+ export { NodeViewContent, NodeViewWrapper, OpenEditorContent, DragHandle2 as OpenEditorDragHandle, OpenEditorThemeProvider, OpenEditorViewer, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
33851
33883
  //# sourceMappingURL=index.js.map
33852
33884
  //# sourceMappingURL=index.js.map