@openeditor/native 0.0.12 → 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";
@@ -16,6 +16,7 @@ export type OpenEditorNativeProps = {
16
16
  icon?: string | null;
17
17
  href?: string | null;
18
18
  }) => void;
19
+ attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
19
20
  };
20
21
  export type OpenEditorNativeViewerProps = Omit<OpenEditorNativeProps, "editable" | "showToolbar" | "onChange"> & {
21
22
  showToolbar?: false;
@@ -67,8 +68,8 @@ export type OpenEditorNativeTheme = {
67
68
  toolbar?: OpenEditorNativeToolbarTheme;
68
69
  };
69
70
  export declare const OpenEditorNativeToolbar: ({ actions, onActionPress, theme, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element;
70
- export declare const OpenEditorNative: ({ document, editable, placeholder, showToolbar, theme, toolbarPlacement, onChange, onOpenPage, }: OpenEditorNativeProps) => import("react").JSX.Element;
71
- export declare const OpenEditorNativeViewer: ({ document, theme, renderers, onOpenPage, }: 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;
72
73
  export { createNativeEditorDocument, fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
73
74
  export { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, nativeToolbarParityCoverage, openEditorNativeToolbarActionKeys, openEditorNativeToolbarItems, } from "./toolbar.js";
74
75
  export type { OpenEditorNativeToolbarActionKey } from "./toolbar.js";
package/dist/index.js CHANGED
@@ -86,6 +86,21 @@ const openEditorNativeSchema = {
86
86
  href: { default: null },
87
87
  },
88
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
+ },
89
104
  { name: "codeBlock", content: "text*", group: "block", role: "textBlock", htmlTag: "pre" },
90
105
  { name: "horizontalRule", content: "", group: "block", role: "block", htmlTag: "hr", isVoid: true },
91
106
  {
@@ -319,11 +334,11 @@ const nativeSeedDocument = createDocument([
319
334
  { type: "horizontalRule" },
320
335
  { type: "image", attrs: { src: "https://placehold.co/960x420/png", alt: "Placeholder" } },
321
336
  ]);
322
- const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage) => {
337
+ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachmentRuntime) => {
323
338
  if (node.type === "text") {
324
339
  return _jsx(Text, { style: { color: theme.text }, children: node.text ?? "" }, key);
325
340
  }
326
- const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, onOpenPage)) ?? null;
341
+ const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, onOpenPage, attachmentRuntime)) ?? null;
327
342
  const blockKey = findBlockSpecForNode(defaultBlockRegistry, node)?.name ?? node.type;
328
343
  const customRenderer = renderers?.[blockKey];
329
344
  if (customRenderer) {
@@ -360,6 +375,17 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage) => {
360
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" })] });
361
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);
362
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
+ }
363
389
  default:
364
390
  if (node.type === "column") {
365
391
  return _jsx(View, { style: { flex: 1 }, children: children }, key);
@@ -396,7 +422,7 @@ export const OpenEditorNativeToolbar = ({ actions = [
396
422
  onActionPress?.(action.key);
397
423
  }, style: [styles.toolbarButton, { backgroundColor: resolvedTheme.accent, borderColor: resolvedTheme.borderStrong }], children: _jsx(Text, { style: [styles.toolbarButtonText, { color: resolvedTheme.accentText }], children: action.label }) }, action.key))) }));
398
424
  };
399
- export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, }) => {
425
+ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, attachmentRuntime, }) => {
400
426
  const editorRef = useRef(null);
401
427
  const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(document));
402
428
  const [selection, setSelection] = useState();
@@ -404,6 +430,8 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
404
430
  const [linkUrl, setLinkUrl] = useState("https://");
405
431
  const [imageRequest, setImageRequest] = useState(null);
406
432
  const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
