@openeditor/native 0.0.24 → 0.0.25
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 +25 -0
- package/dist/emoji-button.d.ts +8 -0
- package/dist/emoji-button.js +14 -0
- package/dist/emoji-data.d.ts +44 -0
- package/dist/emoji-data.js +50 -0
- package/dist/emoji-picker.d.ts +12 -0
- package/dist/emoji-picker.js +67 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +156 -22
- package/dist/toolbar.d.ts +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -8,8 +8,13 @@ Pass `attachmentRuntime` to use the system picker and host storage lifecycle. Th
|
|
|
8
8
|
|
|
9
9
|
- `OpenEditorNative` as the primary editing surface
|
|
10
10
|
- `OpenEditorNativeViewer` for read-only rendering
|
|
11
|
+
- `OpenEditorNativeEmojiPicker` for standalone native emoji selection
|
|
11
12
|
- native document bridge helpers where renderer adaptation is required
|
|
12
13
|
|
|
14
|
+
Callout and Page emoji are editable in `OpenEditorNative`: pressing the emoji
|
|
15
|
+
opens the native picker and commits the selected Unicode value through editor
|
|
16
|
+
history. Inline emoji continue to use the device keyboard.
|
|
17
|
+
|
|
13
18
|
## Required peers
|
|
14
19
|
|
|
15
20
|
- `@openeditor/react-native-prose-editor`
|
|
@@ -33,6 +38,26 @@ export function Example() {
|
|
|
33
38
|
}
|
|
34
39
|
```
|
|
35
40
|
|
|
41
|
+
## Standalone emoji picker
|
|
42
|
+
|
|
43
|
+
The editor wires this automatically for callouts and pages. Hosts can also use
|
|
44
|
+
the same picker for page headers or other product UI:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { OpenEditorNativeEmojiPicker } from "@openeditor/native";
|
|
48
|
+
|
|
49
|
+
<OpenEditorNativeEmojiPicker
|
|
50
|
+
visible={pickerOpen}
|
|
51
|
+
onEmojiSelect={setPageIcon}
|
|
52
|
+
onRequestClose={() => setPickerOpen(false)}
|
|
53
|
+
/>;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The native picker shares OpenEditor’s Emojibase dataset with the Frimousse web
|
|
57
|
+
picker, while using a platform-native medium bottom sheet and native controls
|
|
58
|
+
for scrolling, search, and touch handling. Long-press an emoji that supports
|
|
59
|
+
skin tones to choose a variant from the platform-native context menu.
|
|
60
|
+
|
|
36
61
|
## Internal notes
|
|
37
62
|
|
|
38
63
|
- The native prose editor engine remains an implementation detail behind this package.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type OpenEditorNativeEmoji } from "./emoji-data.js";
|
|
2
|
+
type EmojiButtonProps = {
|
|
3
|
+
item: OpenEditorNativeEmoji;
|
|
4
|
+
size: number;
|
|
5
|
+
onSelect: (emoji: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare const EmojiButton: (props: EmojiButtonProps) => import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MenuView } from "@expo/ui/community/menu";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Pressable, Text } from "react-native";
|
|
5
|
+
import { emojiForNativeSkinTone, openEditorNativeEmojiSkinToneOptions, } from "./emoji-data.js";
|
|
6
|
+
const EmojiTrigger = ({ item, size, onSelect }) => (_jsx(Pressable, { accessibilityLabel: item.emoji, accessibilityRole: "button", onPress: () => onSelect(item.emoji), style: { alignItems: "center", height: 44, justifyContent: "center", width: size }, children: _jsx(Text, { style: { fontSize: 27 }, children: item.emoji }) }));
|
|
7
|
+
const ToneEmojiButton = ({ item, size, onSelect }) => {
|
|
8
|
+
const actions = useMemo(() => openEditorNativeEmojiSkinToneOptions.map((option) => {
|
|
9
|
+
const emoji = emojiForNativeSkinTone(item, option.tone);
|
|
10
|
+
return { id: emoji, title: `${emoji} ${option.label}` };
|
|
11
|
+
}), [item]);
|
|
12
|
+
return (_jsx(MenuView, { actions: actions, onPressAction: ({ nativeEvent }) => onSelect(nativeEvent.event), shouldOpenOnLongPress: true, style: { height: 44, width: size }, children: _jsx(EmojiTrigger, { item: item, onSelect: onSelect, size: size }) }));
|
|
13
|
+
};
|
|
14
|
+
export const EmojiButton = (props) => (props.item.skins?.length ? _jsx(ToneEmojiButton, { ...props }) : _jsx(EmojiTrigger, { ...props }));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type OpenEditorNativeEmojiSkinTone = 0 | 1 | 2 | 3 | 4 | 5;
|
|
2
|
+
export type OpenEditorNativeEmoji = {
|
|
3
|
+
emoji: string;
|
|
4
|
+
label: string;
|
|
5
|
+
search: string;
|
|
6
|
+
group: number;
|
|
7
|
+
order: number;
|
|
8
|
+
skins?: readonly {
|
|
9
|
+
emoji: string;
|
|
10
|
+
tone?: number;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export type OpenEditorNativeEmojiSection = {
|
|
14
|
+
key: string;
|
|
15
|
+
title: string;
|
|
16
|
+
data: readonly OpenEditorNativeEmoji[];
|
|
17
|
+
};
|
|
18
|
+
export declare const openEditorNativeEmojiSkinToneOptions: readonly [{
|
|
19
|
+
readonly tone: 0;
|
|
20
|
+
readonly emoji: "✋";
|
|
21
|
+
readonly label: "Default skin tone";
|
|
22
|
+
}, {
|
|
23
|
+
readonly tone: 1;
|
|
24
|
+
readonly emoji: "✋🏻";
|
|
25
|
+
readonly label: "Light skin tone";
|
|
26
|
+
}, {
|
|
27
|
+
readonly tone: 2;
|
|
28
|
+
readonly emoji: "✋🏼";
|
|
29
|
+
readonly label: "Medium-light skin tone";
|
|
30
|
+
}, {
|
|
31
|
+
readonly tone: 3;
|
|
32
|
+
readonly emoji: "✋🏽";
|
|
33
|
+
readonly label: "Medium skin tone";
|
|
34
|
+
}, {
|
|
35
|
+
readonly tone: 4;
|
|
36
|
+
readonly emoji: "✋🏾";
|
|
37
|
+
readonly label: "Medium-dark skin tone";
|
|
38
|
+
}, {
|
|
39
|
+
readonly tone: 5;
|
|
40
|
+
readonly emoji: "✋🏿";
|
|
41
|
+
readonly label: "Dark skin tone";
|
|
42
|
+
}];
|
|
43
|
+
export declare const emojiForNativeSkinTone: (item: OpenEditorNativeEmoji, skinTone: OpenEditorNativeEmojiSkinTone) => string;
|
|
44
|
+
export declare const getOpenEditorNativeEmojiSections: (query?: string) => readonly OpenEditorNativeEmojiSection[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import emojiDataJson from "emojibase-data/en/data.json";
|
|
2
|
+
import emojiMessagesJson from "emojibase-data/en/messages.json";
|
|
3
|
+
const normalizeSearch = (value) => value.toLocaleLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").trim();
|
|
4
|
+
const rawEmojiData = emojiDataJson;
|
|
5
|
+
const rawMessages = emojiMessagesJson;
|
|
6
|
+
const groupMessages = new Map(rawMessages.groups.map((group) => [group.order, { key: group.key, title: group.message }]));
|
|
7
|
+
const nativeEmoji = rawEmojiData
|
|
8
|
+
.filter((item) => typeof item.group === "number" && item.group !== 2)
|
|
9
|
+
.map((item) => ({
|
|
10
|
+
emoji: item.emoji,
|
|
11
|
+
label: item.label,
|
|
12
|
+
search: normalizeSearch([item.label, ...(item.tags ?? [])].join(" ")),
|
|
13
|
+
group: item.group,
|
|
14
|
+
order: item.order ?? Number.MAX_SAFE_INTEGER,
|
|
15
|
+
skins: item.skins,
|
|
16
|
+
}))
|
|
17
|
+
.sort((left, right) => left.order - right.order);
|
|
18
|
+
export const openEditorNativeEmojiSkinToneOptions = [
|
|
19
|
+
{ tone: 0, emoji: "✋", label: "Default skin tone" },
|
|
20
|
+
{ tone: 1, emoji: "✋🏻", label: "Light skin tone" },
|
|
21
|
+
{ tone: 2, emoji: "✋🏼", label: "Medium-light skin tone" },
|
|
22
|
+
{ tone: 3, emoji: "✋🏽", label: "Medium skin tone" },
|
|
23
|
+
{ tone: 4, emoji: "✋🏾", label: "Medium-dark skin tone" },
|
|
24
|
+
{ tone: 5, emoji: "✋🏿", label: "Dark skin tone" },
|
|
25
|
+
];
|
|
26
|
+
export const emojiForNativeSkinTone = (item, skinTone) => {
|
|
27
|
+
if (skinTone === 0)
|
|
28
|
+
return item.emoji;
|
|
29
|
+
return item.skins?.find((skin) => skin.tone === skinTone)?.emoji ?? item.emoji;
|
|
30
|
+
};
|
|
31
|
+
export const getOpenEditorNativeEmojiSections = (query = "") => {
|
|
32
|
+
const normalizedQuery = normalizeSearch(query);
|
|
33
|
+
const groups = new Map();
|
|
34
|
+
for (const item of nativeEmoji) {
|
|
35
|
+
if (normalizedQuery && !item.search.includes(normalizedQuery))
|
|
36
|
+
continue;
|
|
37
|
+
const group = groups.get(item.group);
|
|
38
|
+
if (group)
|
|
39
|
+
group.push(item);
|
|
40
|
+
else
|
|
41
|
+
groups.set(item.group, [item]);
|
|
42
|
+
}
|
|
43
|
+
return [...groups.entries()]
|
|
44
|
+
.sort(([left], [right]) => left - right)
|
|
45
|
+
.map(([group, data]) => ({
|
|
46
|
+
key: groupMessages.get(group)?.key ?? String(group),
|
|
47
|
+
title: groupMessages.get(group)?.title ?? "Emoji",
|
|
48
|
+
data,
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type OpenEditorNativeEmojiPickerTheme = {
|
|
2
|
+
surfaceMuted?: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
muted?: string;
|
|
5
|
+
};
|
|
6
|
+
export type OpenEditorNativeEmojiPickerProps = {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
onEmojiSelect: (emoji: string) => void;
|
|
9
|
+
onRequestClose: () => void;
|
|
10
|
+
theme?: OpenEditorNativeEmojiPickerTheme;
|
|
11
|
+
};
|
|
12
|
+
export declare const OpenEditorNativeEmojiPicker: ({ visible, onEmojiSelect, onRequestClose, theme, }: OpenEditorNativeEmojiPickerProps) => import("react").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BottomSheet, RNHostView } from "@expo/ui";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { ScrollView, Text, TextInput, useWindowDimensions, View, } from "react-native";
|
|
5
|
+
import { getOpenEditorNativeEmojiSections, } from "./emoji-data.js";
|
|
6
|
+
import { EmojiButton } from "./emoji-button.js";
|
|
7
|
+
const toRows = (items, columns = 8) => {
|
|
8
|
+
const rows = [];
|
|
9
|
+
for (let index = 0; index < items.length; index += columns) {
|
|
10
|
+
rows.push(items.slice(index, index + columns));
|
|
11
|
+
}
|
|
12
|
+
return rows;
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_THEME = {
|
|
15
|
+
surfaceMuted: "#eeeeee",
|
|
16
|
+
text: "#171717",
|
|
17
|
+
muted: "#737373",
|
|
18
|
+
};
|
|
19
|
+
export const OpenEditorNativeEmojiPicker = ({ visible, onEmojiSelect, onRequestClose, theme, }) => {
|
|
20
|
+
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
21
|
+
const [query, setQuery] = useState("");
|
|
22
|
+
const resolvedTheme = { ...DEFAULT_THEME, ...theme };
|
|
23
|
+
const nativeScrollHeight = Math.max(240, Math.floor(windowHeight * 0.5) - 32);
|
|
24
|
+
const contentWidth = Math.max(240, Math.min(windowWidth - 32, 520));
|
|
25
|
+
const emojiButtonWidth = contentWidth / 8;
|
|
26
|
+
const sections = useMemo(() => getOpenEditorNativeEmojiSections(query).map((section) => ({
|
|
27
|
+
...section,
|
|
28
|
+
data: toRows(section.data),
|
|
29
|
+
})), [query]);
|
|
30
|
+
const close = () => {
|
|
31
|
+
setQuery("");
|
|
32
|
+
onRequestClose();
|
|
33
|
+
};
|
|
34
|
+
const selectEmoji = (selectedEmoji) => {
|
|
35
|
+
onEmojiSelect(selectedEmoji);
|
|
36
|
+
close();
|
|
37
|
+
};
|
|
38
|
+
return (_jsx(BottomSheet, { isPresented: visible, onDismiss: close, snapPoints: ["half"], children: _jsx(RNHostView, { matchContents: true, children: _jsxs(ScrollView, { contentContainerStyle: { paddingBottom: 32 }, keyboardShouldPersistTaps: "handled", style: { height: nativeScrollHeight, width: contentWidth }, children: [_jsx(TextInput, { autoCapitalize: "none", autoCorrect: false, onChangeText: setQuery, placeholder: "Search emoji\u2026", placeholderTextColor: resolvedTheme.muted, returnKeyType: "search", style: {
|
|
39
|
+
backgroundColor: resolvedTheme.surfaceMuted,
|
|
40
|
+
borderRadius: 10,
|
|
41
|
+
color: resolvedTheme.text,
|
|
42
|
+
fontSize: 16,
|
|
43
|
+
height: 44,
|
|
44
|
+
paddingHorizontal: 12,
|
|
45
|
+
width: contentWidth,
|
|
46
|
+
} }), sections.length === 0 ? (_jsx(Text, { style: {
|
|
47
|
+
color: resolvedTheme.muted,
|
|
48
|
+
fontSize: 15,
|
|
49
|
+
height: 160,
|
|
50
|
+
paddingTop: 48,
|
|
51
|
+
textAlign: "center",
|
|
52
|
+
width: contentWidth,
|
|
53
|
+
}, children: "No emoji found." })) : sections.map((section) => (_jsxs(View, { style: { width: contentWidth }, children: [_jsx(Text, { style: {
|
|
54
|
+
color: resolvedTheme.muted,
|
|
55
|
+
fontSize: 12,
|
|
56
|
+
fontWeight: "600",
|
|
57
|
+
height: 32,
|
|
58
|
+
paddingHorizontal: 8,
|
|
59
|
+
paddingVertical: 8,
|
|
60
|
+
width: contentWidth,
|
|
61
|
+
}, children: section.title }), section.data.map((row, rowIndex) => (_jsx(View, { style: {
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
flexDirection: "row",
|
|
64
|
+
height: 44,
|
|
65
|
+
width: contentWidth,
|
|
66
|
+
}, children: row.map((item) => (_jsx(EmojiButton, { item: item, onSelect: selectEmoji, size: emojiButtonWidth }, item.emoji))) }, `${section.key}-${rowIndex}`)))] }, section.key)))] }) }) }));
|
|
67
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, type ProseMi
|
|
|
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
|
+
export { OpenEditorNativeEmojiPicker, type OpenEditorNativeEmojiPickerProps, type OpenEditorNativeEmojiPickerTheme, } from "./emoji-picker.js";
|
|
6
|
+
export { emojiForNativeSkinTone, getOpenEditorNativeEmojiSections, openEditorNativeEmojiSkinToneOptions, type OpenEditorNativeEmoji, type OpenEditorNativeEmojiSection, type OpenEditorNativeEmojiSkinTone, } from "./emoji-data.js";
|
|
5
7
|
export type OpenEditorNativeProps = {
|
|
6
8
|
document?: OpenEditorDocument;
|
|
7
9
|
editable?: boolean;
|
|
@@ -44,12 +46,14 @@ export type OpenEditorNativeTheme = {
|
|
|
44
46
|
background: string;
|
|
45
47
|
surface: string;
|
|
46
48
|
surfaceMuted: string;
|
|
49
|
+
blockSurface?: string;
|
|
47
50
|
text: string;
|
|
48
51
|
textSoft?: string;
|
|
49
52
|
muted: string;
|
|
50
53
|
placeholder?: string;
|
|
51
54
|
border: string;
|
|
52
55
|
borderStrong?: string;
|
|
56
|
+
structuralLine?: string;
|
|
53
57
|
accent: string;
|
|
54
58
|
accentText: string;
|
|
55
59
|
accentStrong?: string;
|
package/dist/index.js
CHANGED
|
@@ -7,23 +7,30 @@ import { resolveSelectedTopLevelIndex } from "./selection.js";
|
|
|
7
7
|
import { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, openEditorNativeToolbarActionKeys, } from "./toolbar.js";
|
|
8
8
|
import { normalizeNativeThemeColor } from "./theme.js";
|
|
9
9
|
export { normalizeNativeThemeColor } from "./theme.js";
|
|
10
|
+
import { OpenEditorNativeEmojiPicker } from "./emoji-picker.js";
|
|
11
|
+
export { OpenEditorNativeEmojiPicker, } from "./emoji-picker.js";
|
|
12
|
+
export { emojiForNativeSkinTone, getOpenEditorNativeEmojiSections, openEditorNativeEmojiSkinToneOptions, } from "./emoji-data.js";
|
|
10
13
|
import { defaultBlockRegistry } from "@openeditor/extensions";
|
|
14
|
+
const defaultLightNeutralSurface = "#eeeeee";
|
|
15
|
+
const defaultLightStructuralLine = "#d4d4d4";
|
|
11
16
|
const defaultNativeTheme = {
|
|
12
17
|
background: "#fafafa",
|
|
13
18
|
surface: "#ffffff",
|
|
14
|
-
surfaceMuted:
|
|
19
|
+
surfaceMuted: defaultLightNeutralSurface,
|
|
20
|
+
blockSurface: defaultLightNeutralSurface,
|
|
15
21
|
text: "#171717",
|
|
16
22
|
textSoft: "#262626",
|
|
17
23
|
muted: "#737373",
|
|
18
24
|
placeholder: "#a3a3a3",
|
|
19
|
-
border:
|
|
20
|
-
borderStrong:
|
|
21
|
-
|
|
25
|
+
border: defaultLightNeutralSurface,
|
|
26
|
+
borderStrong: defaultLightStructuralLine,
|
|
27
|
+
structuralLine: defaultLightStructuralLine,
|
|
28
|
+
accent: defaultLightNeutralSurface,
|
|
22
29
|
accentText: "#171717",
|
|
23
30
|
accentStrong: "#171717",
|
|
24
31
|
primary: "#171717",
|
|
25
32
|
primaryText: "#ffffff",
|
|
26
|
-
codeBackground:
|
|
33
|
+
codeBackground: defaultLightNeutralSurface,
|
|
27
34
|
codeText: "#171717",
|
|
28
35
|
debugBackground: "#171717",
|
|
29
36
|
debugText: "#d4d4d4",
|
|
@@ -186,6 +193,10 @@ const openEditorNativeSchema = {
|
|
|
186
193
|
};
|
|
187
194
|
const toNativeEditorTheme = (theme) => ({
|
|
188
195
|
backgroundColor: normalizeNativeThemeColor(theme.background),
|
|
196
|
+
blockSurfaceColor: normalizeNativeThemeColor(theme.blockSurface),
|
|
197
|
+
structuralLineColor: normalizeNativeThemeColor(theme.structuralLine),
|
|
198
|
+
accentStrongColor: normalizeNativeThemeColor(theme.accentStrong),
|
|
199
|
+
accentStrongTextColor: normalizeNativeThemeColor(theme.primaryText),
|
|
189
200
|
borderRadius: 0,
|
|
190
201
|
placeholderColor: normalizeNativeThemeColor(theme.placeholder),
|
|
191
202
|
contentInsets: {
|
|
@@ -222,7 +233,7 @@ const toNativeEditorTheme = (theme) => ({
|
|
|
222
233
|
itemSpacing: 6,
|
|
223
234
|
},
|
|
224
235
|
blockquote: {
|
|
225
|
-
borderColor: normalizeNativeThemeColor(theme.
|
|
236
|
+
borderColor: normalizeNativeThemeColor(theme.structuralLine),
|
|
226
237
|
borderWidth: 3,
|
|
227
238
|
text: {
|
|
228
239
|
color: normalizeNativeThemeColor(theme.textSoft),
|
|
@@ -242,14 +253,14 @@ const toNativeEditorTheme = (theme) => ({
|
|
|
242
253
|
},
|
|
243
254
|
},
|
|
244
255
|
table: {
|
|
245
|
-
cellBackgroundColor: normalizeNativeThemeColor(theme.
|
|
246
|
-
headerBackgroundColor: normalizeNativeThemeColor(theme.
|
|
247
|
-
borderColor: normalizeNativeThemeColor(theme.
|
|
256
|
+
cellBackgroundColor: normalizeNativeThemeColor(theme.blockSurface),
|
|
257
|
+
headerBackgroundColor: normalizeNativeThemeColor(theme.blockSurface),
|
|
258
|
+
borderColor: normalizeNativeThemeColor(theme.structuralLine),
|
|
248
259
|
selectionBackgroundColor: normalizeNativeThemeColor(theme.accent),
|
|
249
260
|
selectionBorderColor: normalizeNativeThemeColor(theme.accentStrong),
|
|
250
261
|
},
|
|
251
262
|
horizontalRule: {
|
|
252
|
-
color: normalizeNativeThemeColor(theme.
|
|
263
|
+
color: normalizeNativeThemeColor(theme.structuralLine),
|
|
253
264
|
thickness: 1,
|
|
254
265
|
verticalMargin: 16,
|
|
255
266
|
},
|
|
@@ -298,6 +309,29 @@ const isSafeInputUrl = (value, allowDataImage = false) => {
|
|
|
298
309
|
return true;
|
|
299
310
|
};
|
|
300
311
|
const documentsMatch = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
312
|
+
const attachmentTypeLabel = (mimeType, name) => {
|
|
313
|
+
const subtype = mimeType?.split("/").at(-1)?.split("+")[0]?.trim();
|
|
314
|
+
const extension = name.includes(".") ? name.split(".").at(-1)?.trim() : undefined;
|
|
315
|
+
return (subtype || extension || "FILE").slice(0, 4).toUpperCase();
|
|
316
|
+
};
|
|
317
|
+
const formatAttachmentSize = (size) => {
|
|
318
|
+
if (size === null || !Number.isFinite(size) || size < 0)
|
|
319
|
+
return null;
|
|
320
|
+
if (size < 1024)
|
|
321
|
+
return `${Math.round(size)} B`;
|
|
322
|
+
if (size < 1024 * 1024)
|
|
323
|
+
return `${(size / 1024).toFixed(size < 10 * 1024 ? 1 : 0)} KB`;
|
|
324
|
+
return `${(size / (1024 * 1024)).toFixed(size < 10 * 1024 * 1024 ? 1 : 0)} MB`;
|
|
325
|
+
};
|
|
326
|
+
const NativeToggleListItem = ({ children, initiallyOpen, theme, }) => {
|
|
327
|
+
const [open, setOpen] = useState(initiallyOpen);
|
|
328
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
329
|
+
return (_jsxs(View, { style: styles.viewerListRow, children: [_jsx(Pressable, { accessibilityLabel: open ? "Collapse toggle item" : "Expand toggle item", accessibilityRole: "button", hitSlop: 8, onPress: () => setOpen((current) => !current), style: styles.viewerListControl, children: _jsx(View, { style: [
|
|
330
|
+
styles.viewerToggleCaret,
|
|
331
|
+
{ borderLeftColor: theme.muted },
|
|
332
|
+
open ? styles.viewerToggleCaretOpen : null,
|
|
333
|
+
] }) }), _jsx(View, { style: styles.viewerListContent, children: open ? childArray : childArray.slice(0, 1) })] }));
|
|
334
|
+
};
|
|
301
335
|
const nativeSeedDocument = createDocument([
|
|
302
336
|
textBlock("heading", "OpenEditor Native", { level: 1 }),
|
|
303
337
|
textBlock("paragraph", "This native surface is intentionally limited to engine-owned blocks for now."),
|
|
@@ -355,28 +389,29 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachm
|
|
|
355
389
|
case "bulletList":
|
|
356
390
|
case "orderedList":
|
|
357
391
|
case "taskList":
|
|
392
|
+
case "toggleList":
|
|
358
393
|
return _jsx(View, { style: { marginBottom: 12 }, children: children }, key);
|
|
359
394
|
case "blockquote":
|
|
360
|
-
return _jsx(View, { style: { borderLeftColor: theme.
|
|
395
|
+
return _jsx(View, { style: { borderLeftColor: theme.structuralLine, borderLeftWidth: 3, marginBottom: 12, paddingLeft: 12 }, children: children }, key);
|
|
361
396
|
case "codeBlock":
|
|
362
397
|
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
398
|
case "callout": {
|
|
364
399
|
const emoji = normalizeEmoji(node.attrs?.emoji, DEFAULT_CALLOUT_EMOJI);
|
|
365
|
-
return _jsxs(View, { style: { backgroundColor: theme.
|
|
400
|
+
return _jsxs(View, { style: { backgroundColor: theme.blockSurface, borderRadius: 12, 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
401
|
}
|
|
367
402
|
case "divider":
|
|
368
|
-
return _jsx(View, { style: { backgroundColor: theme.
|
|
403
|
+
return _jsx(View, { style: { backgroundColor: theme.structuralLine, height: 1, marginVertical: 16 } }, key);
|
|
369
404
|
case "image":
|
|
370
405
|
return (_jsx(NativeImage, { source: { uri: typeof node.attrs?.src === "string" ? node.attrs.src : "" }, accessibilityLabel: typeof node.attrs?.alt === "string" ? node.attrs.alt : undefined, style: { borderRadius: 8, height: 220, marginBottom: 12, width: "100%" }, resizeMode: "cover" }, key));
|
|
371
406
|
case "columns":
|
|
372
407
|
return _jsx(View, { style: { flexDirection: "row", gap: 12, marginBottom: 12 }, children: children }, key);
|
|
373
408
|
case "table":
|
|
374
|
-
return _jsx(View, { style: { borderColor: theme.
|
|
409
|
+
return _jsx(View, { style: { borderColor: theme.structuralLine, borderRadius: 8, borderWidth: 1, marginBottom: 12, overflow: "hidden" }, children: children }, key);
|
|
375
410
|
case "page": {
|
|
376
411
|
const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
|
|
377
412
|
const title = node.content?.map((child) => child.text ?? "").join("") || "Untitled";
|
|
378
413
|
const icon = normalizeEmoji(node.attrs?.icon, DEFAULT_PAGE_EMOJI);
|
|
379
|
-
const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: {
|
|
414
|
+
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" })] });
|
|
380
415
|
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);
|
|
381
416
|
}
|
|
382
417
|
case "attachment": {
|
|
@@ -387,20 +422,29 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachm
|
|
|
387
422
|
size: typeof node.attrs?.size === "number" ? node.attrs.size : null,
|
|
388
423
|
url: typeof node.attrs?.url === "string" ? node.attrs.url : null,
|
|
389
424
|
};
|
|
390
|
-
const
|
|
391
|
-
|
|
425
|
+
const size = formatAttachmentSize(snapshot.size);
|
|
426
|
+
const type = attachmentTypeLabel(snapshot.mimeType, snapshot.name);
|
|
427
|
+
const content = _jsxs(_Fragment, { children: [_jsx(View, { style: [styles.viewerAttachmentIcon, { backgroundColor: theme.blockSurface }], children: _jsx(Text, { style: [styles.viewerAttachmentType, { color: theme.muted }], children: type }) }), _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, marginTop: 3 }, children: [size, type].filter(Boolean).join(" · ") })] }), _jsx(Text, { style: { color: theme.muted }, children: "\u2192" })] });
|
|
428
|
+
return attachmentRuntime?.openAttachment ? _jsx(Pressable, { accessibilityLabel: `Open ${snapshot.name}`, accessibilityRole: "button", onPress: () => void attachmentRuntime.openAttachment?.(snapshot), style: [styles.viewerAttachment, { backgroundColor: theme.surface, borderColor: theme.structuralLine }], children: content }, key) : _jsx(View, { style: [styles.viewerAttachment, { backgroundColor: theme.surface, borderColor: theme.structuralLine }], children: content }, key);
|
|
392
429
|
}
|
|
393
430
|
default:
|
|
394
431
|
if (node.type === "column") {
|
|
395
432
|
return _jsx(View, { style: { flex: 1 }, children: children }, key);
|
|
396
433
|
}
|
|
397
434
|
if (node.type === "tableRow") {
|
|
398
|
-
return _jsx(View, { style: { borderBottomColor: theme.
|
|
435
|
+
return _jsx(View, { style: { borderBottomColor: theme.structuralLine, borderBottomWidth: StyleSheet.hairlineWidth, flexDirection: "row" }, children: children }, key);
|
|
399
436
|
}
|
|
400
437
|
if (node.type === "tableHeader" || node.type === "tableCell") {
|
|
401
|
-
return _jsx(View, { style: { flex: 1, padding: 10 }, children: children }, key);
|
|
438
|
+
return _jsx(View, { style: { backgroundColor: theme.blockSurface, flex: 1, padding: 10 }, children: children }, key);
|
|
439
|
+
}
|
|
440
|
+
if (node.type === "taskItem") {
|
|
441
|
+
const checked = node.attrs?.checked === true;
|
|
442
|
+
return _jsxs(View, { style: styles.viewerListRow, children: [_jsx(View, { style: [styles.viewerTaskCheckbox, { backgroundColor: checked ? theme.accentStrong : theme.blockSurface }], children: checked ? _jsx(Text, { style: [styles.viewerTaskCheck, { color: theme.primaryText }], children: "\u2713" }) : null }), _jsx(View, { style: styles.viewerListContent, children: children })] }, key);
|
|
402
443
|
}
|
|
403
|
-
if (node.type === "
|
|
444
|
+
if (node.type === "toggleListItem") {
|
|
445
|
+
return _jsx(NativeToggleListItem, { initiallyOpen: node.attrs?.open !== false, theme: theme, children: children }, key);
|
|
446
|
+
}
|
|
447
|
+
if (node.type === "listItem") {
|
|
404
448
|
return _jsxs(View, { style: { flexDirection: "row", gap: 8, marginBottom: 6 }, children: [_jsx(Text, { style: { color: theme.muted }, children: "\u2022" }), _jsx(View, { style: { flex: 1 }, children: children })] }, key);
|
|
405
449
|
}
|
|
406
450
|
return _jsx(View, { children: children }, key);
|
|
@@ -433,6 +477,7 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
|
|
|
433
477
|
const [linkRequest, setLinkRequest] = useState(null);
|
|
434
478
|
const [linkUrl, setLinkUrl] = useState("https://");
|
|
435
479
|
const [imageRequest, setImageRequest] = useState(null);
|
|
480
|
+
const [emojiRequest, setEmojiRequest] = useState(null);
|
|
436
481
|
const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
|
|
437
482
|
const [attachmentUpload, setAttachmentUpload] = useState(null);
|
|
438
483
|
const attachmentAbortRef = useRef(null);
|
|
@@ -597,9 +642,22 @@ export const OpenEditorNative = ({ document = nativeSeedDocument, editable = tru
|
|
|
597
642
|
}, onRequestImage: (request) => {
|
|
598
643
|
setImageUrl("https://placehold.co/960x420/png");
|
|
599
644
|
setImageRequest(request);
|
|
600
|
-
}, onOpenPage: onOpenPage, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
|
|
645
|
+
}, onOpenPage: onOpenPage, onRequestEmoji: setEmojiRequest, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onContentChangeJSON: (json) => {
|
|
601
646
|
syncDocument(contentFromNativeJson(json), false);
|
|
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 })
|
|
647
|
+
}, 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) => {
|
|
648
|
+
if (!emojiRequest)
|
|
649
|
+
return;
|
|
650
|
+
const attribute = emojiRequest.nodeType === "page" ? "icon" : "emoji";
|
|
651
|
+
editorRef.current?.updateNodeAttrs(emojiRequest.docPos, {
|
|
652
|
+
...emojiRequest.attrs,
|
|
653
|
+
[attribute]: emoji,
|
|
654
|
+
});
|
|
655
|
+
setEmojiRequest(null);
|
|
656
|
+
}, onRequestClose: () => setEmojiRequest(null), theme: {
|
|
657
|
+
surfaceMuted: resolvedTheme.surfaceMuted,
|
|
658
|
+
text: resolvedTheme.text,
|
|
659
|
+
muted: resolvedTheme.muted,
|
|
660
|
+
}, visible: emojiRequest !== null })] }));
|
|
603
661
|
};
|
|
604
662
|
export const OpenEditorNativeViewer = ({ document = nativeSeedDocument, theme, renderers, onOpenPage, attachmentRuntime, }) => {
|
|
605
663
|
const resolvedTheme = {
|
|
@@ -627,6 +685,82 @@ const styles = StyleSheet.create({
|
|
|
627
685
|
gap: 4,
|
|
628
686
|
padding: 16,
|
|
629
687
|
},
|
|
688
|
+
viewerListRow: {
|
|
689
|
+
alignItems: "flex-start",
|
|
690
|
+
flexDirection: "row",
|
|
691
|
+
gap: 8,
|
|
692
|
+
marginBottom: 6,
|
|
693
|
+
},
|
|
694
|
+
viewerListControl: {
|
|
695
|
+
alignItems: "center",
|
|
696
|
+
height: 24,
|
|
697
|
+
justifyContent: "center",
|
|
698
|
+
width: 20,
|
|
699
|
+
},
|
|
700
|
+
viewerListContent: {
|
|
701
|
+
flex: 1,
|
|
702
|
+
},
|
|
703
|
+
viewerToggleCaret: {
|
|
704
|
+
borderBottomColor: "transparent",
|
|
705
|
+
borderBottomWidth: 4,
|
|
706
|
+
borderLeftWidth: 6,
|
|
707
|
+
borderTopColor: "transparent",
|
|
708
|
+
borderTopWidth: 4,
|
|
709
|
+
height: 0,
|
|
710
|
+
transform: [{ rotate: "0deg" }],
|
|
711
|
+
width: 0,
|
|
712
|
+
},
|
|
713
|
+
viewerToggleCaretOpen: {
|
|
714
|
+
transform: [{ rotate: "90deg" }],
|
|
715
|
+
},
|
|
716
|
+
viewerTaskCheckbox: {
|
|
717
|
+
alignItems: "center",
|
|
718
|
+
borderRadius: 4,
|
|
719
|
+
height: 20,
|
|
720
|
+
justifyContent: "center",
|
|
721
|
+
marginTop: 2,
|
|
722
|
+
width: 20,
|
|
723
|
+
},
|
|
724
|
+
viewerTaskCheck: {
|
|
725
|
+
fontSize: 13,
|
|
726
|
+
fontWeight: "800",
|
|
727
|
+
lineHeight: 16,
|
|
728
|
+
},
|
|
729
|
+
viewerPageIcon: {
|
|
730
|
+
fontSize: 18,
|
|
731
|
+
lineHeight: 24,
|
|
732
|
+
textAlign: "center",
|
|
733
|
+
width: 24,
|
|
734
|
+
},
|
|
735
|
+
viewerPageTitle: {
|
|
736
|
+
flex: 1,
|
|
737
|
+
fontWeight: "600",
|
|
738
|
+
lineHeight: 24,
|
|
739
|
+
textDecorationLine: "underline",
|
|
740
|
+
},
|
|
741
|
+
viewerAttachment: {
|
|
742
|
+
alignItems: "center",
|
|
743
|
+
borderRadius: 12,
|
|
744
|
+
borderWidth: 1,
|
|
745
|
+
flexDirection: "row",
|
|
746
|
+
gap: 12,
|
|
747
|
+
marginBottom: 8,
|
|
748
|
+
minHeight: 68,
|
|
749
|
+
paddingHorizontal: 12,
|
|
750
|
+
paddingVertical: 10,
|
|
751
|
+
},
|
|
752
|
+
viewerAttachmentIcon: {
|
|
753
|
+
alignItems: "center",
|
|
754
|
+
borderRadius: 9,
|
|
755
|
+
height: 48,
|
|
756
|
+
justifyContent: "center",
|
|
757
|
+
width: 42,
|
|
758
|
+
},
|
|
759
|
+
viewerAttachmentType: {
|
|
760
|
+
fontSize: 9,
|
|
761
|
+
fontWeight: "800",
|
|
762
|
+
letterSpacing: 0.4,
|
|
763
|
+
},
|
|
630
764
|
nativeEditor: {
|
|
631
765
|
minHeight: 320,
|
|
632
766
|
},
|
package/dist/toolbar.d.ts
CHANGED
|
@@ -24,6 +24,6 @@ export declare const createOpenEditorNativeToolbarItems: ({ canUndo, canRedo, }?
|
|
|
24
24
|
export declare const openEditorNativeToolbarItems: readonly EditorToolbarItem[];
|
|
25
25
|
export declare const nativeToolbarParityCoverage: () => {
|
|
26
26
|
missingBlockControls: string[];
|
|
27
|
-
missingMarkControls: ("bold" | "
|
|
27
|
+
missingMarkControls: ("bold" | "link" | "strike" | "italic" | "underline" | "code")[];
|
|
28
28
|
};
|
|
29
29
|
export declare const defaultDocumentForToolbarAction: (key: string) => ProseMirrorDocument | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openeditor/native",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"repository": {
|
|
@@ -26,15 +26,18 @@
|
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@expo/ui": "~56.0.18",
|
|
29
30
|
"@expo/vector-icons": "15.0.3",
|
|
30
|
-
"@openeditor/core": "0.0.
|
|
31
|
-
"@openeditor/exporters": "0.0.
|
|
32
|
-
"@openeditor/extensions": "0.0.
|
|
31
|
+
"@openeditor/core": "0.0.25",
|
|
32
|
+
"@openeditor/exporters": "0.0.25",
|
|
33
|
+
"@openeditor/extensions": "0.0.25",
|
|
34
|
+
"emojibase": "17.0.0",
|
|
35
|
+
"emojibase-data": "16.0.2",
|
|
33
36
|
"react": "19.2.3",
|
|
34
37
|
"react-native": "0.85.3"
|
|
35
38
|
},
|
|
36
39
|
"peerDependencies": {
|
|
37
|
-
"@openeditor/react-native-prose-editor": "0.0.
|
|
40
|
+
"@openeditor/react-native-prose-editor": "0.0.25",
|
|
38
41
|
"react-native-keyboard-controller": ">=1.21",
|
|
39
42
|
"react": ">=19",
|
|
40
43
|
"react-native": ">=0.85"
|