@openeditor/native 0.0.11 → 0.0.13

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Component-first React Native SDK for OpenEditor.
4
4
 
5
+ Pass `attachmentRuntime` to use the system picker and host storage lifecycle. The native toolbar exposes “Upload file,” native surfaces render attachment cards, and the viewer delegates opening/sharing to the runtime.
6
+
5
7
  ## Public surface
6
8
 
7
9
  - `OpenEditorNative` as the primary editing surface
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type OpenEditorDocument, type ProseMirrorNode } from "@openeditor/core";
1
+ import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, 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";
@@ -10,6 +10,13 @@ export type OpenEditorNativeProps = {
10
10
  theme?: Partial<OpenEditorNativeTheme>;
11
11
  toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
12
12
  onChange?: (document: OpenEditorDocument) => void;
13
+ onOpenPage?: (page: {
14
+ pageId: string;
15
+ title: string;
16
+ icon?: string | null;
17
+ href?: string | null;
18
+ }) => void;
19
+ attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
13
20
  };
14
21
  export type OpenEditorNativeViewerProps = Omit<OpenEditorNativeProps, "editable" | "showToolbar" | "onChange"> & {
15
22
  showToolbar?: false;
@@ -61,8 +68,8 @@ export type OpenEditorNativeTheme = {
61
68
  toolbar?: OpenEditorNativeToolbarTheme;
62
69
  };
63
70
  export declare const OpenEditorNativeToolbar: ({ actions, onActionPress, theme, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element;
64
- export declare const OpenEditorNative: ({ document, editable, placeholder, showToolbar, theme, toolbarPlacement, onChange, }: OpenEditorNativeProps) => import("react").JSX.Element;
65
- export declare const OpenEditorNativeViewer: ({ document, theme, renderers, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
71
+ export declare const OpenEditorNative: ({ document, editable, placeholder, showToolbar, theme, toolbarPlacement, onChange, onOpenPage, attachmentRuntime, }: OpenEditorNativeProps) => import("react").JSX.Element;
72
+ export declare const OpenEditorNativeViewer: ({ document, theme, renderers, onOpenPage, attachmentRuntime, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
66
73
  export { createNativeEditorDocument, fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
67
74
  export { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, nativeToolbarParityCoverage, openEditorNativeToolbarActionKeys, openEditorNativeToolbarItems, } from "./toolbar.js";
68
75
  export type { OpenEditorNativeToolbarActionKey } from "./toolbar.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createDocument, findBlockSpecForNode, moveTopLevelBlock, textBlock, } from "@openeditor/core";
3
3
  import { useMemo, useRef, useState } from "react";
4
4
  import { Image as NativeImage, Keyboard, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
@@ -74,6 +74,33 @@ const openEditorNativeSchema = {
74
74
  htmlTag: "aside",
75
75
  attrs: { tone: { default: "info" } },
76
76
  },
77
+ {
78
+ name: "page",
79
+ content: "inline*",
80
+ group: "block",
81
+ role: "textBlock",
82
+ htmlTag: "p",
83
+ attrs: {
84
+ pageId: { default: null },
85
+ icon: { default: "📄" },
86
+ href: { default: null },
87
+ },
88
+ },
89
+ {
90
+ name: "attachment",
91
+ content: "",
92
+ group: "block",
93
+ role: "block",
94
+ htmlTag: "div",
95
+ isVoid: true,
96
+ attrs: {
97
+ attachmentId: { default: null },
98
+ name: { default: "" },
99
+ mimeType: { default: null },
100
+ size: { default: null },
101
+ url: { default: null },
102
+ },
103
+ },
77
104
  { name: "codeBlock", content: "text*", group: "block", role: "textBlock", htmlTag: "pre" },
78
105
  { name: "horizontalRule", content: "", group: "block", role: "block", htmlTag: "hr", isVoid: true },
79
106
  {
@@ -307,11 +334,11 @@ const nativeSeedDocument = createDocument([
307
334
  { type: "horizontalRule" },
308
335
  { type: "image", attrs: { src: "https://placehold.co/960x420/png", alt: "Placeholder" } },
309
336
  ]);
310
- const renderNativeViewerNode = (node, key, theme, renderers) => {
337
+ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachmentRuntime) => {
311
338
  if (node.type === "text") {
312
339
  return _jsx(Text, { style: { color: theme.text }, children: node.text ?? "" }, key);
313
340
  }
314
- const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers)) ?? null;
341
+ const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, onOpenPage, attachmentRuntime)) ?? null;
315
342
  const blockKey = findBlockSpecForNode(defaultBlockRegistry, node)?.name ?? node.type;
316
343
  const customRenderer = renderers?.[blockKey];
317
344
  if (customRenderer) {
@@ -341,6 +368,24 @@ const renderNativeViewerNode = (node, key, theme, renderers) => {
341
368
  return _jsx(View, { style: { flexDirection: "row", gap: 12, marginBottom: 12 }, children: children }, key);
342
369
  case "table":
343
370
  return _jsx(View, { style: { borderColor: theme.border, borderRadius: 8, borderWidth: 1, marginBottom: 12, overflow: "hidden" }, children: children }, key);
371
+ case "page": {
372
+ const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
373
+ const title = node.content?.map((child) => child.text ?? "").join("") || "Untitled";
374
+ const icon = typeof node.attrs?.icon === "string" ? node.attrs.icon : "📄";
375
+ const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: { fontSize: 18 }, children: icon }), _jsx(Text, { style: { color: theme.text, flex: 1, fontWeight: "600", textDecorationLine: "underline" }, children: title }), _jsx(Text, { style: { color: theme.muted }, children: "\u2192" })] });
376
+ 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);
377
+ }
378
+ case "attachment": {
379
+ const snapshot = {
380
+ attachmentId: typeof node.attrs?.attachmentId === "string" ? node.attrs.attachmentId : null,
381
+ name: typeof node.attrs?.name === "string" ? node.attrs.name : "Attachment",
382
+ mimeType: typeof node.attrs?.mimeType === "string" ? node.attrs.mimeType : null,
383
+ size: typeof node.attrs?.size === "number" ? node.attrs.size : null,
384
+ url: typeof node.attrs?.url === "string" ? node.attrs.url : null,
385
+ };
386
+ const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: { fontSize: 22 }, children: "\uD83D\uDCCE" }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontWeight: "600" }, children: snapshot.name }), _jsx(Text, { style: { color: theme.muted, fontSize: 12 }, children: snapshot.mimeType ?? "File" })] }), _jsx(Text, { style: { color: theme.muted }, children: "Open" })] });
387
+ return attachmentRuntime?.openAttachment ? _jsx(Pressable, { accessibilityLabel: `Open ${snapshot.name}`, accessibilityRole: "button", onPress: () => void attachmentRuntime.openAttachment?.(snapshot), style: { alignItems: "center", borderColor: theme.border, borderRadius: 10, borderWidth: 1, flexDirection: "row", gap: 10, marginBottom: 8, minHeight: 64, padding: 10 }, children: content }, key) : _jsx(View, { style: { alignItems: "center", borderColor: theme.border, borderRadius: 10, borderWidth: 1, flexDirection: "row", gap: 10, marginBottom: 8, minHeight: 64, padding: 10 }, children: content }, key);
388
+ }
344
389
  default:
