@openeditor/native 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
@@ -7,6 +7,7 @@ Pass `attachmentRuntime` to use the system picker and host storage lifecycle. Th
7
7
  ## Public surface
8
8
 
9
9
  - `OpenEditorNative` as the primary editing surface
10
+ - `OpenEditorNativePageHeader` for host-backed page title and icon editing
10
11
  - `OpenEditorNativeViewer` for read-only rendering
11
12
  - `OpenEditorNativeEmojiPicker` for standalone native emoji selection
12
13
  - native document bridge helpers where renderer adaptation is required
@@ -25,11 +26,15 @@ history. Inline emoji continue to use the device keyboard.
25
26
  ## Quick start
26
27
 
27
28
  ```tsx
28
- import { OpenEditorNative } from "@openeditor/native";
29
+ import { useRef } from "react";
30
+ import { OpenEditorNative, type OpenEditorNativeController } from "@openeditor/native";
29
31
 
30
32
  export function Example() {
33
+ const editor = useRef<OpenEditorNativeController>(null);
31
34
  return (
32
35
  <OpenEditorNative
36
+ ref={editor}
37
+ initialDocument={document}
33
38
  onChange={(document) => {
34
39
  console.log(document);
35
40
  }}
@@ -38,6 +43,15 @@ export function Example() {
38
43
  }
39
44
  ```
40
45
 
46
+ `OpenEditorNative` follows the same uncontrolled lifecycle as web:
47
+ `initialDocument` is read once and `ref.current.setContent(document)` performs an
48
+ undoable programmatic replacement. Undo and redo delegate directly to the Rust
49
+ editor engine; the React wrapper does not keep document snapshots.
50
+
51
+ Pass `enabledBlocks` to restrict authoring. Page and attachment controls are
52
+ automatically omitted until their required host runtimes are present, while
53
+ existing nodes remain readable.
54
+
41
55
  ## Standalone emoji picker
42
56
 
43
57
  The editor wires this automatically for callouts and pages. Hosts can also use
package/dist/index.d.ts CHANGED
@@ -1,29 +1,33 @@
1
- import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, type ProseMirrorNode } from "@openeditor/core";
1
+ import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, type OpenEditorAuthoringCapabilities, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type ProseMirrorNode } from "@openeditor/core";
2
2
  import { type ReactNode } from "react";
3
3
  import type { NativeRichTextEditorToolbarPlacement } from "@openeditor/react-native-prose-editor";
4
4
  export { normalizeNativeThemeColor } from "./theme.js";
5
5
  export { OpenEditorNativeEmojiPicker, type OpenEditorNativeEmojiPickerProps, type OpenEditorNativeEmojiPickerTheme, } from "./emoji-picker.js";
6
6
  export { emojiForNativeSkinTone, getOpenEditorNativeEmojiSections, openEditorNativeEmojiSkinToneOptions, type OpenEditorNativeEmoji, type OpenEditorNativeEmojiSection, type OpenEditorNativeEmojiSkinTone, } from "./emoji-data.js";