433
+ const [attachmentUpload, setAttachmentUpload] = useState(null);
434
+ const attachmentAbortRef = useRef(null);
407
435
  const [history, setHistory] = useState({
408
436
  undoStack: [],
409
437
  redoStack: [],
@@ -445,6 +473,31 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
445
473
  }
446
474
  syncDocument(moveTopLevelBlock(nativeDocument, selectedIndex, nextIndex), true);
447
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
+ };
448
501
  const handleToolbarAction = (key) => {
449
502
  if (key === openEditorNativeToolbarActionKeys.dismissKeyboard) {
450
503
  dismissKeyboard();
@@ -458,6 +511,10 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
458
511
  moveSelectedBlock(1);
459
512
  return;
460
513
  }
514
+ if (key === openEditorNativeToolbarActionKeys.attachment) {
515
+ void uploadAttachment();
516
+ return;
517
+ }
461
518
  if (key === openEditorNativeToolbarActionKeys.undo) {
462
519
  const previous = history.undoStack[history.undoStack.length - 1];
463
520
  if (!previous) {
@@ -536,11 +593,11 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
536
593
  }, onRequestImage: (request) => {
537
594
  setImageUrl("https://placehold.co/960x420/png");
538
595
  setImageRequest(request);
539
- }, onOpenPage: onOpenPage, onSelectionChange: setSelection, onContentChangeJSON: (json) => {
596
+ }, onOpenPage: onOpenPage, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
540
597
  syncDocument(contentFromNativeJson(json), false);
541
- }, 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 })] }));
542
599
  };
543
- export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, }) => {
600
+ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, attachmentRuntime, }) => {
544
601
  const resolvedTheme = {
545
602
  ...defaultNativeTheme,
546
603
  ...theme,
@@ -550,7 +607,7 @@ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, r
550
607
  },
551
608
  };
552
609
  const normalizedDocument = normalizeForNativeEditor(document);
553
- 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)) }));
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)) }));
554
611
  };
555
612
  const CommandButton = ({ label, active = false, primary = false, theme, onPress, }) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected: active }, onPress: onPress, style: [
556
613
  styles.commandButton,
package/dist/toolbar.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare const openEditorNativeToolbarActionKeys: {
9
9
  readonly toggleList: "toggleList";
10
10
  readonly callout: "callout";
11
11
  readonly page: "page";
12
+ readonly attachment: "attachment";
12
13
  readonly moveBlockUp: "openeditor:block:moveUp";
13
14
  readonly moveBlockDown: "openeditor:block:moveDown";
14
15
  readonly undo: "openeditor:history:undo";
package/dist/toolbar.js CHANGED
@@ -17,6 +17,7 @@ export const openEditorNativeToolbarActionKeys = {
17
17
  toggleList: "toggleList",
18
18
  callout: "callout",
19
19
  page: "page",
20
+ attachment: "attachment",
20
21
  moveBlockUp: "openeditor:block:moveUp",
21
22
  moveBlockDown: "openeditor:block:moveDown",
22
23
  undo: "openeditor:history:undo",
@@ -90,6 +91,12 @@ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = t
90
91
  { type: "image", label: "Image", icon: defaultIcon("image") },
91
92
  ],
92
93
  },
94
+ {
95
+ type: "action",
96
+ key: openEditorNativeToolbarActionKeys.attachment,
97
+ label: "Upload file",
98
+ icon: platformIcon("paperclip", "attach-file", "File"),
99
+ },
93
100
  {
94
101
  type: "action",
95
102
  key: openEditorNativeToolbarActionKeys.moveBlockUp,
@@ -171,6 +178,7 @@ export const nativeToolbarParityCoverage = () => {
171
178
  ["toggleList", openEditorNativeToolbarActionKeys.toggleList],
172
179
  ["callout", openEditorNativeToolbarActionKeys.callout],
173
180
  ["page", openEditorNativeToolbarActionKeys.page],
181
+ ["attachment", openEditorNativeToolbarActionKeys.attachment],
174
182
  ]);
175
183
  const markControls = new Map([
176
184
  ["bold", "bold"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/native",
3
- "version": "0.0.12",
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.12",
31
- "@openeditor/exporters": "0.0.12",
32
- "@openeditor/extensions": "0.0.12",
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.12",
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"