345
390
  if (node.type === "column") {
346
391
  return _jsx(View, { style: { flex: 1 }, children: children }, key);
@@ -377,7 +422,7 @@ export const OpenEditorNativeToolbar = ({ actions = [
377
422
  onActionPress?.(action.key);
378
423
  }, style: [styles.toolbarButton, { backgroundColor: resolvedTheme.accent, borderColor: resolvedTheme.borderStrong }], children: _jsx(Text, { style: [styles.toolbarButtonText, { color: resolvedTheme.accentText }], children: action.label }) }, action.key))) }));
379
424
  };
380
- export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, }) => {
425
+ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, attachmentRuntime, }) => {
381
426
  const editorRef = useRef(null);
382
427
  const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(document));
383
428
  const [selection, setSelection] = useState();
@@ -385,6 +430,8 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
385
430
  const [linkUrl, setLinkUrl] = useState("https://");
386
431
  const [imageRequest, setImageRequest] = useState(null);
387
432
  const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
433
+ const [attachmentUpload, setAttachmentUpload] = useState(null);
434
+ const attachmentAbortRef = useRef(null);
388
435
  const [history, setHistory] = useState({
389
436
  undoStack: [],
390
437
  redoStack: [],
@@ -426,6 +473,31 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
426
473
  }
427
474
  syncDocument(moveTopLevelBlock(nativeDocument, selectedIndex, nextIndex), true);
428
475
  };