7
- export type OpenEditorNativeProps = {
8
- document?: OpenEditorDocument;
7
+ export type OpenEditorNativeProps = OpenEditorAuthoringCapabilities & {
8
+ initialDocument?: OpenEditorDocument;
9
9
  editable?: boolean;
10
10
  placeholder?: string;
11
11
  showToolbar?: boolean;
12
12
  theme?: Partial<OpenEditorNativeTheme>;
13
13
  toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
14
14
  onChange?: (document: OpenEditorDocument) => void;
15
- onOpenPage?: (page: {
16
- pageId: string;
17
- title: string;
18
- icon?: string | null;
19
- href?: string | null;
20
- }) => void;
15
+ pageRuntime?: OpenEditorPageRuntime;
21
16
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
22
17
  };
23
- export type OpenEditorNativeViewerProps = Omit<OpenEditorNativeProps, "editable" | "showToolbar" | "onChange"> & {
24
- showToolbar?: false;
18
+ export type OpenEditorNativeViewerProps = {
19
+ document: OpenEditorDocument;
20
+ theme?: Partial<OpenEditorNativeTheme>;
21
+ pageRuntime?: OpenEditorPageRuntime;
22
+ attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
25
23
  renderers?: Partial<Record<string, OpenEditorNativeViewerRenderer>>;
26
24
  };
25
+ export type OpenEditorNativeController = {
26
+ getContent: () => OpenEditorDocument;
27
+ setContent: (document: OpenEditorDocument) => void;
28
+ undo: () => void;
29
+ redo: () => void;
30
+ };
27
31
  export type OpenEditorNativeToolbarAction = {
28
32
  key: string;
29
33
  label: string;
@@ -72,8 +76,26 @@ export type OpenEditorNativeTheme = {
72
76
  toolbar?: OpenEditorNativeToolbarTheme;
73
77
  };
74
78
  export declare const OpenEditorNativeToolbar: ({ actions, onActionPress, theme, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element;
75
- export declare const OpenEditorNative: ({ document, editable, placeholder, showToolbar, theme, toolbarPlacement, onChange, onOpenPage, attachmentRuntime, }: OpenEditorNativeProps) => import("react").JSX.Element;
76
- export declare const OpenEditorNativeViewer: ({ document, theme, renderers, onOpenPage, attachmentRuntime, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
79
+ export type OpenEditorNativePageHeaderProps = {
80
+ page: OpenEditorPageSnapshot;
81
+ runtime: OpenEditorPageRuntime;
82
+ theme?: Partial<OpenEditorNativeTheme>;
83
+ onPageChange?: (page: OpenEditorPageSnapshot) => void;
84
+ };
85
+ /** Canonical native page-title and page-icon editor for opened page surfaces. */
86
+ export declare const OpenEditorNativePageHeader: ({ page, runtime, theme, onPageChange }: OpenEditorNativePageHeaderProps) => import("react").JSX.Element;
87
+ export declare const OpenEditorNative: import("react").ForwardRefExoticComponent<OpenEditorAuthoringCapabilities & {
88
+ initialDocument?: OpenEditorDocument;
89
+ editable?: boolean;
90
+ placeholder?: string;
91
+ showToolbar?: boolean;
92
+ theme?: Partial<OpenEditorNativeTheme>;
93
+ toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
94
+ onChange?: (document: OpenEditorDocument) => void;
95
+ pageRuntime?: OpenEditorPageRuntime;
96
+ attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
97
+ } & import("react").RefAttributes<OpenEditorNativeController>>;
98
+ export declare const OpenEditorNativeViewer: ({ document, theme, renderers, pageRuntime, attachmentRuntime, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
77
99
  export { createNativeEditorDocument, fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
78
100
  export { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, nativeToolbarParityCoverage, openEditorNativeToolbarActionKeys, openEditorNativeToolbarItems, } from "./toolbar.js";
79
101
  export type { OpenEditorNativeToolbarActionKey } from "./toolbar.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { DEFAULT_CALLOUT_EMOJI, DEFAULT_PAGE_EMOJI, createDocument, findBlockSpecForNode, moveTopLevelBlock, normalizeEmoji, } from "@openeditor/core";
3
- import { useMemo, useRef, useState } from "react";
2
+ import { DEFAULT_CALLOUT_EMOJI, DEFAULT_PAGE_EMOJI, createDocument, findBlockSpecForNode, isOpenEditorBlockEnabled, moveTopLevelBlock, normalizeEmoji, } from "@openeditor/core";
3
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
4
4
  import { Image as NativeImage, Keyboard, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
5
5
  import { fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
6
6
  import { resolveSelectedTopLevelIndex } from "./selection.js";
@@ -332,11 +332,29 @@ const NativeToggleListItem = ({ children, initiallyOpen, theme, }) => {
332
332
  open ? styles.viewerToggleCaretOpen : null,
333
333
  ] }) }), _jsx(View, { style: styles.viewerListContent, children: open ? childArray : childArray.slice(0, 1) })] }));
334
334
  };
335
- const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachmentRuntime) => {
335
+ const OpenEditorNativePageViewer = ({ cachedPage, pageRuntime, theme, }) => {
336
+ const [page, setPage] = useState(cachedPage);
337
+ useEffect(() => {
338
+ setPage(cachedPage);
339
+ if (!pageRuntime?.resolvePage)
340
+ return;
341
+ let active = true;
342
+ void pageRuntime.resolvePage(cachedPage.pageId).then((resolved) => {
343
+ if (active && resolved)
344
+ setPage(resolved);
345
+ }).catch(() => undefined);
346
+ return () => {
347
+ active = false;
348
+ };
349
+ }, [cachedPage.href, cachedPage.icon, cachedPage.pageId, cachedPage.title, pageRuntime]);
350
+ const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: [styles.viewerPageIcon, { color: theme.text }], children: normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI) }), _jsx(Text, { style: [styles.viewerPageTitle, { color: theme.text, textDecorationColor: theme.muted }], children: page.title }), _jsx(Text, { style: { color: theme.muted }, children: "\u2192" })] });
351
+ return pageRuntime?.openPage ? (_jsx(Pressable, { accessibilityLabel: `Open ${page.title}`, accessibilityRole: "button", onPress: () => void pageRuntime.openPage?.(page), style: { alignItems: "center", borderRadius: 8, flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content })) : (_jsx(View, { style: { alignItems: "center", flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }));
352
+ };
353
+ const renderNativeViewerNode = (node, key, theme, renderers, pageRuntime, attachmentRuntime) => {
336
354
  if (node.type === "text") {
337
355
  return _jsx(Text, { style: { color: theme.text }, children: node.text ?? "" }, key);
338
356
  }
339
- const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, onOpenPage, attachmentRuntime)) ?? null;
357
+ const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, pageRuntime, attachmentRuntime)) ?? null;
340
358
  const blockKey = findBlockSpecForNode(defaultBlockRegistry, node)?.name ?? node.type;
