@openeditor/native 0.0.33 → 0.0.35

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.
@@ -0,0 +1,211 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { BottomSheet as UniversalBottomSheet, Button as NativeButton, Column as NativeColumn, ListItem as NativeListItem, RNHostView as NativeRNHostView, Row as NativeRow, ScrollView as NativeScrollView, Spacer as NativeSpacer, Text as NativeText, } from "@expo/ui";
3
+ import { BottomSheet as SwiftUIBottomSheet, Group as SwiftUIGroup, Host as SwiftUIHost, } from "@expo/ui/swift-ui";
4
+ import { accessibilityLabel as nativeAccessibilityLabel, frame as nativeFrame, glassEffect, padding as nativePadding, presentationDetents, presentationDragIndicator, } from "@expo/ui/swift-ui/modifiers";
5
+ import { openEditorIcons } from "@openeditor/icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react-native";
7
+ import { memo, useRef, useState } from "react";
8
+ import { Keyboard, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
9
+ import { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, openEditorNativeTableToolbarItems, } from "./toolbar-items.js";
10
+ export { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, } from "./toolbar-items.js";
11
+ const isItemActive = (item, state) => (item.activeWhen?.block !== undefined &&
12
+ state.activeNodes.includes(item.activeWhen.block) &&
13
+ (item.activeWhen?.headingLevel === undefined ||
14
+ state.headingLevel === item.activeWhen.headingLevel)) ||
15
+ (item.activeWhen?.mark !== undefined &&
16
+ state.activeMarks.includes(item.activeWhen.mark)) ||
17
+ (item.activeWhen?.table !== undefined &&
18
+ state.table?.[item.activeWhen.table] === true);
19
+ const isItemDisabled = (item, state) => !state.editable ||
20
+ (item.disabledWhen === "cannotUndo" && !state.canUndo) ||
21
+ (item.disabledWhen === "cannotRedo" && !state.canRedo) ||
22
+ (item.disabledWhen === "cannotMergeCells" && !state.table?.canMergeCells);
23
+ const TOOLBAR_BUTTON_SIZE = 32;
24
+ const IOS_BLOCK_PICKER_PRESENTATION_MODIFIERS = [
25
+ nativeFrame({ alignment: "topLeading", maxWidth: Infinity }),
26
+ nativePadding({ leading: 16, top: 16, trailing: 16 }),
27
+ presentationDragIndicator("visible"),
28
+ presentationDetents(["medium", "large"]),
29
+ ];
30
+ /**
31
+ * Expo's universal wrapper maps `onDismiss` to the binding-change event on
32
+ * iOS, losing SwiftUI's actual post-animation dismissal callback. Editor
33
+ * commands need that real lifecycle boundary so focus-owning native effects do
34
+ * not race the outgoing sheet.
35
+ */
36
+ const NativeBlockPickerSheet = ({ children, isPresented, onDismiss, onIsPresentedChange, }) => {
37
+ if (Platform.OS !== "ios") {
38
+ return (_jsx(UniversalBottomSheet, { isPresented: isPresented, onDismiss: onDismiss, showDragIndicator: true, snapPoints: ["half", "full"], testID: "openeditor-block-picker", children: children }));
39
+ }
40
+ return (_jsx(SwiftUIHost, { pointerEvents: "none", style: { position: "absolute" }, children: _jsx(SwiftUIBottomSheet, { isPresented: isPresented, onDismiss: onDismiss, onIsPresentedChange: onIsPresentedChange, testID: "openeditor-block-picker", children: _jsx(SwiftUIGroup, { modifiers: IOS_BLOCK_PICKER_PRESENTATION_MODIFIERS, children: children }) }) }));
41
+ };
42
+ export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ availableNativeEffects = [], controller, state, items = defaultOpenEditorNativeToolbarItems, theme, style, onCommandError, visible = true, blockHandlesVisible = false, onBlockHandlesVisibleChange, }) {
43
+ const [blockPickerOpen, setBlockPickerOpen] = useState(false);
44
+ const [linkEditorOpen, setLinkEditorOpen] = useState(false);
45
+ const [linkValue, setLinkValue] = useState("");
46
+ const pendingBlockRef = useRef(null);
47
+ const pickerSelectionRef = useRef(undefined);
48
+ const runCommand = (command) => {
49
+ void command.catch((cause) => {
50
+ onCommandError?.(cause instanceof Error
51
+ ? cause
52
+ : new Error("OpenEditor command failed."));
53
+ });
54
+ };
55
+ const colors = {
56
+ background: theme?.surface ?? "#ffffff",
57
+ border: theme?.border ?? "#e5e7eb",
58
+ text: theme?.text ?? "#171717",
59
+ muted: theme?.muted ?? "#737373",
60
+ accent: theme?.accent ?? "#171717",
61
+ accentText: theme?.accentText ?? "#ffffff",
62
+ };
63
+ const blockPickerItems = openEditorNativeBlockPickerItems.filter((item) => {
64
+ if (item.key === "image")
65
+ return availableNativeEffects.includes("pickImage");
66
+ if (item.key === "attachment")
67
+ return availableNativeEffects.includes("pickAttachment");
68
+ if (item.key === "page")
69
+ return availableNativeEffects.includes("createPage");
70
+ return true;
71
+ });
72
+ const blockPickerGroups = blockPickerItems.reduce((groups, item) => {
73
+ const current = groups.at(-1);
74
+ if (current?.label === item.group)
75
+ current.items.push(item);
76
+ else
77
+ groups.push({ label: item.group, items: [item] });
78
+ return groups;
79
+ }, []);
80
+ const visibleToolbarItems = state.activeNodes.includes("table")
81
+ ? [
82
+ ...defaultOpenEditorNativeToolbarItems.filter((item) => item.action === "toggleBlockHandles"),
83
+ ...openEditorNativeTableToolbarItems.filter((item) => item.key === "table-split-cell"
84
+ ? state.table?.canSplitCell
85
+ : item.key !== "table-merge-cells" || !state.table?.canSplitCell),
86
+ ]
87
+ : items;
88
+ return (_jsxs(_Fragment, { children: [visible ? (_jsx(View, { style: [styles.root, style], children: _jsx(ScrollView, { contentContainerStyle: styles.content, horizontal: true, keyboardShouldPersistTaps: "always", showsHorizontalScrollIndicator: false, children: visibleToolbarItems.map((item) => {
89
+ const active = item.action === "toggleBlockHandles"
90
+ ? blockHandlesVisible
91
+ : isItemActive(item, state);
92
+ const disabled = isItemDisabled(item, state);
93
+ const accessibilityLabel = item.action === "toggleBlockHandles" && blockHandlesVisible
94
+ ? "Hide block handles"
95
+ : item.label;
96
+ return (_jsx(Pressable, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", accessibilityState: { disabled, selected: active }, disabled: disabled, hitSlop: 6, onPress: () => {
97
+ if (item.action === "toggleBlockHandles") {
98
+ onBlockHandlesVisibleChange?.(!blockHandlesVisible);
99
+ return;
100
+ }
101
+ if (item.action === "openBlockPicker") {
102
+ pickerSelectionRef.current =
103
+ state.selection.type === "text"
104
+ ? {
105
+ anchor: state.selection.anchor,
106
+ head: state.selection.head,
107
+ }
108
+ : undefined;
109
+ setBlockPickerOpen(true);
110
+ return;
111
+ }
112
+ if (item.action === "openLinkEditor") {
113
+ setLinkValue("");
114
+ setLinkEditorOpen(true);
115
+ return;
116
+ }
117
+ if (!item.command) {
118
+ Keyboard.dismiss();
119
+ runCommand(controller.blur());
120
+ return;
121
+ }
122
+ runCommand(controller.command(item.command));
123
+ }, style: ({ pressed }) => [
124
+ styles.button,
125
+ active && { backgroundColor: colors.accent },
126
+ pressed && styles.pressed,
127
+ disabled && styles.disabled,
128
+ ], children: _jsx(HugeiconsIcon, { accessible: false, color: active
129
+ ? colors.accentText
130
+ : disabled
131
+ ? colors.muted
132
+ : colors.text, icon: openEditorIcons[item.icon], size: 20, strokeWidth: 1.8 }) }, item.key));
133
+ }) }) })) : null, _jsx(NativeBlockPickerSheet, { isPresented: blockPickerOpen, onDismiss: () => {
134
+ const block = pendingBlockRef.current;
135
+ pendingBlockRef.current = null;
136
+ pickerSelectionRef.current = undefined;
137
+ if (block) {
138
+ runCommand(controller.command(block).then(() => controller.focus()));
139
+ }
140
+ }, onIsPresentedChange: setBlockPickerOpen, children: _jsxs(NativeColumn, { alignment: "start", spacing: 0, style: { height: "100%", width: "100%" }, children: [_jsxs(NativeRow, { alignment: "center", spacing: 8, style: { paddingBottom: 10, width: "100%" }, children: [_jsx(NativeButton, { modifiers: Platform.OS === "ios"
141
+ ? [
142
+ nativeAccessibilityLabel("Cancel"),
143
+ glassEffect({
144
+ glass: { interactive: true, variant: "clear" },
145
+ shape: "circle",
146
+ }),
147
+ ]
148
+ : undefined, onPress: () => setBlockPickerOpen(false), style: { borderRadius: 18, height: 36, width: 36 }, testID: "close-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.close, size: 18, strokeWidth: 1.8 }) }) }) }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeText, { testID: "block-picker-title", textStyle: { fontSize: 17, fontWeight: "600" }, children: "Insert block" }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeButton, { modifiers: Platform.OS === "ios"
149
+ ? [
150
+ nativeAccessibilityLabel("Done"),
151
+ glassEffect({
152
+ glass: { interactive: true, variant: "clear" },
153
+ shape: "circle",
154
+ }),
155
+ ]
156
+ : undefined, onPress: () => setBlockPickerOpen(false), style: { borderRadius: 18, height: 36, width: 36 }, testID: "done-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.check, size: 18, strokeWidth: 1.8 }) }) }) })] }), _jsx(NativeScrollView, { showsIndicators: true, style: { height: "100%", width: "100%" }, children: _jsx(NativeColumn, { alignment: "start", spacing: 20, style: { width: "100%" }, children: blockPickerGroups.map((group) => (_jsxs(NativeColumn, { alignment: "start", spacing: 4, style: { width: "100%" }, children: [_jsx(NativeText, { textStyle: {
157
+ color: colors.muted,
158
+ fontSize: 12,
159
+ fontWeight: "600",
160
+ }, children: group.label }), group.items.map((item) => (_jsx(NativeListItem, { leading: (_jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons[item.icon], size: 20, strokeWidth: 1.8 })), modifiers: Platform.OS === "ios"
161
+ ? [nativePadding({ vertical: 10 })]
162
+ : undefined, onPress: () => {
163
+ pendingBlockRef.current = {
164
+ type: "insertBlock",
165
+ block: item.key,
166
+ selection: pickerSelectionRef.current,
167
+ };
168
+ setBlockPickerOpen(false);
169
+ }, testID: `insert-${item.key}`, trailing: (_jsx(HugeiconsIcon, { accessible: false, color: colors.muted, icon: openEditorIcons.chevronRight, size: 16, strokeWidth: 1.8 })), children: _jsx(NativeText, { children: item.label }, `${item.key}-label`) }, item.key)))] }, group.label))) }) })] }) }), _jsx(Modal, { animationType: "fade", onRequestClose: () => setLinkEditorOpen(false), presentationStyle: "formSheet", visible: linkEditorOpen, children: _jsxs(View, { style: [styles.linkSheet, { backgroundColor: colors.background }], children: [_jsx(Text, { accessibilityRole: "header", style: [styles.sheetTitle, { color: colors.text }], children: "Edit link" }), _jsx(TextInput, { accessibilityLabel: "Link URL", autoCapitalize: "none", autoCorrect: false, onChangeText: setLinkValue, placeholder: "https://example.com", placeholderTextColor: colors.muted, style: [
170
+ styles.linkInput,
171
+ { borderColor: colors.border, color: colors.text },
172
+ ], value: linkValue }), _jsxs(View, { style: styles.linkActions, children: [_jsx(Pressable, { accessibilityLabel: "Cancel link editing", accessibilityRole: "button", onPress: () => setLinkEditorOpen(false), style: styles.linkAction, children: _jsx(Text, { style: { color: colors.text }, children: "Cancel" }) }), _jsx(Pressable, { accessibilityLabel: "Remove", accessibilityRole: "button", onPress: () => {
173
+ setLinkEditorOpen(false);
174
+ runCommand(controller.command({ type: "setLink", href: null }));
175
+ }, style: styles.linkAction, children: _jsx(Text, { style: { color: colors.text }, children: "Remove" }) }), _jsx(Pressable, { accessibilityLabel: "Apply", accessibilityRole: "button", disabled: !linkValue.trim(), onPress: () => {
176
+ const href = linkValue.trim();
177
+ setLinkEditorOpen(false);
178
+ runCommand(controller.command({ type: "setLink", href }));
179
+ }, style: [styles.linkAction, !linkValue.trim() && styles.disabled], children: _jsx(Text, { style: { color: colors.accent }, children: "Apply" }) })] })] }) })] }));
180
+ });
181
+ const styles = StyleSheet.create({
182
+ root: {
183
+ minHeight: 46,
184
+ },
185
+ content: {
186
+ alignItems: "center",
187
+ gap: 6,
188
+ paddingHorizontal: 8,
189
+ paddingVertical: 7,
190
+ },
191
+ button: {
192
+ alignItems: "center",
193
+ borderRadius: TOOLBAR_BUTTON_SIZE / 2,
194
+ height: TOOLBAR_BUTTON_SIZE,
195
+ justifyContent: "center",
196
+ width: TOOLBAR_BUTTON_SIZE,
197
+ },
198
+ pressed: { opacity: 0.64 },
199
+ disabled: { opacity: 0.42 },
200
+ sheetTitle: { fontSize: 18, fontWeight: "700" },
201
+ linkSheet: { flex: 1, gap: 20, justifyContent: "center", padding: 24 },
202
+ linkInput: {
203
+ borderRadius: 10,
204
+ borderWidth: 1,
205
+ fontSize: 16,
206
+ minHeight: 48,
207
+ paddingHorizontal: 14,
208
+ },
209
+ linkActions: { flexDirection: "row", justifyContent: "flex-end", gap: 18 },
210
+ linkAction: { minHeight: 44, justifyContent: "center", paddingHorizontal: 4 },
211
+ });
@@ -0,0 +1,109 @@
1
+ import type { OpenEditorRuntimeCommand } from "@openeditor/embedded-runtime";
2
+ import type { OpenEditorIconName } from "@openeditor/icons";
3
+ export type OpenEditorNativeToolbarItem = {
4
+ key: string;
5
+ label: string;
6
+ icon: OpenEditorIconName;
7
+ command?: OpenEditorRuntimeCommand;
8
+ action?: "openBlockPicker" | "openLinkEditor" | "toggleBlockHandles" | "dismissKeyboard";
9
+ activeWhen?: {
10
+ block?: string;
11
+ headingLevel?: number;
12
+ mark?: string;
13
+ table?: "headerRow" | "headerColumn";
14
+ };
15
+ disabledWhen?: "cannotUndo" | "cannotRedo" | "cannotMergeCells";
16
+ };
17
+ export declare const defaultOpenEditorNativeToolbarItems: readonly OpenEditorNativeToolbarItem[];
18
+ export declare const openEditorNativeBlockPickerItems: readonly [{
19
+ readonly key: "paragraph";
20
+ readonly label: "Text";
21
+ readonly group: "Text";
22
+ readonly icon: "text";
23
+ }, {
24
+ readonly key: "heading1";
25
+ readonly label: "Heading 1";
26
+ readonly group: "Text";
27
+ readonly icon: "heading1";
28
+ }, {
29
+ readonly key: "heading2";
30
+ readonly label: "Heading 2";
31
+ readonly group: "Text";
32
+ readonly icon: "heading2";
33
+ }, {
34
+ readonly key: "heading3";
35
+ readonly label: "Heading 3";
36
+ readonly group: "Text";
37
+ readonly icon: "heading3";
38
+ }, {
39
+ readonly key: "bulletList";
40
+ readonly label: "Bullet list";
41
+ readonly group: "Text";
42
+ readonly icon: "bulletList";
43
+ }, {
44
+ readonly key: "orderedList";
45
+ readonly label: "Numbered list";
46
+ readonly group: "Text";
47
+ readonly icon: "orderedList";
48
+ }, {
49
+ readonly key: "taskList";
50
+ readonly label: "Task list";
51
+ readonly group: "Text";
52
+ readonly icon: "taskList";
53
+ }, {
54
+ readonly key: "toggleList";
55
+ readonly label: "Toggle list";
56
+ readonly group: "Text";
57
+ readonly icon: "toggleList";
58
+ }, {
59
+ readonly key: "blockquote";
60
+ readonly label: "Quote";
61
+ readonly group: "Text";
62
+ readonly icon: "blockquote";
63
+ }, {
64
+ readonly key: "codeBlock";
65
+ readonly label: "Code block";
66
+ readonly group: "Text";
67
+ readonly icon: "codeBlock";
68
+ }, {
69
+ readonly key: "divider";
70
+ readonly label: "Divider";
71
+ readonly group: "Structure";
72
+ readonly icon: "divider";
73
+ }, {
74
+ readonly key: "columns";
75
+ readonly label: "Columns";
76
+ readonly group: "Layout";
77
+ readonly icon: "columns";
78
+ }, {
79
+ readonly key: "table";
80
+ readonly label: "Table";
81
+ readonly group: "Layout";
82
+ readonly icon: "table";
83
+ }, {
84
+ readonly key: "callout";
85
+ readonly label: "Callout";
86
+ readonly group: "Embed";
87
+ readonly icon: "callout";
88
+ }, {
89
+ readonly key: "diagram";
90
+ readonly label: "Diagram";
91
+ readonly group: "Embed";
92
+ readonly icon: "diagram";
93
+ }, {
94
+ readonly key: "page";
95
+ readonly label: "Page";
96
+ readonly group: "Embed";
97
+ readonly icon: "page";
98
+ }, {
99
+ readonly key: "image";
100
+ readonly label: "Image";
101
+ readonly group: "Media";
102
+ readonly icon: "image";
103
+ }, {
104
+ readonly key: "attachment";
105
+ readonly label: "File";
106
+ readonly group: "Media";
107
+ readonly icon: "attachment";
108
+ }];
109
+ export declare const openEditorNativeTableToolbarItems: readonly OpenEditorNativeToolbarItem[];
@@ -0,0 +1,57 @@
1
+ export const defaultOpenEditorNativeToolbarItems = [
2
+ { key: "insert", label: "Insert block", icon: "addBlock", action: "openBlockPicker" },
3
+ { key: "handles", label: "Show block handles", icon: "dragHandle", action: "toggleBlockHandles" },
4
+ { key: "paragraph", label: "Text", icon: "text", command: { type: "setParagraph" }, activeWhen: { block: "paragraph" } },
5
+ { key: "heading-1", label: "Heading 1", icon: "heading1", command: { type: "toggleHeading", level: 1 }, activeWhen: { block: "heading", headingLevel: 1 } },
6
+ { key: "heading-2", label: "Heading 2", icon: "heading2", command: { type: "toggleHeading", level: 2 }, activeWhen: { block: "heading", headingLevel: 2 } },
7
+ { key: "bold", label: "Bold", icon: "bold", command: { type: "toggleMark", mark: "bold" }, activeWhen: { mark: "bold" } },
8
+ { key: "italic", label: "Italic", icon: "italic", command: { type: "toggleMark", mark: "italic" }, activeWhen: { mark: "italic" } },
9
+ { key: "underline", label: "Underline", icon: "underline", command: { type: "toggleMark", mark: "underline" }, activeWhen: { mark: "underline" } },
10
+ { key: "strike", label: "Strikethrough", icon: "strike", command: { type: "toggleMark", mark: "strike" }, activeWhen: { mark: "strike" } },
11
+ { key: "code", label: "Inline code", icon: "code", command: { type: "toggleMark", mark: "code" }, activeWhen: { mark: "code" } },
12
+ { key: "link", label: "Link", icon: "link", action: "openLinkEditor", activeWhen: { mark: "link" } },
13
+ { key: "bullet-list", label: "Bullet list", icon: "bulletList", command: { type: "toggleList", list: "bullet" }, activeWhen: { block: "bulletList" } },
14
+ { key: "ordered-list", label: "Numbered list", icon: "orderedList", command: { type: "toggleList", list: "ordered" }, activeWhen: { block: "orderedList" } },
15
+ { key: "task-list", label: "Task list", icon: "taskList", command: { type: "toggleList", list: "task" }, activeWhen: { block: "taskList" } },
16
+ { key: "quote", label: "Quote", icon: "blockquote", command: { type: "toggleBlockquote" }, activeWhen: { block: "blockquote" } },
17
+ { key: "code-block", label: "Code block", icon: "codeBlock", command: { type: "toggleCodeBlock" }, activeWhen: { block: "codeBlock" } },
18
+ { key: "undo", label: "Undo", icon: "undo", command: { type: "undo" }, disabledWhen: "cannotUndo" },
19
+ { key: "redo", label: "Redo", icon: "redo", command: { type: "redo" }, disabledWhen: "cannotRedo" },
20
+ { key: "keyboard", label: "Dismiss keyboard", icon: "keyboard", action: "dismissKeyboard" },
21
+ ];
22
+ export const openEditorNativeBlockPickerItems = [
23
+ { key: "paragraph", label: "Text", group: "Text", icon: "text" },
24
+ { key: "heading1", label: "Heading 1", group: "Text", icon: "heading1" },
25
+ { key: "heading2", label: "Heading 2", group: "Text", icon: "heading2" },
26
+ { key: "heading3", label: "Heading 3", group: "Text", icon: "heading3" },
27
+ { key: "bulletList", label: "Bullet list", group: "Text", icon: "bulletList" },
28
+ { key: "orderedList", label: "Numbered list", group: "Text", icon: "orderedList" },
29
+ { key: "taskList", label: "Task list", group: "Text", icon: "taskList" },
30
+ { key: "toggleList", label: "Toggle list", group: "Text", icon: "toggleList" },
31
+ { key: "blockquote", label: "Quote", group: "Text", icon: "blockquote" },
32
+ { key: "codeBlock", label: "Code block", group: "Text", icon: "codeBlock" },
33
+ { key: "divider", label: "Divider", group: "Structure", icon: "divider" },
34
+ { key: "columns", label: "Columns", group: "Layout", icon: "columns" },
35
+ { key: "table", label: "Table", group: "Layout", icon: "table" },
36
+ { key: "callout", label: "Callout", group: "Embed", icon: "callout" },
37
+ { key: "diagram", label: "Diagram", group: "Embed", icon: "diagram" },
38
+ { key: "page", label: "Page", group: "Embed", icon: "page" },
39
+ { key: "image", label: "Image", group: "Media", icon: "image" },
40
+ { key: "attachment", label: "File", group: "Media", icon: "attachment" },
41
+ ];
42
+ export const openEditorNativeTableToolbarItems = [
43
+ { key: "table-row-before", label: "Insert row above", icon: "tableRowInsertBefore", command: { type: "addTableRow", after: false } },
44
+ { key: "table-row-after", label: "Insert row below", icon: "tableRowInsertAfter", command: { type: "addTableRow" } },
45
+ { key: "table-delete-row", label: "Delete row", icon: "tableRowDelete", command: { type: "deleteTableRow" } },
46
+ { key: "table-column-before", label: "Insert column left", icon: "tableColumnInsertBefore", command: { type: "addTableColumn", after: false } },
47
+ { key: "table-column-after", label: "Insert column right", icon: "tableColumnInsertAfter", command: { type: "addTableColumn" } },
48
+ { key: "table-delete-column", label: "Delete column", icon: "tableColumnDelete", command: { type: "deleteTableColumn" } },
49
+ { key: "table-header-row", label: "Toggle header row", icon: "tableHeaderRow", command: { type: "toggleTableHeaderRow" }, activeWhen: { table: "headerRow" } },
50
+ { key: "table-header-column", label: "Toggle header column", icon: "tableHeaderColumn", command: { type: "toggleTableHeaderColumn" }, activeWhen: { table: "headerColumn" } },
51
+ { key: "table-merge-cells", label: "Merge selected cells", icon: "mergeCells", command: { type: "mergeTableCells" }, disabledWhen: "cannotMergeCells" },
52
+ { key: "table-split-cell", label: "Split cell", icon: "splitCells", command: { type: "splitTableCell" } },
53
+ { key: "table-delete", label: "Delete table", icon: "delete", command: { type: "deleteTable" } },
54
+ { key: "table-undo", label: "Undo", icon: "undo", command: { type: "undo" }, disabledWhen: "cannotUndo" },
55
+ { key: "table-redo", label: "Redo", icon: "redo", command: { type: "redo" }, disabledWhen: "cannotRedo" },
56
+ { key: "table-keyboard", label: "Dismiss keyboard", icon: "keyboard", action: "dismissKeyboard" },
57
+ ];
@@ -0,0 +1,17 @@
1
+ import { type GlassColorScheme } from "expo-glass-effect";
2
+ import type { ReactNode } from "react";
3
+ import { type StyleProp, type ViewStyle } from "react-native";
4
+ import type { OpenEditorNativeTheme } from "./native-editor.js";
5
+ export type OpenEditorNativeToolbarSurfaceProps = {
6
+ active?: boolean;
7
+ children: ReactNode;
8
+ colorScheme?: GlassColorScheme;
9
+ style?: StyleProp<ViewStyle>;
10
+ theme?: OpenEditorNativeTheme;
11
+ };
12
+ /**
13
+ * The platform material surrounding OpenEditor's keyboard toolbar.
14
+ * iOS uses the system glass implementation when available; other runtimes
15
+ * receive the same geometry with a theme-aware opaque fallback.
16
+ */
17
+ export declare function OpenEditorNativeToolbarSurface({ active, children, colorScheme, style, theme, }: OpenEditorNativeToolbarSurfaceProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { GlassView, isGlassEffectAPIAvailable, } from "expo-glass-effect";
3
+ import { Platform, StyleSheet, View, } from "react-native";
4
+ import { OPENEDITOR_NATIVE_TOOLBAR_HEIGHT, OPENEDITOR_NATIVE_TOOLBAR_RADIUS, } from "./native-layout.js";
5
+ /**
6
+ * The platform material surrounding OpenEditor's keyboard toolbar.
7
+ * iOS uses the system glass implementation when available; other runtimes
8
+ * receive the same geometry with a theme-aware opaque fallback.
9
+ */
10
+ export function OpenEditorNativeToolbarSurface({ active = true, children, colorScheme = "auto", style, theme, }) {
11
+ if (Platform.OS === "ios" && isGlassEffectAPIAvailable()) {
12
+ return (_jsx(GlassView, { colorScheme: colorScheme, glassEffectStyle: active ? "regular" : "none", isInteractive: true, style: [styles.surface, style], children: children }));
13
+ }
14
+ return (_jsx(View, { style: [
15
+ styles.surface,
16
+ styles.fallback,
17
+ {
18
+ backgroundColor: theme?.surfaceRaised ?? theme?.surfaceMuted ?? theme?.surface ?? "#ffffff",
19
+ borderColor: theme?.border ?? "#e5e7eb",
20
+ },
21
+ style,
22
+ ], children: children }));
23
+ }
24
+ const styles = StyleSheet.create({
25
+ surface: {
26
+ borderRadius: OPENEDITOR_NATIVE_TOOLBAR_RADIUS,
27
+ height: OPENEDITOR_NATIVE_TOOLBAR_HEIGHT,
28
+ overflow: "hidden",
29
+ },
30
+ fallback: {
31
+ borderWidth: StyleSheet.hairlineWidth,
32
+ },
33
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/native",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -15,7 +15,8 @@
15
15
  "access": "public"
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "plugin.cjs"
19
20
  ],
20
21
  "exports": {
21
22
  ".": {
@@ -23,28 +24,26 @@
23
24
  "types": "./dist/index.d.ts",
24
25
  "import": "./dist/index.js"
25
26
  },
27
+ "./plugin": "./plugin.cjs",
26
28
  "./package.json": "./package.json"
27
29
  },
28
30
  "dependencies": {
29
- "@expo/ui": "~56.0.18",
30
- "@expo/vector-icons": "15.0.3",
31
- "@openeditor/core": "0.0.33",
32
- "@openeditor/exporters": "0.0.33",
33
- "@openeditor/extensions": "0.0.33",
34
- "emojibase": "17.0.0",
35
- "emojibase-data": "16.0.2",
36
- "react": "19.2.3",
37
- "react-native": "0.85.3"
31
+ "@expo/config-plugins": "57.0.6",
32
+ "@expo/ui": "~57.0.7",
33
+ "@hugeicons/react-native": "^1.0.15",
34
+ "@openeditor/core": "0.0.35",
35
+ "@openeditor/embedded-runtime": "0.0.35",
36
+ "@openeditor/embedded-surface": "0.0.35",
37
+ "@openeditor/icons": "0.0.35"
38
38
  },
39
39
  "peerDependencies": {
40
- "@openeditor/react-native-prose-editor": "0.0.33",
41
- "react-native-keyboard-controller": ">=1.21",
40
+ "expo": ">=57",
42
41
  "react": ">=19",
43
- "react-native": ">=0.85"
44
- },
45
- "peerDependenciesMeta": {
46
- "react-native-keyboard-controller": {
47
- "optional": true
48
- }
42
+ "react-native": ">=0.85",
43
+ "expo-glass-effect": ">=57",
44
+ "react-native-keyboard-controller": ">=1.21",
45
+ "react-native-reanimated": ">=3",
46
+ "react-native-svg": ">=15",
47
+ "react-native-webview": ">=13"
49
48
  }
50
49
  }
package/plugin.cjs ADDED
@@ -0,0 +1,79 @@
1
+ const { withAppDelegate } = require("@expo/config-plugins");
2
+
3
+ const WEBKIT_IMPORT = "import WebKit\n";
4
+ const OBSERVER_PROPERTY =
5
+ " private var openEditorKeyboardWillShowObserver: NSObjectProtocol?\n";
6
+ const OBSERVER_SETUP = ` openEditorKeyboardWillShowObserver = NotificationCenter.default.addObserver(
7
+ forName: UIResponder.keyboardWillShowNotification,
8
+ object: nil,
9
+ queue: .main
10
+ ) { [weak self] _ in
11
+ guard let rootView = self?.window?.rootViewController?.view else { return }
12
+ self?.enableOpenEditorInteractiveKeyboardDismissal(in: rootView)
13
+ }
14
+
15
+ `;
16
+ const DISMISSAL_METHOD = ` deinit {
17
+ if let openEditorKeyboardWillShowObserver {
18
+ NotificationCenter.default.removeObserver(openEditorKeyboardWillShowObserver)
19
+ }
20
+ }
21
+
22
+ private func enableOpenEditorInteractiveKeyboardDismissal(in view: UIView) {
23
+ if let webView = view as? WKWebView {
24
+ webView.scrollView.keyboardDismissMode = .interactive
25
+ }
26
+ view.subviews.forEach { enableOpenEditorInteractiveKeyboardDismissal(in: $0) }
27
+ }
28
+
29
+ `;
30
+
31
+ const applyOpenEditorKeyboardDismissal = (contents) => {
32
+ for (const anchor of [
33
+ "import React\n",
34
+ "class AppDelegate: ExpoAppDelegate {\n",
35
+ " return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n",
36
+ " // Linking API\n",
37
+ ]) {
38
+ if (!contents.includes(anchor)) {
39
+ throw new Error(
40
+ `OpenEditor's Expo plugin could not find ${JSON.stringify(anchor)} in AppDelegate.swift.`,
41
+ );
42
+ }
43
+ }
44
+
45
+ let next = contents;
46
+ if (!next.includes("import WebKit")) {
47
+ next = next.replace("import React\n", `import React\n${WEBKIT_IMPORT}`);
48
+ }
49
+ if (!next.includes("openEditorKeyboardWillShowObserver")) {
50
+ next = next.replace(
51
+ "class AppDelegate: ExpoAppDelegate {\n",
52
+ `class AppDelegate: ExpoAppDelegate {\n${OBSERVER_PROPERTY}`,
53
+ );
54
+ next = next.replace(
55
+ " return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n",
56
+ `${OBSERVER_SETUP} return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n`,
57
+ );
58
+ next = next.replace(
59
+ " // Linking API\n",
60
+ `${DISMISSAL_METHOD} // Linking API\n`,
61
+ );
62
+ }
63
+ return next;
64
+ };
65
+
66
+ const withOpenEditorNative = (config) =>
67
+ withAppDelegate(config, (mod) => {
68
+ if (mod.modResults.language !== "swift") {
69
+ throw new Error("OpenEditor's Expo plugin requires a Swift AppDelegate.");
70
+ }
71
+ mod.modResults.contents = applyOpenEditorKeyboardDismissal(
72
+ mod.modResults.contents,
73
+ );
74
+ return mod;
75
+ });
76
+
77
+ module.exports = withOpenEditorNative;
78
+ module.exports.applyOpenEditorKeyboardDismissal =
79
+ applyOpenEditorKeyboardDismissal;
@@ -1,7 +0,0 @@
1
- import { type OpenEditorDocument, type OpenEditorDocumentMeta, type ProseMirrorDocument, type ProseMirrorNode } from "@openeditor/core";
2
- export declare const sanitizeNativeEditorDocument: (document: OpenEditorDocument, meta?: OpenEditorDocumentMeta | undefined) => OpenEditorDocument;
3
- export declare const toNativeEditorNode: (node: ProseMirrorNode) => ProseMirrorNode;
4
- export declare const fromNativeEditorNode: (node: ProseMirrorNode) => ProseMirrorNode;
5
- export declare const toNativeEditorDocument: (document: OpenEditorDocument) => ProseMirrorDocument;
6
- export declare const fromNativeEditorDocument: (document: ProseMirrorDocument, meta?: OpenEditorDocumentMeta) => OpenEditorDocument;
7
- export declare const createNativeEditorDocument: (content: ProseMirrorNode[], meta?: OpenEditorDocumentMeta) => ProseMirrorDocument;