476
+ const uploadAttachment = async () => {
477
+ if (!attachmentRuntime?.selectAttachment || !attachmentRuntime.uploadAttachment)
478
+ return;
479
+ attachmentAbortRef.current?.abort();
480
+ const controller = new AbortController();
481
+ attachmentAbortRef.current = controller;
482
+ try {
483
+ const input = await attachmentRuntime.selectAttachment({ signal: controller.signal });
484
+ if (!input || controller.signal.aborted)
485
+ return;
486
+ setAttachmentUpload({ input, progress: 0, error: null });
487
+ const validation = await attachmentRuntime.validateAttachment?.(input);
488
+ if (validation && !validation.accepted)
489
+ throw new Error(validation.message);
490
+ const snapshot = await attachmentRuntime.uploadAttachment(input, { signal: controller.signal, onProgress: (progress) => setAttachmentUpload((current) => current ? { ...current, progress } : current) });
491
+ if (controller.signal.aborted)
492
+ return;
493
+ editorRef.current?.insertContentJson({ type: "doc", content: [{ type: "attachment", attrs: snapshot }] });
494
+ setAttachmentUpload(null);
495
+ }
496
+ catch (cause) {
497
+ if (!controller.signal.aborted)
498
+ setAttachmentUpload((current) => current ? { ...current, error: cause instanceof Error ? cause.message : "Upload failed." } : current);
499
+ }
500
+ };
429
501
  const handleToolbarAction = (key) => {
430
502
  if (key === openEditorNativeToolbarActionKeys.dismissKeyboard) {
431
503
  dismissKeyboard();
@@ -439,6 +511,10 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
439
511
  moveSelectedBlock(1);
440
512
  return;
441
513
  }
514
+ if (key === openEditorNativeToolbarActionKeys.attachment) {
515
+ void uploadAttachment();
516
+ return;
517
+ }
442
518
  if (key === openEditorNativeToolbarActionKeys.undo) {
443
519
  const previous = history.undoStack[history.undoStack.length - 1];
444
520
  if (!previous) {
@@ -517,11 +593,11 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
517
593
  }, onRequestImage: (request) => {
518
594
  setImageUrl("https://placehold.co/960x420/png");
519
595
  setImageRequest(request);
520
- }, onSelectionChange: setSelection, onContentChangeJSON: (json) => {
596
+ }, onOpenPage: onOpenPage, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
521
597
  syncDocument(contentFromNativeJson(json), false);
522
- }, 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(UrlDialog, { title: "Image", value: imageUrl, visible: imageRequest !== null, placeholder: "https://", confirmLabel: "Insert", onChangeText: setImageUrl, onCancel: () => setImageRequest(null), onSubmit: submitImageDialog, theme: resolvedTheme })] }));
598
+ }, 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 })] }));
523
599
  };
524
- export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, }) => {
600
+ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, attachmentRuntime, }) => {
525
601
  const resolvedTheme = {
526
602
  ...defaultNativeTheme,
527
603
  ...theme,
@@ -531,7 +607,7 @@ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, r
531
607
  },
532
608
  };
533
609
  const normalizedDocument = normalizeForNativeEditor(document);
534
- 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)) }));
610
+ 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)) }));
535
611
  };