341
359
  const customRenderer = renderers?.[blockKey];
342
360
  if (customRenderer) {
@@ -375,8 +393,7 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachm
375
393
  const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
376
394
  const title = node.content?.map((child) => child.text ?? "").join("") || "Untitled";
377
395
  const icon = normalizeEmoji(node.attrs?.icon, DEFAULT_PAGE_EMOJI);
378
- const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: [styles.viewerPageIcon, { color: theme.text }], children: icon }), _jsx(Text, { style: [styles.viewerPageTitle, { color: theme.text, textDecorationColor: theme.muted }], children: title }), _jsx(Text, { style: { color: theme.muted }, children: "\u2192" })] });
379
- return onOpenPage && pageId ? (_jsx(Pressable, { accessibilityRole: "button", onPress: () => onOpenPage({ pageId, title, icon, href: typeof node.attrs?.href === "string" ? node.attrs.href : null }), style: { alignItems: "center", borderRadius: 8, flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }, key)) : _jsx(View, { style: { alignItems: "center", flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }, key);
396
+ return _jsx(OpenEditorNativePageViewer, { cachedPage: { pageId, title, icon, href: typeof node.attrs?.href === "string" ? node.attrs.href : null }, pageRuntime: pageId ? pageRuntime : undefined, theme: theme }, key);
380
397
  }
381
398
  case "attachment": {
382
399
  const snapshot = {
@@ -434,9 +451,45 @@ export const OpenEditorNativeToolbar = ({ actions = [
434
451
  onActionPress?.(action.key);
435
452
  }, style: [styles.toolbarButton, { backgroundColor: resolvedTheme.accent, borderColor: resolvedTheme.borderStrong }], children: _jsx(Text, { style: [styles.toolbarButtonText, { color: resolvedTheme.accentText }], children: action.label }) }, action.key))) }));
436
453
  };
