@openeditor/native 0.0.12 → 0.0.14

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
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createDocument, findBlockSpecForNode, moveTopLevelBlock, textBlock, } from "@openeditor/core";
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, 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";
5
5
  import { fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
@@ -72,7 +72,7 @@ const openEditorNativeSchema = {
72
72
  group: "block",
73
73
  role: "block",
74
74
  htmlTag: "aside",
75
- attrs: { tone: { default: "info" } },
75
+ attrs: { emoji: { default: DEFAULT_CALLOUT_EMOJI } },
76
76
  },
77
77
  {
78
78
  name: "page",
@@ -82,10 +82,25 @@ const openEditorNativeSchema = {
82
82
  htmlTag: "p",
83
83
  attrs: {
84
84
  pageId: { default: null },
85
- icon: { default: "📄" },
85
+ icon: { default: DEFAULT_PAGE_EMOJI },
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) {
@@ -345,6 +360,10 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage) => {
345
360
  return _jsx(View, { style: { borderLeftColor: theme.accent, borderLeftWidth: 3, marginBottom: 12, paddingLeft: 12 }, children: children }, key);
346
361
  case "codeBlock":
347
362
  return _jsx(View, { style: { backgroundColor: theme.codeBackground, borderRadius: 8, marginBottom: 12, padding: 12 }, children: _jsx(Text, { style: { color: theme.codeText }, children: node.content?.map((child) => child.text ?? "").join("") }) }, key);
363
+ case "callout": {
364
+ const emoji = normalizeEmoji(node.attrs?.emoji, DEFAULT_CALLOUT_EMOJI);
365
+ return _jsxs(View, { style: { backgroundColor: theme.codeBackground, borderRadius: 10, flexDirection: "row", gap: 10, marginBottom: 12, padding: 14 }, children: [_jsx(Text, { style: { fontSize: 20 }, children: emoji }), _jsx(View, { style: { flex: 1 }, children: children })] }, key);
366
+ }
348
367
  case "divider":
349
368
  return _jsx(View, { style: { backgroundColor: theme.border, height: 1, marginVertical: 16 } }, key);
350
369
  case "image":
@@ -356,10 +375,21 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage) => {
356
375
  case "page": {
357
376
  const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
358
377
  const title = node.content?.map((child) => child.text ?? "").join("") || "Untitled";
359
- const icon = typeof node.attrs?.icon === "string" ? node.attrs.icon : "📄";
378
+ const icon = normalizeEmoji(node.attrs?.icon, DEFAULT_PAGE_EMOJI);
360
379
  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
380
  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
381
  }
382
+ case "attachment": {
383
+ const snapshot = {
384
+ attachmentId: typeof node.attrs?.attachmentId === "string" ? node.attrs.attachmentId : null,
385
+ name: typeof node.attrs?.name === "string" ? node.attrs.name : "Attachment",
386
+ mimeType: typeof node.attrs?.mimeType === "string" ? node.attrs.mimeType : null,
387
+ size: typeof node.attrs?.size === "number" ? node.attrs.size : null,
388
+ url: typeof node.attrs?.url === "string" ? node.attrs.url : null,
389
+ };
390
+ 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" })] });
391
+ 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);
392
+ }
363
393
  default:
364
394
  if (node.type === "column") {
365
395
  return _jsx(View, { style: { flex: 1 }, children: children }, key);
@@ -396,7 +426,7 @@ export const OpenEditorNativeToolbar = ({ actions = [
396
426
  onActionPress?.(action.key);
397
427
  }, style: [styles.toolbarButton, { backgroundColor: resolvedTheme.accent, borderColor: resolvedTheme.borderStrong }], children: _jsx(Text, { style: [styles.toolbarButtonText, { color: resolvedTheme.accentText }], children: action.label }) }, action.key))) }));
398
428
  };
399
- export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, }) => {
429
+ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, onOpenPage, attachmentRuntime, }) => {
400
430
  const editorRef = useRef(null);
401
431
  const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(document));