536
612
  const CommandButton = ({ label, active = false, primary = false, theme, onPress, }) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected: active }, onPress: onPress, style: [
537
613
  styles.commandButton,
package/dist/toolbar.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare const openEditorNativeToolbarActionKeys: {
8
8
  readonly table: "table";
9
9
  readonly toggleList: "toggleList";
10
10
  readonly callout: "callout";
11
+ readonly page: "page";
12
+ readonly attachment: "attachment";
11
13
  readonly moveBlockUp: "openeditor:block:moveUp";
12
14
  readonly moveBlockDown: "openeditor:block:moveDown";
13
15
  readonly undo: "openeditor:history:undo";
package/dist/toolbar.js CHANGED
@@ -16,6 +16,8 @@ export const openEditorNativeToolbarActionKeys = {
16
16
  table: "table",
17
17
  toggleList: "toggleList",
18
18
  callout: "callout",
19
+ page: "page",
20
+ attachment: "attachment",
19
21
  moveBlockUp: "openeditor:block:moveUp",
20
22
  moveBlockDown: "openeditor:block:moveDown",
21
23
  undo: "openeditor:history:undo",
@@ -81,6 +83,7 @@ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = t
81
83
  { type: "action", key: openEditorNativeToolbarActionKeys.table, label: "Table", icon: glyphIcon("Tbl") },
82
84
  { type: "action", key: openEditorNativeToolbarActionKeys.toggleList, label: "Toggle List", icon: glyphIcon("Tgl") },
83
85
  { type: "action", key: openEditorNativeToolbarActionKeys.callout, label: "Callout", icon: glyphIcon("!") },
86
+ { type: "action", key: openEditorNativeToolbarActionKeys.page, label: "Page", icon: glyphIcon("Pg") },
84
87
  { type: "command", command: "insertTableRowAfter", label: "Add Row", icon: glyphIcon("+R") },
85
88
  { type: "command", command: "insertTableColumnAfter", label: "Add Column", icon: glyphIcon("+C") },
86
89
  { type: "command", command: "deleteTableRow", label: "Delete Row", icon: glyphIcon("-R") },
@@ -88,6 +91,12 @@ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = t
88
91
  { type: "image", label: "Image", icon: defaultIcon("image") },
89
92
  ],
90
93
  },
94
+ {
95
+ type: "action",
96
+ key: openEditorNativeToolbarActionKeys.attachment,
97
+ label: "Upload file",
98
+ icon: platformIcon("paperclip", "attach-file", "File"),
99
+ },
91
100
  {
92
101
  type: "action",
93
102
  key: openEditorNativeToolbarActionKeys.moveBlockUp,
@@ -168,6 +177,8 @@ export const nativeToolbarParityCoverage = () => {
168
177
  ["table", openEditorNativeToolbarActionKeys.table],
169
178
  ["toggleList", openEditorNativeToolbarActionKeys.toggleList],
170
179
  ["callout", openEditorNativeToolbarActionKeys.callout],
180
+ ["page", openEditorNativeToolbarActionKeys.page],
181
+ ["attachment", openEditorNativeToolbarActionKeys.attachment],
171
182
  ]);
172
183
  const markControls = new Map([
173
184
  ["bold", "bold"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/native",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -27,14 +27,14 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@expo/vector-icons": "15.0.3",
30
- "@openeditor/core": "0.0.11",
31
- "@openeditor/exporters": "0.0.11",
32
- "@openeditor/extensions": "0.0.11",
30
+ "@openeditor/core": "0.0.13",
31
+ "@openeditor/exporters": "0.0.13",
32
+ "@openeditor/extensions": "0.0.13",
33
33
  "react": "19.2.3",
34
34
  "react-native": "0.85.3"
35
35
  },
36
36
  "peerDependencies": {
37
- "@openeditor/react-native-prose-editor": "0.0.11",
37
+ "@openeditor/react-native-prose-editor": "0.0.13",
38
38
  "react-native-keyboard-controller": ">=1.21",
39
39
  "react": ">=19",
40
40
  "react-native": ">=0.85"