437
- export const OpenEditorNative = ({ document = createDocument(), editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, attachmentRuntime, }) => {
454
+ /** Canonical native page-title and page-icon editor for opened page surfaces. */
455
+ export const OpenEditorNativePageHeader = ({ page, runtime, theme, onPageChange }) => {
456
+ const [title, setTitle] = useState(page.title);
457
+ const [icon, setIcon] = useState(normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI));
458
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
459
+ const [error, setError] = useState(null);
460
+ const resolvedTheme = { ...defaultNativeTheme, ...theme };
461
+ useEffect(() => setTitle(page.title), [page.pageId, page.title]);
462
+ useEffect(() => setIcon(normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI)), [page.icon, page.pageId]);
463
+ const updatePage = async (update) => {
464
+ setError(null);
465
+ try {
466
+ const resolved = await runtime.updatePage(page.pageId, update);
467
+ const next = resolved ?? { ...page, ...update };
468
+ setTitle(next.title);
469
+ setIcon(normalizeEmoji(next.icon, DEFAULT_PAGE_EMOJI));
470
+ onPageChange?.(next);
471
+ }
472
+ catch (cause) {
473
+ setError(cause instanceof Error ? cause.message : "Could not update the page.");
474
+ }
475
+ };
476
+ return _jsxs(View, { style: styles.pageHeader, children: [_jsx(Pressable, { accessibilityLabel: "Change page icon", accessibilityRole: "button", onPress: () => setShowEmojiPicker(true), children: _jsx(Text, { style: styles.pageHeaderIcon, children: icon }) }), _jsx(TextInput, { accessibilityLabel: "Page title", multiline: true, onBlur: () => {
477
+ const nextTitle = title.trim() || "Untitled";
478
+ setTitle(nextTitle);
479
+ if (nextTitle !== page.title)
480
+ void updatePage({ title: nextTitle });
481
+ }, onChangeText: setTitle, placeholder: "Untitled", placeholderTextColor: resolvedTheme.muted, style: [styles.pageHeaderTitle, { color: resolvedTheme.text }], value: title }), error ? _jsx(Text, { accessibilityRole: "alert", style: styles.pageHeaderError, children: error }) : null, _jsx(OpenEditorNativeEmojiPicker, { onEmojiSelect: (nextIcon) => {
482
+ setIcon(nextIcon);
483
+ void updatePage({ icon: nextIcon });
484
+ }, onRequestClose: () => setShowEmojiPicker(false), theme: { surfaceMuted: resolvedTheme.surfaceMuted, text: resolvedTheme.text, muted: resolvedTheme.muted }, visible: showEmojiPicker })] });
485
+ };
486
+ export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDocument = createDocument(), editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, pageRuntime, attachmentRuntime, enabledBlocks, }, forwardedRef) {
438
487
  const editorRef = useRef(null);
439
- const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(document));
488
+ const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(initialDocument));
489
+ const currentDocumentRef = useRef(currentDocument);
490
+ currentDocumentRef.current = currentDocument;
491
+ const onChangeRef = useRef(onChange);
492
+ onChangeRef.current = onChange;
440
493
  const [selection, setSelection] = useState();
441
494
  const [linkRequest, setLinkRequest] = useState(null);
442
495
  const [linkUrl, setLinkUrl] = useState("https://");
@@ -445,10 +498,7 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
445
498
  const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
446
499
  const [attachmentUpload, setAttachmentUpload] = useState(null);
447
500
  const attachmentAbortRef = useRef(null);
448
- const [history, setHistory] = useState({
449
- undoStack: [],
450
- redoStack: [],
451
- });
501
+ const [history, setHistory] = useState({ canUndo: false, canRedo: false });
452
502
  const nativeDocument = currentDocument;
453
503
  const NativeEditor = useMemo(() => loadNativeEditor(), []);
454
504
  const resolvedTheme = useMemo(() => ({
@@ -460,24 +510,35 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
460
510
  },
461
511
  }), [theme]);
462
512
  const nativeEditorTheme = useMemo(() => toNativeEditorTheme(resolvedTheme), [resolvedTheme]);
