release: prepare v0.4.5 mobile core

This commit is contained in:
tianma-if committed 2026-07-19 12:23:01 +08:00
1 parent db4b8bd86c
commit d2fc58c82d
8 files changed
+209 -92

No files matched your search

+2 -2
View File
@@ -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 -1
View File
@@ -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;
+1
View File
@@ -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",
});
});
});
+70
View File
@@ -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",
});