release: prepare v0.4.5 mobile core
This commit is contained in:
1 parent
db4b8bd86c
commit
d2fc58c82d
8 files changed
+209
-92
No files matched your search
@@ -3,7 +3,7 @@
|
||||
"name": "EdgeEver",
|
||||
"slug": "edgeever",
|
||||
"scheme": "edgeever",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"orientation": "portrait",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": true,
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"android": {
|
||||
"package": "org.edgeever.mobile",
|
||||
"versionCode": 10,
|
||||
"versionCode": 11,
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edgeever/mobile",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "expo-router/entry",
|
||||
|
||||
@@ -5,6 +5,15 @@ import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import type { TiptapDoc } from "@edgeever/shared";
|
||||
import {
|
||||
MOBILE_EDITOR_ACTIVE_FLAGS,
|
||||
MOBILE_EDITOR_TOOLBAR_ACTIONS,
|
||||
getMobileEditorInputAttributes,
|
||||
getMobileEditorPlaceholder,
|
||||
getMobileEditorToolbarActionLabel,
|
||||
getMobileEditorToolbarLabel,
|
||||
type MobileEditorToolbarActionId,
|
||||
} from "@edgeever/shared/mobile-editor";
|
||||
import { useDOMImperativeHandle, type DOMImperativeFactory, type DOMProps } from "expo/dom";
|
||||
import { useCallback, useEffect, useMemo, useRef, type ReactNode, type Ref } from "react";
|
||||
|
||||
@@ -65,19 +74,12 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) {
|
||||
StarterKit,
|
||||
protectedImageExtension,
|
||||
Placeholder.configure({
|
||||
placeholder: props.locale === "en-US" ? "Start writing..." : "开始记录...",
|
||||
placeholder: getMobileEditorPlaceholder(props.locale),
|
||||
}),
|
||||
],
|
||||
content: resolveImageSources(props.content, props.baseUrl),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
autocapitalize: "sentences",
|
||||
autocomplete: "on",
|
||||
autocorrect: "on",
|
||||
class: "edgeever-editor-content",
|
||||
inputmode: "text",
|
||||
spellcheck: "true",
|
||||
},
|
||||
attributes: getMobileEditorInputAttributes("edgeever-editor-content"),
|
||||
},
|
||||
onUpdate: ({ editor: activeEditor }) => {
|
||||
if (changeTimerRef.current !== null) {
|
||||
@@ -174,9 +176,9 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) {
|
||||
const toolbarState = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor: activeEditor }) =>
|
||||
(activeEditor?.isActive("bold") ? 1 : 0) |
|
||||
(activeEditor?.isActive("bulletList") ? 8 : 0) |
|
||||
(activeEditor?.isActive("blockquote") ? 16 : 0),
|
||||
(activeEditor?.isActive("bold") ? MOBILE_EDITOR_ACTIVE_FLAGS.bold : 0) |
|
||||
(activeEditor?.isActive("bulletList") ? MOBILE_EDITOR_ACTIVE_FLAGS.bulletList : 0) |
|
||||
(activeEditor?.isActive("blockquote") ? MOBILE_EDITOR_ACTIVE_FLAGS.blockquote : 0),
|
||||
});
|
||||
|
||||
const insertImage = async () => {
|
||||
@@ -189,15 +191,34 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const toolbarIcons: Record<MobileEditorToolbarActionId, ReactNode> = {
|
||||
image: <ImagePlusIcon />,
|
||||
bold: <BoldIcon />,
|
||||
bulletList: <ListIcon />,
|
||||
blockquote: <QuoteIcon />,
|
||||
horizontalRule: <MinusIcon />,
|
||||
};
|
||||
const toolbarHandlers: Record<MobileEditorToolbarActionId, () => void> = {
|
||||
image: () => void insertImage(),
|
||||
bold: () => editor?.chain().focus().toggleBold().run(),
|
||||
bulletList: () => editor?.chain().focus().toggleBulletList().run(),
|
||||
blockquote: () => editor?.chain().focus().toggleBlockquote().run(),
|
||||
horizontalRule: () => editor?.chain().focus().setHorizontalRule().run(),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="edgeever-editor-shell">
|
||||
<style>{getEditorStyles(props.theme)}</style>
|
||||
<div aria-label={props.locale === "en-US" ? "Editor toolbar" : "编辑器工具栏"} className="edgeever-editor-toolbar" role="toolbar">
|
||||
<ToolbarButton icon={<ImagePlusIcon />} label={props.locale === "en-US" ? "Upload image" : "上传图片"} onRun={() => void insertImage()} />
|
||||
<ToolbarButton active={Boolean(toolbarState & 1)} icon={<BoldIcon />} label={props.locale === "en-US" ? "Bold" : "加粗"} onRun={() => editor?.chain().focus().toggleBold().run()} />
|
||||
<ToolbarButton active={Boolean(toolbarState & 8)} icon={<ListIcon />} label={props.locale === "en-US" ? "Bullet list" : "无序列表"} onRun={() => editor?.chain().focus().toggleBulletList().run()} />
|
||||
<ToolbarButton active={Boolean(toolbarState & 16)} icon={<QuoteIcon />} label={props.locale === "en-US" ? "Quote" : "引用"} onRun={() => editor?.chain().focus().toggleBlockquote().run()} />
|
||||
<ToolbarButton icon={<MinusIcon />} label={props.locale === "en-US" ? "Horizontal rule" : "分割线"} onRun={() => editor?.chain().focus().setHorizontalRule().run()} />
|
||||
<div aria-label={getMobileEditorToolbarLabel(props.locale)} className="edgeever-editor-toolbar" role="toolbar">
|
||||
{MOBILE_EDITOR_TOOLBAR_ACTIONS.map((action) => (
|
||||
<ToolbarButton
|
||||
key={action.id}
|
||||
active={action.activeFlag > 0 && Boolean(toolbarState & action.activeFlag)}
|
||||
icon={toolbarIcons[action.id]}
|
||||
label={getMobileEditorToolbarActionLabel(action.id, props.locale)}
|
||||
onRun={toolbarHandlers[action.id]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Bold, Check, ChevronDown, ImagePlus, List, Minus, Quote } from "lucide-react";
|
||||
import {
|
||||
MOBILE_EDITOR_TOOLBAR_ACTIONS,
|
||||
getMobileEditorToolbarActionLabel,
|
||||
type MobileEditorToolbarActionId,
|
||||
} from "@edgeever/shared/mobile-editor";
|
||||
import type { ReactNode } from "react";
|
||||
import type { NotebookMoveOption } from "@/lib/app-helpers";
|
||||
import type { MobileEditorSaveState } from "@/lib/mobile-editor-standalone";
|
||||
|
||||
@@ -46,68 +52,51 @@ export const MobileEditorToolbar = ({
|
||||
onToggleBulletList: () => void;
|
||||
onToggleBlockquote: () => void;
|
||||
onSetHorizontalRule: () => void;
|
||||
}) => (
|
||||
<div className="mobile-editor-tool-row">
|
||||
<button
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label="上传图片"
|
||||
title="上传图片"
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={onPickImage}
|
||||
>
|
||||
<ImagePlus aria-hidden="true" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label="加粗"
|
||||
title="加粗"
|
||||
aria-pressed={boldActive}
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={onToggleBold}
|
||||
>
|
||||
<Bold aria-hidden="true" size={17} strokeWidth={2.4} />
|
||||
</button>
|
||||
<button
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label="无序列表"
|
||||
title="无序列表"
|
||||
aria-pressed={bulletListActive}
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={onToggleBulletList}
|
||||
>
|
||||
<List aria-hidden="true" size={18} strokeWidth={2.2} />
|
||||
</button>
|
||||
<button
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label="引用"
|
||||
title="引用"
|
||||
aria-pressed={blockquoteActive}
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={onToggleBlockquote}
|
||||
>
|
||||
<Quote aria-hidden="true" size={17} strokeWidth={2.2} />
|
||||
</button>
|
||||
<button
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label="分割线"
|
||||
title="分割线"
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={onSetHorizontalRule}
|
||||
>
|
||||
<Minus aria-hidden="true" size={18} strokeWidth={2.4} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const icons: Record<MobileEditorToolbarActionId, ReactNode> = {
|
||||
image: <ImagePlus aria-hidden="true" size={18} strokeWidth={2} />,
|
||||
bold: <Bold aria-hidden="true" size={17} strokeWidth={2.4} />,
|
||||
bulletList: <List aria-hidden="true" size={18} strokeWidth={2.2} />,
|
||||
blockquote: <Quote aria-hidden="true" size={17} strokeWidth={2.2} />,
|
||||
horizontalRule: <Minus aria-hidden="true" size={18} strokeWidth={2.4} />,
|
||||
};
|
||||
const handlers: Record<MobileEditorToolbarActionId, () => void> = {
|
||||
image: onPickImage,
|
||||
bold: onToggleBold,
|
||||
bulletList: onToggleBulletList,
|
||||
blockquote: onToggleBlockquote,
|
||||
horizontalRule: onSetHorizontalRule,
|
||||
};
|
||||
const activeStates: Partial<Record<MobileEditorToolbarActionId, boolean>> = {
|
||||
bold: boldActive,
|
||||
bulletList: bulletListActive,
|
||||
blockquote: blockquoteActive,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-editor-tool-row">
|
||||
{MOBILE_EDITOR_TOOLBAR_ACTIONS.map(({ id }) => {
|
||||
const label = getMobileEditorToolbarActionLabel(id, "zh-CN");
|
||||
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
className="mobile-editor-tool-button"
|
||||
type="button"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
aria-pressed={activeStates[id]}
|
||||
disabled={disabled}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={handlers[id]}
|
||||
>
|
||||
{icons[id]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileEditorNotebookButton = ({
|
||||
label,
|
||||
|
||||
@@ -4,6 +4,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { createExcerpt, docToMarkdown, docToText, emptyDoc, type MemoDetail, type MemoEditSession, type Notebook, type TiptapDoc } from "@edgeever/shared";
|
||||
import { getMobileEditorInputAttributes, getMobileEditorPlaceholder } from "@edgeever/shared/mobile-editor";
|
||||
import {
|
||||
MobileEditorFallback,
|
||||
MobileEditorHeader,
|
||||
@@ -171,19 +172,12 @@ export const MobileStandaloneTiptapEditor = ({
|
||||
inline: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: "开始记录...",
|
||||
placeholder: getMobileEditorPlaceholder("zh-CN"),
|
||||
}),
|
||||
],
|
||||
content: emptyDoc(),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "edgeever-mobile-tiptap-content",
|
||||
autocapitalize: "sentences",
|
||||
autocomplete: "on",
|
||||
autocorrect: "on",
|
||||
inputmode: "text",
|
||||
spellcheck: "true",
|
||||
},
|
||||
attributes: getMobileEditorInputAttributes("edgeever-mobile-tiptap-content"),
|
||||
},
|
||||
onUpdate: ({ editor: activeEditor }) => {
|
||||
contentJsonRef.current = activeEditor.getJSON() as TiptapDoc;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./i18n": "./src/i18n/index.ts",
|
||||
"./mobile-editor": "./src/mobile-editor.ts",
|
||||
"./mobile-ui": "./src/mobile-ui.ts"
|
||||
},
|
||||
"types": "./src/index.ts",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
MOBILE_EDITOR_ACTIVE_FLAGS,
|
||||
MOBILE_EDITOR_TOOLBAR_ACTIONS,
|
||||
getMobileEditorInputAttributes,
|
||||
getMobileEditorPlaceholder,
|
||||
getMobileEditorToolbarActionLabel,
|
||||
getMobileEditorToolbarLabel,
|
||||
} from "./mobile-editor.ts";
|
||||
|
||||
describe("mobile editor contract", () => {
|
||||
test("keeps the core toolbar compact and ordered by editing frequency", () => {
|
||||
expect(MOBILE_EDITOR_TOOLBAR_ACTIONS.map(({ id }) => id)).toEqual([
|
||||
"image",
|
||||
"bold",
|
||||
"bulletList",
|
||||
"blockquote",
|
||||
"horizontalRule",
|
||||
]);
|
||||
expect(MOBILE_EDITOR_TOOLBAR_ACTIONS.find(({ id }) => id === "bold")?.activeFlag).toBe(
|
||||
MOBILE_EDITOR_ACTIVE_FLAGS.bold
|
||||
);
|
||||
});
|
||||
|
||||
test("provides the same localized copy to both mobile clients", () => {
|
||||
expect(getMobileEditorPlaceholder("zh-CN")).toBe("开始记录...");
|
||||
expect(getMobileEditorPlaceholder("en-US")).toBe("Start writing...");
|
||||
expect(getMobileEditorToolbarLabel("zh-CN")).toBe("编辑器工具栏");
|
||||
expect(getMobileEditorToolbarActionLabel("bulletList", "en-US")).toBe("Bullet list");
|
||||
});
|
||||
|
||||
test("keeps mobile typing assistance enabled", () => {
|
||||
expect(getMobileEditorInputAttributes("editor-content")).toEqual({
|
||||
autocapitalize: "sentences",
|
||||
autocomplete: "on",
|
||||
autocorrect: "on",
|
||||
class: "editor-content",
|
||||
inputmode: "text",
|
||||
spellcheck: "true",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
export type MobileEditorLocale = "zh-CN" | "en-US";
|
||||
|
||||
export type MobileEditorToolbarActionId =
|
||||
| "image"
|
||||
| "bold"
|
||||
| "bulletList"
|
||||
| "blockquote"
|
||||
| "horizontalRule";
|
||||
|
||||
export const MOBILE_EDITOR_ACTIVE_FLAGS = {
|
||||
bold: 1,
|
||||
bulletList: 8,
|
||||
blockquote: 16,
|
||||
} as const;
|
||||
|
||||
export const MOBILE_EDITOR_TOOLBAR_ACTIONS = [
|
||||
{ id: "image", activeFlag: 0 },
|
||||
{ id: "bold", activeFlag: MOBILE_EDITOR_ACTIVE_FLAGS.bold },
|
||||
{ id: "bulletList", activeFlag: MOBILE_EDITOR_ACTIVE_FLAGS.bulletList },
|
||||
{ id: "blockquote", activeFlag: MOBILE_EDITOR_ACTIVE_FLAGS.blockquote },
|
||||
{ id: "horizontalRule", activeFlag: 0 },
|
||||
] as const satisfies ReadonlyArray<{
|
||||
id: MobileEditorToolbarActionId;
|
||||
activeFlag: number;
|
||||
}>;
|
||||
|
||||
const MOBILE_EDITOR_COPY = {
|
||||
"zh-CN": {
|
||||
placeholder: "开始记录...",
|
||||
toolbar: "编辑器工具栏",
|
||||
actions: {
|
||||
image: "上传图片",
|
||||
bold: "加粗",
|
||||
bulletList: "无序列表",
|
||||
blockquote: "引用",
|
||||
horizontalRule: "分割线",
|
||||
},
|
||||
},
|
||||
"en-US": {
|
||||
placeholder: "Start writing...",
|
||||
toolbar: "Editor toolbar",
|
||||
actions: {
|
||||
image: "Upload image",
|
||||
bold: "Bold",
|
||||
bulletList: "Bullet list",
|
||||
blockquote: "Quote",
|
||||
horizontalRule: "Horizontal rule",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const getMobileEditorPlaceholder = (locale: MobileEditorLocale): string =>
|
||||
MOBILE_EDITOR_COPY[locale].placeholder;
|
||||
|
||||
export const getMobileEditorToolbarLabel = (locale: MobileEditorLocale): string =>
|
||||
MOBILE_EDITOR_COPY[locale].toolbar;
|
||||
|
||||
export const getMobileEditorToolbarActionLabel = (
|
||||
action: MobileEditorToolbarActionId,
|
||||
locale: MobileEditorLocale
|
||||
): string => MOBILE_EDITOR_COPY[locale].actions[action];
|
||||
|
||||
export const getMobileEditorInputAttributes = (className: string): Record<string, string> => ({
|
||||
autocapitalize: "sentences",
|
||||
autocomplete: "on",
|
||||
autocorrect: "on",
|
||||
class: className,
|
||||
inputmode: "text",
|
||||
spellcheck: "true",
|
||||
});
|
||||
Reference in new issue
Block a user