513
+ const authorableBlocks = useMemo(() => [...defaultBlockRegistry.values()]
514
+ .filter((block) => block.support?.native !== "unsupported")
515
+ .map((block) => block.name)
516
+ .filter((blockName) => isOpenEditorBlockEnabled(blockName, { enabledBlocks }))
517
+ .filter((blockName) => blockName !== "page" || Boolean(pageRuntime?.createPage))
518
+ .filter((blockName) => blockName !== "attachment" || Boolean(attachmentRuntime?.selectAttachment && attachmentRuntime.uploadAttachment)), [attachmentRuntime, enabledBlocks, pageRuntime]);
519
+ const isBlockEnabled = (blockName) => authorableBlocks.includes(/^heading[1-6]$/.test(blockName) ? "heading" : blockName);
463
520
  const toolbarItems = useMemo(() => createOpenEditorNativeToolbarItems({
464
- canUndo: history.undoStack.length > 0,
465
- canRedo: history.redoStack.length > 0,
466
- }), [history.redoStack.length, history.undoStack.length]);
521
+ canUndo: history.canUndo,
522
+ canRedo: history.canRedo,
523
+ enabledBlocks: authorableBlocks,
524
+ }), [authorableBlocks, history.canRedo, history.canUndo]);
467
525
  const syncDocument = (nextDocument, updateEditor = true) => {
468
526
  const normalized = normalizeForNativeEditor(nextDocument);
469
- if (!documentsMatch(normalized, nativeDocument)) {
470
- setHistory((currentHistory) => ({
471
- undoStack: [...currentHistory.undoStack.slice(-99), nativeDocument],
472
- redoStack: [],
473
- }));
474
- }
527
+ if (documentsMatch(normalized, currentDocumentRef.current))
528
+ return;
529
+ currentDocumentRef.current = normalized;
475
530
  setCurrentDocument(normalized);
476
- onChange?.(normalized);
531
+ onChangeRef.current?.(normalized);
477
532
  if (updateEditor) {
478
533
  editorRef.current?.setContentJson(toNativeEditorDocument(normalized));
479
534
  }
480
535
  };
536
+ useImperativeHandle(forwardedRef, () => ({
537
+ getContent: () => currentDocumentRef.current,
538
+ setContent: (nextDocument) => syncDocument(nextDocument, true),
539
+ undo: () => editorRef.current?.undo(),
540
+ redo: () => editorRef.current?.redo(),
541
+ }));
481
542
  const moveSelectedBlock = (direction) => {
482
543
  const selectedIndex = resolveSelectedTopLevelIndex(nativeDocument, selection);
483
544
  const nextIndex = selectedIndex + direction;
@@ -529,31 +590,11 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
529
590
  return;
530
591
  }
531
592
  if (key === openEditorNativeToolbarActionKeys.undo) {
532
- const previous = history.undoStack[history.undoStack.length - 1];
533
- if (!previous) {
534
- return;
535
- }
536
- setHistory((currentHistory) => ({
537
- undoStack: currentHistory.undoStack.slice(0, -1),
538
- redoStack: [nativeDocument, ...currentHistory.redoStack].slice(0, 100),
539
- }));
540
- setCurrentDocument(previous);
541
- onChange?.(previous);
542
- editorRef.current?.setContentJson(toNativeEditorDocument(previous));
593
+ editorRef.current?.undo();
543
594
  return;
544
595
  }
545
596
  if (key === openEditorNativeToolbarActionKeys.redo) {
546
- const next = history.redoStack[0];
547
- if (!next) {
548
- return;
549
- }
550
- setHistory((currentHistory) => ({
551
- undoStack: [...currentHistory.undoStack.slice(-99), nativeDocument],
552
- redoStack: currentHistory.redoStack.slice(1),
553
- }));
554
- setCurrentDocument(next);
555
- onChange?.(next);
556
- editorRef.current?.setContentJson(toNativeEditorDocument(next));
597
+ editorRef.current?.redo();
557
598
  return;
558
599
  }
559
600
  if (key === openEditorNativeToolbarActionKeys.bulletList) {
@@ -572,6 +613,21 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
572
613
  editorRef.current?.insertTable();
573
614
  return;
574
615
  }