402
432
  const [selection, setSelection] = useState();
@@ -404,6 +434,8 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
404
434
  const [linkUrl, setLinkUrl] = useState("https://");
405
435
  const [imageRequest, setImageRequest] = useState(null);
406
436
  const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
437
+ const [attachmentUpload, setAttachmentUpload] = useState(null);
438
+ const attachmentAbortRef = useRef(null);
407
439
  const [history, setHistory] = useState({
408
440
  undoStack: [],
409
441
  redoStack: [],
@@ -445,6 +477,31 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
445
477
  }
446
478
  syncDocument(moveTopLevelBlock(nativeDocument, selectedIndex, nextIndex), true);
447
479
  };
480
+ const uploadAttachment = async () => {
481
+ if (!attachmentRuntime?.selectAttachment || !attachmentRuntime.uploadAttachment)
482
+ return;
483
+ attachmentAbortRef.current?.abort();
484
+ const controller = new AbortController();
485
+ attachmentAbortRef.current = controller;
486
+ try {
487
+ const input = await attachmentRuntime.selectAttachment({ signal: controller.signal });
488
+ if (!input || controller.signal.aborted)
489
+ return;
490
+ setAttachmentUpload({ input, progress: 0, error: null });
491
+ const validation = await attachmentRuntime.validateAttachment?.(input);
492
+ if (validation && !validation.accepted)
493
+ throw new Error(validation.message);
494
+ const snapshot = await attachmentRuntime.uploadAttachment(input, { signal: controller.signal, onProgress: (progress) => setAttachmentUpload((current) => current ? { ...current, progress } : current) });
495
+ if (controller.signal.aborted)
496
+ return;
497
+ editorRef.current?.insertContentJson({ type: "doc", content: [{ type: "attachment", attrs: snapshot }] });
498
+ setAttachmentUpload(null);
499
+ }
500
+ catch (cause) {
501
+ if (!controller.signal.aborted)
502
+ setAttachmentUpload((current) => current ? { ...current, error: cause instanceof Error ? cause.message : "Upload failed." } : current);
503
+ }
504
+ };
448
505
  const handleToolbarAction = (key) => {
449
506
  if (key === openEditorNativeToolbarActionKeys.dismissKeyboard) {
450
507
  dismissKeyboard();
@@ -458,6 +515,10 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
458
515
  moveSelectedBlock(1);
459
516
  return;
460
517
  }
518
+ if (key === openEditorNativeToolbarActionKeys.attachment) {
519
+ void uploadAttachment();
520
+ return;
521
+ }
461
522
  if (key === openEditorNativeToolbarActionKeys.undo) {
462
523
  const previous = history.undoStack[history.undoStack.length - 1];
463
524
  if (!previous) {
@@ -536,11 +597,11 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
536
597
  }, onRequestImage: (request) => {
537
598
  setImageUrl("https://placehold.co/960x420/png");
538
599
  setImageRequest(request);
539
- }, onOpenPage: onOpenPage, onSelectionChange: setSelection, onContentChangeJSON: (json) => {
600
+ }, onOpenPage: onOpenPage, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
540
601
  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 })] }));
602
+ }, 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
603
  };
543
- export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, }) => {
604
+ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, attachmentRuntime, }) => {
544
605
  const resolvedTheme = {
545
606
  ...defaultNativeTheme,
546
607
  ...theme,
@@ -550,7 +611,7 @@ export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, r
550
611
  },
551
612
  };
552
613
  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)) }));
614
+ 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
615
  };
555
616
  const CommandButton = ({ label, active = false, primary = false, theme, onPress, }) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected: active }, onPress: onPress, style: [
556
617
  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.14",
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.14",
31
+ "@openeditor/exporters": "0.0.14",
32
+ "@openeditor/extensions": "0.0.14",
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.14",
38
38
  "react-native-keyboard-controller": ">=1.21",
39
39
  "react": ">=19",
40
40
  "react-native": ">=0.85"