616
+ if (!isBlockEnabled(key))
617
+ return;
618
+ if (key === openEditorNativeToolbarActionKeys.page && pageRuntime?.createPage) {
619
+ void pageRuntime.createPage({ title: "Untitled", icon: DEFAULT_PAGE_EMOJI }).then((page) => {
620
+ editorRef.current?.insertContentJson({
621
+ type: "doc",
622
+ content: [{
623
+ type: "page",
624
+ attrs: { pageId: page.pageId, icon: page.icon ?? DEFAULT_PAGE_EMOJI, href: page.href ?? null },
625
+ content: [{ type: "text", text: page.title || "Untitled" }],
626
+ }],
627
+ });
628
+ }).catch(() => undefined);
629
+ return;
630
+ }
575
631
  const actionDocument = defaultDocumentForToolbarAction(key);
576
632
  if (actionDocument) {
577
633
  editorRef.current?.insertContentJson(actionDocument);
@@ -606,7 +662,21 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
606
662
  }, onRequestImage: (request) => {
607
663
  setImageUrl("https://placehold.co/960x420/png");
608
664
  setImageRequest(request);
609
- }, onOpenPage: onOpenPage, onRequestEmoji: setEmojiRequest, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
665
+ }, onOpenPage: (page) => {
666
+ if (!pageRuntime?.openPage)
667
+ return;
668
+ if (!pageRuntime.resolvePage) {
669
+ void pageRuntime.openPage(page);
670
+ return;
671
+ }
672
+ void pageRuntime.resolvePage(page.pageId)
673
+ .then((resolved) => pageRuntime.openPage?.(resolved ?? page))
674
+ .catch(() => pageRuntime.openPage?.(page));
675
+ }, onRequestEmoji: (request) => {
676
+ if (request.nodeType === "page" && !pageRuntime)
677
+ return;
678
+ setEmojiRequest(request);
679
+ }, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onHistoryStateChange: setHistory, onContentChangeJSON: (json) => {
610
680
  syncDocument(contentFromNativeJson(json), false);
611
681
  }, style: styles.nativeEditor }), _jsx(UrlDialog, { title: "Link", value: linkUrl, visible: linkRequest !== null, placeholder: "https://", confirmLabel: linkUrl.trim() ? "Apply" : "Remove", onChangeText: setLinkUrl, onCancel: () => setLinkRequest(null), onSubmit: submitLinkDialog, theme: resolvedTheme }), _jsx(Modal, { animationType: "fade", onRequestClose: () => { attachmentAbortRef.current?.abort(); setAttachmentUpload(null); }, transparent: true, visible: attachmentUpload !== null, children: _jsx(View, { style: [styles.dialogBackdrop, { backgroundColor: resolvedTheme.backdrop }], children: _jsxs(View, { accessibilityViewIsModal: true, style: [styles.dialog, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: [_jsx(Text, { style: [styles.unavailableTitle, { color: resolvedTheme.text }], children: attachmentUpload?.input.name }), _jsx(Text, { style: [styles.unavailableText, { color: attachmentUpload?.error ? "#b42318" : resolvedTheme.muted }], children: attachmentUpload?.error ?? `Uploading ${Math.round((attachmentUpload?.progress ?? 0) * 100)}%` }), _jsxs(View, { style: styles.dialogActions, children: [attachmentUpload?.error ? _jsx(CommandButton, { label: "Retry", onPress: () => void uploadAttachment(), theme: resolvedTheme }) : null, _jsx(CommandButton, { label: "Cancel", onPress: () => { attachmentAbortRef.current?.abort(); setAttachmentUpload(null); }, theme: resolvedTheme })] })] }) }) }), _jsx(UrlDialog, { title: "Image", value: imageUrl, visible: imageRequest !== null, placeholder: "https://", confirmLabel: "Insert", onChangeText: setImageUrl, onCancel: () => setImageRequest(null), onSubmit: submitImageDialog, theme: resolvedTheme }), _jsx(OpenEditorNativeEmojiPicker, { onEmojiSelect: (emoji) => {
612
682
  if (!emojiRequest)
@@ -616,14 +686,17 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
616
686
  ...emojiRequest.attrs,
617
687
  [attribute]: emoji,
618
688
  });
689
+ if (emojiRequest.nodeType === "page" && typeof emojiRequest.attrs.pageId === "string") {
690
+ void pageRuntime?.updatePage?.(emojiRequest.attrs.pageId, { icon: emoji });
691
+ }
619
692
  setEmojiRequest(null);
620
693
  }, onRequestClose: () => setEmojiRequest(null), theme: {
621
694
  surfaceMuted: resolvedTheme.surfaceMuted,
622
695
  text: resolvedTheme.text,
623
696
  muted: resolvedTheme.muted,
624
697
  }, visible: emojiRequest !== null })] }));
625
- };
626
- export const OpenEditorNativeViewer = ({ document = createDocument(), theme, renderers, onOpenPage, attachmentRuntime, }) => {
698
+ });
699
+ export const OpenEditorNativeViewer = ({ document = createDocument(), theme, renderers, pageRuntime, attachmentRuntime, }) => {
627
700
  const resolvedTheme = {
628
701
  ...defaultNativeTheme,
629
702
  ...theme,
@@ -633,7 +706,7 @@ export const OpenEditorNativeViewer = ({ document = createDocument(), theme, ren
633
706
  },
634
707
  };
635
708
  const normalizedDocument = normalizeForNativeEditor(document);
636
- return (_jsx(View, { style: [styles.viewer, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: normalizedDocument.content.map((node, index) => renderNativeViewerNode(node, `native-viewer-${index}`, resolvedTheme, renderers, onOpenPage, attachmentRuntime)) }));
709
+ return (_jsx(View, { style: [styles.viewer, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: normalizedDocument.content.map((node, index) => renderNativeViewerNode(node, `native-viewer-${index}`, resolvedTheme, renderers, pageRuntime, attachmentRuntime)) }));
637
710
  };
638
711
  const CommandButton = ({ label, active = false, primary = false, theme, onPress, }) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected: active }, onPress: onPress, style: [
639
712
  styles.commandButton,
@@ -780,6 +853,24 @@ const styles = StyleSheet.create({
780
853
  fontSize: 13,
781
854
  fontWeight: "700",
782
855
  },
856
+ pageHeader: {
857
+ gap: 8,
858
+ paddingBottom: 16,
859
+ },
860
+ pageHeaderIcon: {
861
+ fontSize: 40,
862
+ lineHeight: 48,
863
+ },
864
+ pageHeaderTitle: {
865
+ fontSize: 30,
866
+ fontWeight: "700",
867
+ lineHeight: 36,
868
+ padding: 0,
869
+ },
870
+ pageHeaderError: {
871
+ color: "#dc2626",
872
+ fontSize: 12,
873
+ },
783
874
  dialogBackdrop: {
784
875
  alignItems: "center",
785
876
  backgroundColor: "rgba(0, 0, 0, 0.32)",
package/dist/toolbar.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ProseMirrorDocument } from "@openeditor/core";
1
+ import { type ProseMirrorDocument } from "@openeditor/core";
2
2
  import type { EditorToolbarItem } from "@openeditor/react-native-prose-editor";
3
3
  export declare const openEditorNativeToolbarActionKeys: {
4
4
  readonly paragraph: "paragraph";
@@ -17,9 +17,10 @@ export declare const openEditorNativeToolbarActionKeys: {
17
17
  readonly dismissKeyboard: "openeditor:keyboard:dismiss";
18
18
  };
19
19
  export type OpenEditorNativeToolbarActionKey = (typeof openEditorNativeToolbarActionKeys)[keyof typeof openEditorNativeToolbarActionKeys];
20
- export declare const createOpenEditorNativeToolbarItems: ({ canUndo, canRedo, }?: {
20
+ export declare const createOpenEditorNativeToolbarItems: ({ canUndo, canRedo, enabledBlocks, }?: {
21
21
  canUndo?: boolean;
22
22
  canRedo?: boolean;
23
+ enabledBlocks?: readonly string[];
23
24
  }) => readonly EditorToolbarItem[];
24
25
  export declare const openEditorNativeToolbarItems: readonly EditorToolbarItem[];
25
26
  export declare const nativeToolbarParityCoverage: () => {
package/dist/toolbar.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isOpenEditorBlockEnabled } from "@openeditor/core";
1
2
  import { createOpenEditorInsertDocument, defaultBlockSpecs, defaultMarkSpecs, } from "@openeditor/extensions";
2
3
  import { toNativeEditorDocument } from "./document.js";
3
4
  const glyphIcon = (text) => ({ type: "glyph", text });
@@ -24,7 +25,7 @@ export const openEditorNativeToolbarActionKeys = {
24
25
  redo: "openeditor:history:redo",
25
26
  dismissKeyboard: "openeditor:keyboard:dismiss",
26
27
  };
27
- export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = true, } = {}) => {
28
+ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = true, enabledBlocks, } = {}) => {
28
29
  const items = [
29
30
  {
30
31
  type: "group",
@@ -149,7 +150,51 @@ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = t
149
150
  placement: "end",
150
151
  },
151
152
  ];
152
- return items;
153
+ const actionBlocks = new Map([
154
+ [openEditorNativeToolbarActionKeys.paragraph, "paragraph"],
155
+ [openEditorNativeToolbarActionKeys.bulletList, "bulletList"],
156
+ [openEditorNativeToolbarActionKeys.orderedList, "orderedList"],
157
+ [openEditorNativeToolbarActionKeys.columns, "columns"],
158
+ [openEditorNativeToolbarActionKeys.table, "table"],
159
+ [openEditorNativeToolbarActionKeys.toggleList, "toggleList"],
160
+ [openEditorNativeToolbarActionKeys.callout, "callout"],
161
+ [openEditorNativeToolbarActionKeys.page, "page"],
162
+ [openEditorNativeToolbarActionKeys.attachment, "attachment"],
163
+ ]);
164
+ const blockForItem = (item) => {
165
+ if (item.type === "action")
166
+ return actionBlocks.get(item.key);
167
+ if (item.type === "heading")
168
+ return "heading";
169
+ if (item.type === "blockquote")
170
+ return "blockquote";
171
+ if (item.type === "image")
172
+ return "image";
173
+ if (item.type === "node" && item.nodeType === "horizontalRule")
174
+ return "divider";
175
+ if (item.type === "command" && item.command.toLowerCase().includes("table"))
176
+ return "table";
177
+ return undefined;
178
+ };
179
+ const filterItems = (source) => {
180
+ const filtered = [];
181
+ for (const item of source) {
182
+ if (item.type === "group") {
183
+ const children = item.items.filter((child) => {
184
+ const block = blockForItem(child);
185
+ return !block || isOpenEditorBlockEnabled(block, { enabledBlocks });
186
+ });
187
+ if (children.length)
188
+ filtered.push({ ...item, items: children });
189
+ continue;
190
+ }
191
+ const block = blockForItem(item);
192
+ if (!block || isOpenEditorBlockEnabled(block, { enabledBlocks }))
193
+ filtered.push(item);
194
+ }
195
+ return filtered;
196
+ };
197
+ return filterItems(items);
153
198
  };
154
199
  export const openEditorNativeToolbarItems = createOpenEditorNativeToolbarItems();
155
200
  const walkToolbarItems = (items) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/native",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -28,16 +28,16 @@
28
28
  "dependencies": {
29
29
  "@expo/ui": "~56.0.18",
30
30
  "@expo/vector-icons": "15.0.3",
31
- "@openeditor/core": "0.0.27",
32
- "@openeditor/exporters": "0.0.27",
33
- "@openeditor/extensions": "0.0.27",
31
+ "@openeditor/core": "0.0.29",
32
+ "@openeditor/exporters": "0.0.29",
33
+ "@openeditor/extensions": "0.0.29",
34
34
  "emojibase": "17.0.0",
35
35
  "emojibase-data": "16.0.2",
36
36
  "react": "19.2.3",
37
37
  "react-native": "0.85.3"
38
38
  },
39
39
  "peerDependencies": {
40
- "@openeditor/react-native-prose-editor": "0.0.27",
40
+ "@openeditor/react-native-prose-editor": "0.0.29",
41
41
  "react-native-keyboard-controller": ">=1.21",
42
42
  "react": ">=19",
43
43
  "react-native": ">=0.85"