feat(editor): add Marxico theme and refine mobile editing
This commit is contained in:
1 parent
eb2f03e1c4
commit
af02e7fa2f
25 files changed
+1037
-138
No files matched your search
@@ -40,6 +40,7 @@ The public demo resets every day at 3:00 AM (China Standard Time) and restores s
|
||||
- **Lossless ZIP Backup & Portability**: Export your complete library as a clean archive containing Markdown, Front Matter, nested folders, relative attachment links, and version histories for instant restoration anywhere.
|
||||
- **Native AI Agent Synergy**: Deep integration with Model Context Protocol (MCP) allows AI tools like Claude Code, Codex, and Antigravity to read, organize, and summarize your notes, or sync seamlessly with Notion and Feishu Bitable.
|
||||
- **Bring Your Own AI Models**: Connect OpenAI, Anthropic, or Gemini-compatible services and third-party API relays to empower your editor with smart note summarization, key point extraction, proofreading, translation, and text continuation on full notes or selected text.
|
||||
- **Plugin Extensibility**: Install client plugins and themes from the Plugin Marketplace to extend note actions, editor commands, custom panels, and more.
|
||||
- **Unlimited Multi-Device Sync**: No commercial device caps or paywalls. Enjoy seamless synchronization across PC, tablet, and mobile via web, PWA, or browser.
|
||||
- **Classic Three-Pane Layout & Focus Mode**: Clean navigation featuring notebook trees, note lists, and an expansive editor, with a desktop focus mode to eliminate distractions.
|
||||
- **Unlimited Nested Notebooks**: Organize your knowledge with arbitrary folder depth.
|
||||
@@ -222,17 +223,11 @@ https://your-domain/api/openapi.json
|
||||
|
||||
Repository file: [docs/openapi.json](docs/openapi.json).
|
||||
|
||||
## Plugin Development Preview
|
||||
|
||||
The standalone Plugin Marketplace supports trusted client plugins and code-free theme packages installed from its verified index, a public GitHub repository, or a manifest URL. Plugins can query and update notes, work with editor selections, register commands and custom panels, use encrypted secret storage, and make allowlisted network requests. A unified top-right desktop entry includes recently used actions. Cron and background jobs are intentionally deferred. See [Plugin Development](docs/plugin-development.md).
|
||||
|
||||
## MCP
|
||||
|
||||
Create an API token in **Profile** -> **MCP settings**, then give the token or full MCP configuration to your AI Agent. Once connected, the Agent can securely read, organize, and import notes within your account permissions. MCP also exposes full management of reusable note templates and AI instructions: agents can list, inspect, create, update, and delete both, create a note from a template, and restore missing built-in AI instructions. Template and instruction reads use the `read:memos` scope, while changes use `write:memos`. Repeating the same memo import will not create duplicate notes.
|
||||
Create an API token in **Profile** -> **MCP settings** and give it to your AI Agent. The Agent can then securely read, organize, and import notes, manage note templates and AI instructions, and connect your notes with tools such as Notion databases and Feishu Bitable—all within your account permissions.
|
||||
|
||||
The Remote MCP endpoint supports the stateless `2026-07-28` protocol while retaining the handshake-based 2025 revisions for existing clients.
|
||||
|
||||
With MCP, EdgeEver can also connect to tools such as Notion databases and Feishu Bitable, turning scattered ideas, information, and materials from everyday notes into structured data that is easier to organize, search, and manage.
|
||||
> Let your ideas run free: ask an AI Agent to organize fleeting thoughts, build a personal knowledge graph, create a profile from your notes, or tag them automatically.
|
||||
|
||||
## Image Compression
|
||||
|
||||
@@ -261,6 +256,7 @@ Docker runs the same frontend, API routes, services, authentication, MCP impleme
|
||||
|
||||
- The "Minimal Emerald" theme typography layout is inspired by [obsidian-minimal](https://github.com/kepano/obsidian-minimal).
|
||||
- The "Outline Emerald" theme typography layout is inspired by [Outline](https://github.com/outline/outline).
|
||||
- The "Marxico Classic" theme adapts the default typography of [Marxico](https://maxiang.io/) to EdgeEver's editor.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
+4
-10
@@ -40,6 +40,7 @@ EdgeEver 是一款现代化的开源笔记工作区。它为你找回经典印
|
||||
- **无损 ZIP 打包与无缝迁移**:一键打包导出包含 Markdown、Front Matter、嵌套目录及附件的完整档案,同时保留历史版本与结构化数据,方便在不同实例间完整还原。
|
||||
- **原生 AI Agent 智脑联动**:内置 MCP(Model Context Protocol)协议,支持 Claude Code、Codex、Antigravity 等 AI 助手直接读取与整理笔记,也可与 Notion Database、飞书多维表格轻松打通。
|
||||
- **接入自己的 AI 模型**:支持添加多个 OpenAI、Anthropic、Gemini 兼容服务与第三方中转平台,在编辑器中随时对全文或选区进行智能总结、要点提炼、语法校对、翻译与续写润色。
|
||||
- **插件扩展能力**:支持从插件市场安装客户端插件与主题,扩展笔记操作、编辑器命令和自定义面板等能力。
|
||||
- **多端无缝同步,无设备限制**:自托管数据无商业限制,摆脱免费账号仅限 2 台设备的束缚,在 PC、平板与手机上随心同步。
|
||||
- **经典三栏布局与专注模式**:笔记本树、笔记列表与编辑区一目了然;桌面端一键开启专注模式,让思绪尽情铺满屏幕。
|
||||
- **无限层级笔记本**:轻松构建清晰的多级目录结构。
|
||||
@@ -219,19 +220,11 @@ https://你的域名/api/openapi.json
|
||||
|
||||
仓库内文件:[docs/openapi.json](docs/openapi.json)。
|
||||
|
||||
## 插件开发预览
|
||||
|
||||
独立的插件市场页面支持从已验证索引、公开 GitHub 仓库或 Manifest 地址安装受信任的客户端插件和无代码主题包。插件可以查询与修改笔记、操作编辑器选区、注册命令和自定义面板、使用加密 Secret Storage,并向声明过的域名发起网络请求。桌面端右上角提供统一插件入口和最近使用记录;Cron 与后台任务暂不纳入。详见[插件开发文档](docs/plugin-development.zh-CN.md)。
|
||||
|
||||
## MCP
|
||||
|
||||
先在 EdgeEver 左下角 **个人中心** 的 **MCP 设置** 中创建 API Token,再将 Token 或完整 MCP 配置发送给 AI Agent。连接后,Agent 即可在你的授权范围内安全地读取、整理和导入笔记。MCP 也开放了完整的笔记模板与 AI 指令管理能力:Agent 可以列出、查看、新建、更新和删除模板与指令,也可以使用模板创建笔记、恢复缺失的内置指令。读取模板和指令需要 `read:memos` 权限,变更需要 `write:memos` 权限;重复执行同一笔记导入任务不会创建重复笔记。
|
||||
在 **个人中心** -> **MCP 设置** 中创建 API Token 并交给 AI Agent,即可让 Agent 在账号授权范围内安全地读取、整理和导入笔记,管理笔记模板与 AI 指令,并与 Notion Database、飞书多维表格等工具联动。
|
||||
|
||||
Remote MCP 端点支持无状态的 `2026-07-28` 协议,同时继续兼容现有客户端使用的 2025 握手式协议版本。
|
||||
|
||||
> 放飞你的思路,这种情况下是有很多灵活玩法:
|
||||
比如让AI Agent归纳你随机记录的灵感创意、针对你的笔记做精准的人物画像、构建自己的知识图谱、自动为笔记打标签)
|
||||
借助 MCP,EdgeEver 还可以与 Notion Database、飞书多维表格等工具联动,把日常笔记中零散的灵感、信息和素材沉淀到结构化数据库中,方便后续整理、检索与管理。
|
||||
> 放飞你的想法:让 AI Agent 归纳随手记录的灵感、构建个人知识图谱、根据笔记生成用户画像,或自动为笔记打标签。
|
||||
|
||||
## 图片压缩规则
|
||||
|
||||
@@ -259,6 +252,7 @@ Docker 与 Cloudflare 共用同一套前端、API 路由、业务服务、鉴权
|
||||
|
||||
- “minimal品牌绿”主题排版架构借鉴于 [obsidian-minimal](https://github.com/kepano/obsidian-minimal)。
|
||||
- “Outline 品牌绿”主题排版架构借鉴于 [Outline](https://github.com/outline/outline)。
|
||||
- “马克飞象经典”主题将[马克飞象](https://maxiang.io/)的默认排版适配到了 EdgeEver 编辑器。
|
||||
|
||||
## 免责声明
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,9 +1,6 @@
|
||||
'use dom';
|
||||
|
||||
import "mermaid/dist/mermaid.min.js";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { renderMermaidSVG, THEMES } from "beautiful-mermaid";
|
||||
import { toCanvas } from "html-to-image";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import CodeBlock from "@tiptap/extension-code-block";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
@@ -343,12 +340,18 @@ const handleMobileResourceEvent = (
|
||||
return false;
|
||||
};
|
||||
|
||||
const getMobileMermaidTheme = (theme: "light" | "dark") => THEMES[theme === "dark" ? "zinc-dark" : "zinc-light"];
|
||||
let beautifulMermaidRuntime: Promise<typeof import("beautiful-mermaid")> | null = null;
|
||||
|
||||
const renderWithBeautifulMermaid = (source: string, theme: "light" | "dark") => {
|
||||
const loadBeautifulMermaid = () => {
|
||||
beautifulMermaidRuntime ??= import("beautiful-mermaid");
|
||||
return beautifulMermaidRuntime;
|
||||
};
|
||||
|
||||
const renderWithBeautifulMermaid = async (source: string, theme: "light" | "dark") => {
|
||||
try {
|
||||
const { renderMermaidSVG, THEMES } = await loadBeautifulMermaid();
|
||||
return renderMermaidSVG(source, {
|
||||
...getMobileMermaidTheme(theme),
|
||||
...THEMES[theme === "dark" ? "zinc-dark" : "zinc-light"],
|
||||
transparent: true,
|
||||
font: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
padding: 24,
|
||||
@@ -392,7 +395,7 @@ const MermaidRenderRuntime = (props: MermaidRendererProps) => {
|
||||
const results: Array<{ source: string; svg: string | null }> = [];
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const beautifulSvg = renderWithBeautifulMermaid(source, props.theme);
|
||||
const beautifulSvg = await renderWithBeautifulMermaid(source, props.theme);
|
||||
if (beautifulSvg) {
|
||||
results.push({ source, svg: inlineMermaidSvgStyles(beautifulSvg) });
|
||||
continue;
|
||||
@@ -671,7 +674,9 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) {
|
||||
|
||||
const editor = useEditor({
|
||||
editable: !isViewer,
|
||||
autofocus: autoFocus ? "end" : false,
|
||||
// Focus only after the DOM view reports ready. Initial TipTap autofocus plus
|
||||
// the Android bridge retry raced each other and could leave the WebView stuck.
|
||||
autofocus: false,
|
||||
extensions: [
|
||||
StarterKit.configure({ codeBlock: false, link: { openOnClick: false } }),
|
||||
TaskList,
|
||||
@@ -1158,6 +1163,7 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) {
|
||||
const totalHeight = Math.max(1, Math.ceil(documentRoot.getBoundingClientRect().height));
|
||||
const backgroundColor = NOTE_IMAGE_BACKGROUND_COLORS[resolvedTheme] || themeCfg.canvasBg;
|
||||
|
||||
const { toCanvas } = await import("html-to-image");
|
||||
const canvas = await toCanvas(documentRoot, {
|
||||
backgroundColor,
|
||||
cacheBust: false,
|
||||
@@ -2015,14 +2021,17 @@ const getMobileMermaidThemeVariables = (theme: "light" | "dark") => {
|
||||
};
|
||||
};
|
||||
|
||||
let mermaidRuntime: Promise<typeof import("mermaid")["default"]> | null = null;
|
||||
|
||||
const loadMermaid = () => {
|
||||
const mermaid = (globalThis as typeof globalThis & {
|
||||
mermaid?: typeof import("mermaid")["default"];
|
||||
}).mermaid;
|
||||
if (!mermaid) {
|
||||
return Promise.reject(new Error("Mermaid runtime unavailable"));
|
||||
}
|
||||
return Promise.resolve(mermaid);
|
||||
mermaidRuntime ??= import("mermaid/dist/mermaid.min.js").then(() => {
|
||||
const mermaid = (globalThis as typeof globalThis & {
|
||||
mermaid?: typeof import("mermaid")["default"];
|
||||
}).mermaid;
|
||||
if (!mermaid) throw new Error("Mermaid runtime unavailable");
|
||||
return mermaid;
|
||||
});
|
||||
return mermaidRuntime;
|
||||
};
|
||||
|
||||
const createMobileCodeBlockExtension = (
|
||||
@@ -2133,7 +2142,7 @@ const createMobileCodeBlockExtension = (
|
||||
preview.replaceChildren(message);
|
||||
void loadMermaid()
|
||||
.then(async (mermaid) => {
|
||||
const beautifulSvg = renderWithBeautifulMermaid(source, theme);
|
||||
const beautifulSvg = await renderWithBeautifulMermaid(source, theme);
|
||||
if (beautifulSvg) {
|
||||
return { svg: beautifulSvg };
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ const mobileOnlyTranslations = new Map<string, string>([
|
||||
["启动至列表数据就绪", "Launch to list data ready"],
|
||||
["启动至交互空闲", "Launch to interaction idle"],
|
||||
["最近一次本地编辑器启动", "Latest local editor startup"],
|
||||
["正在启动编辑器", "Starting editor"],
|
||||
["编辑器启动时间过长", "The editor is taking too long to start"],
|
||||
["正在准备本地编辑器,笔记内容是安全的。", "Preparing the local editor. Your note is safe."],
|
||||
["本地编辑器未能及时启动,可以重试或返回,当前草稿不会丢失。", "The local editor did not start in time. You can retry or go back; your current draft will not be lost."],
|
||||
["暂不可用", "Unavailable"],
|
||||
["尚未记录", "Not recorded"],
|
||||
["正在搜索", "Searching"],
|
||||
|
||||
@@ -192,6 +192,62 @@ const ANDROID_SYSTEM_NAVIGATION_FALLBACK = 48;
|
||||
const DETAIL_CONTENT_HORIZONTAL_PADDING = 16;
|
||||
const DETAIL_TABLE_FIT_COLUMN_COUNT = 3;
|
||||
const DETAIL_TABLE_MIN_COLUMN_WIDTH = 132;
|
||||
const MOBILE_EDITOR_STARTUP_TIMEOUT_MS = 10_000;
|
||||
|
||||
const useMobileEditorStartupGuard = ({ active, ready }: { active: boolean; ready: boolean }) => {
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const [timedOut, setTimedOut] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || ready) {
|
||||
setTimedOut(false);
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => setTimedOut(true), MOBILE_EDITOR_STARTUP_TIMEOUT_MS);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [active, attempt, ready]);
|
||||
|
||||
const restart = useCallback(() => {
|
||||
setTimedOut(false);
|
||||
setAttempt((current) => current + 1);
|
||||
}, []);
|
||||
|
||||
return { attempt, restart, timedOut };
|
||||
};
|
||||
|
||||
const MobileEditorStartupOverlay = ({
|
||||
onRetry,
|
||||
timedOut,
|
||||
}: {
|
||||
onRetry: () => void;
|
||||
timedOut: boolean;
|
||||
}) => {
|
||||
const { translate } = useMobileLocale();
|
||||
return (
|
||||
<View accessibilityLiveRegion="polite" style={styles.richEditorLoading}>
|
||||
{!timedOut ? <ActivityIndicator color="#16a06e" size="large" /> : null}
|
||||
<Text style={styles.richEditorLoadingTitle}>
|
||||
{translate(timedOut ? "编辑器启动时间过长" : "正在启动编辑器")}
|
||||
</Text>
|
||||
<Text style={styles.mutedText}>
|
||||
{translate(timedOut
|
||||
? "本地编辑器未能及时启动,可以重试或返回,当前草稿不会丢失。"
|
||||
: "正在准备本地编辑器,笔记内容是安全的。")}
|
||||
</Text>
|
||||
{timedOut ? (
|
||||
<Pressable
|
||||
accessibilityLabel={translate("重试")}
|
||||
accessibilityRole="button"
|
||||
onPress={onRetry}
|
||||
style={styles.actionButton}
|
||||
>
|
||||
<RotateCcw color="#0f172a" size={16} />
|
||||
<Text style={styles.actionButtonText}>{translate("重试")}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const resolveEditableMemoTitle = (title?: string | null) => {
|
||||
const trimmedTitle = title?.trim() ?? "";
|
||||
@@ -2075,6 +2131,7 @@ const CreateMemoModal = ({
|
||||
const submitStartedRef = useRef(false);
|
||||
const [submitStarted, setSubmitStarted] = useState(false);
|
||||
const [resourceTarget, setResourceTarget] = useState<MobileResourceTarget | null>(null);
|
||||
const editorStartup = useMobileEditorStartupGuard({ active: draftLoaded && Boolean(baseUrl), ready: editorReady });
|
||||
const { pickUploadAsset, uploadSourcePicker } = useMobileEditorUploadAsset();
|
||||
const targetNotebookId = notebookId || fallbackNotebookId;
|
||||
const selectedNotebookName = notebooks.find((notebook) => notebook.id === targetNotebookId)?.name ?? "选择笔记本";
|
||||
@@ -2114,21 +2171,29 @@ const CreateMemoModal = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scheduleBodyKeyboard = useCallback((delayMs = 160) => {
|
||||
const scheduleBodyKeyboard = useCallback((delayMs = 160, focusEditor = true) => {
|
||||
clearFocusTimers();
|
||||
// Full-tree create only mounts the editor DomWebView, so native IME show is safe again.
|
||||
focusTimerRef.current = setTimeout(() => {
|
||||
focusTimerRef.current = null;
|
||||
safeDomCall(() => editorRef.current?.focusEnd());
|
||||
if (focusEditor) {
|
||||
safeDomCall(() => editorRef.current?.focusEnd());
|
||||
}
|
||||
if (Platform.OS === "android") {
|
||||
keyboardTimerRef.current = setTimeout(() => {
|
||||
keyboardTimerRef.current = null;
|
||||
showEdgeEverKeyboard();
|
||||
}, 120);
|
||||
}, focusEditor ? 120 : 0);
|
||||
}
|
||||
}, delayMs);
|
||||
}, [clearFocusTimers]);
|
||||
|
||||
const retryEditorStartup = useCallback(() => {
|
||||
clearFocusTimers();
|
||||
setEditorReady(false);
|
||||
editorStartup.restart();
|
||||
}, [clearFocusTimers, editorStartup.restart]);
|
||||
|
||||
// Component is only mounted while create is open — init once on mount.
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -2514,13 +2579,16 @@ const CreateMemoModal = ({
|
||||
onReady={async (elapsedMs) => {
|
||||
setEditorReady(true);
|
||||
recordEditorStartup(elapsedMs);
|
||||
scheduleBodyKeyboard(60);
|
||||
// LocalTiptapEditor owns caret placement; native only reveals the IME after
|
||||
// its final focus retry so the two layers cannot race each other.
|
||||
scheduleBodyKeyboard(180, false);
|
||||
}}
|
||||
key={editorStartup.attempt}
|
||||
ref={editorRef}
|
||||
locale={resolvedLocale}
|
||||
theme={resolvedTheme}
|
||||
/>
|
||||
) : null, [aiPromptsJson, baseUrl, cancelSelectionAi, draftLoaded, loadEditorResource, pushBodyToEditor, requestSelectionAi, resolvedLocale, resolvedTheme, scheduleBodyKeyboard, selectResource]);
|
||||
) : null, [aiPromptsJson, baseUrl, cancelSelectionAi, draftLoaded, editorStartup.attempt, loadEditorResource, pushBodyToEditor, requestSelectionAi, resolvedLocale, resolvedTheme, scheduleBodyKeyboard, selectResource]);
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "left", "right", "bottom"]} style={styles.createMemoSafeArea}>
|
||||
@@ -2591,6 +2659,7 @@ const CreateMemoModal = ({
|
||||
|
||||
<View style={styles.createMemoEditorFrame}>
|
||||
{editorElement}
|
||||
{!editorReady ? <MobileEditorStartupOverlay onRetry={retryEditorStartup} timedOut={editorStartup.timedOut} /> : null}
|
||||
</View>
|
||||
|
||||
{createMutation.error ? (
|
||||
@@ -2851,6 +2920,7 @@ const RichEditorModal = ({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [startupMs, setStartupMs] = useState<number | null>(null);
|
||||
const [resourceTarget, setResourceTarget] = useState<MobileResourceTarget | null>(null);
|
||||
const editorStartup = useMobileEditorStartupGuard({ active: Boolean(memo && baseUrl), ready });
|
||||
const { pickUploadAsset, uploadSourcePicker } = useMobileEditorUploadAsset();
|
||||
const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? "未分类";
|
||||
const saveLabel = error ? "保存失败" : saving ? "保存中" : uploading ? "上传中" : dirty ? (draftRestored ? "本地草稿" : "未保存") : ready ? "已保存" : "加载中";
|
||||
@@ -2868,6 +2938,17 @@ const RichEditorModal = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const retryEditorStartup = useCallback(() => {
|
||||
if (initialFocusTimerRef.current !== null) {
|
||||
clearTimeout(initialFocusTimerRef.current);
|
||||
initialFocusTimerRef.current = null;
|
||||
}
|
||||
setReady(false);
|
||||
setStartupMs(null);
|
||||
setError(null);
|
||||
editorStartup.restart();
|
||||
}, [editorStartup.restart]);
|
||||
|
||||
const persistDraft = async (contentJson: TiptapDoc) => {
|
||||
const currentMemo = memoBaseRef.current;
|
||||
if (!currentMemo) {
|
||||
@@ -3062,23 +3143,22 @@ const RichEditorModal = ({
|
||||
if (initialFocusTimerRef.current !== null) {
|
||||
clearTimeout(initialFocusTimerRef.current);
|
||||
}
|
||||
// Full-tree edit mounts a single DomWebView — focus then show the soft keyboard.
|
||||
initialFocusTimerRef.current = setTimeout(() => {
|
||||
initialFocusTimerRef.current = null;
|
||||
safeDomCall(() => editorRef.current?.focusEnd());
|
||||
if (Platform.OS === "android") {
|
||||
setTimeout(() => {
|
||||
showEdgeEverKeyboard();
|
||||
}, 120);
|
||||
}
|
||||
}, 60);
|
||||
// The DOM editor performs its own bounded focus retry. Reveal the Android
|
||||
// keyboard only after that retry instead of issuing another competing focus.
|
||||
if (Platform.OS === "android") {
|
||||
initialFocusTimerRef.current = setTimeout(() => {
|
||||
initialFocusTimerRef.current = null;
|
||||
showEdgeEverKeyboard();
|
||||
}, 180);
|
||||
}
|
||||
}}
|
||||
key={editorStartup.attempt}
|
||||
ref={editorRef}
|
||||
locale={resolvedLocale}
|
||||
theme={resolvedTheme}
|
||||
/>
|
||||
) : null,
|
||||
[aiPromptsJson, baseUrl, cancelSelectionAi, loadEditorResource, memo?.id, requestSelectionAi, resolvedLocale, resolvedTheme, selectResource]
|
||||
[aiPromptsJson, baseUrl, cancelSelectionAi, editorStartup.attempt, loadEditorResource, memo?.id, requestSelectionAi, resolvedLocale, resolvedTheme, selectResource]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -3175,6 +3255,7 @@ const RichEditorModal = ({
|
||||
{draftRestored ? <Text style={styles.richEditorDraftNotice}>已恢复上次未完成的本地草稿</Text> : null}
|
||||
<View style={styles.richEditorFrame}>
|
||||
{editorElement}
|
||||
{!ready ? <MobileEditorStartupOverlay onRetry={retryEditorStartup} timedOut={editorStartup.timedOut} /> : null}
|
||||
</View>
|
||||
{error ? <Text style={styles.richEditorInlineError}>{error}</Text> : null}
|
||||
{startupMs !== null && __DEV__ ? <Text style={styles.richEditorPerf}>本地编辑器启动:{startupMs}ms</Text> : null}
|
||||
|
||||
@@ -1310,6 +1310,12 @@ const baseWorkspaceStyles = StyleSheet.create({
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
},
|
||||
richEditorLoadingTitle: {
|
||||
color: "#0f172a",
|
||||
fontSize: 15,
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
richEditorWebView: {
|
||||
backgroundColor: "#ffffff",
|
||||
flex: 1,
|
||||
|
||||
+169
-1
@@ -14,9 +14,177 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<title>编辑笔记 - EdgeEver</title>
|
||||
<style>
|
||||
#mobile-editor-bootstrap {
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
padding: 24px;
|
||||
}
|
||||
.mobile-editor-bootstrap-card {
|
||||
width: min(100%, 360px);
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-editor-bootstrap-spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 auto 16px;
|
||||
border: 3px solid #d1fae5;
|
||||
border-top-color: #16a06e;
|
||||
border-radius: 999px;
|
||||
animation: mobile-editor-bootstrap-spin 0.8s linear infinite;
|
||||
}
|
||||
.mobile-editor-bootstrap-title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.mobile-editor-bootstrap-detail {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.mobile-editor-bootstrap-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.mobile-editor-bootstrap-action {
|
||||
min-height: 40px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 9px;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.mobile-editor-bootstrap-action[data-primary="true"] {
|
||||
border-color: #16a06e;
|
||||
background: #16a06e;
|
||||
color: #fff;
|
||||
}
|
||||
@keyframes mobile-editor-bootstrap-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mobile-editor-bootstrap-spinner { animation: none; }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#mobile-editor-bootstrap { background: #19222b; color: #f8fafc; }
|
||||
.mobile-editor-bootstrap-detail { color: #94a3b8; }
|
||||
.mobile-editor-bootstrap-action { border-color: #475569; background: #1e293b; color: #f8fafc; }
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(() => {
|
||||
const slowLoadDelayMs = 8000;
|
||||
const english = navigator.language.toLowerCase().startsWith("en");
|
||||
const labels = english
|
||||
? {
|
||||
loading: "Opening editor…",
|
||||
loadingDetail: "Your note is safe. This may take a moment on a slow connection.",
|
||||
delayed: "The editor is taking longer than expected to open.",
|
||||
failed: "The editor could not start.",
|
||||
failedDetail: "Check your connection, then retry or return to the note list.",
|
||||
back: "Back",
|
||||
retry: "Retry",
|
||||
}
|
||||
: {
|
||||
loading: "正在打开编辑器…",
|
||||
loadingDetail: "笔记内容是安全的,网络较慢时可能需要稍等片刻。",
|
||||
delayed: "编辑器启动时间较长,请检查网络连接。",
|
||||
failed: "编辑器未能启动",
|
||||
failedDetail: "请检查网络后重试,或先返回笔记列表。",
|
||||
back: "返回",
|
||||
retry: "重试",
|
||||
};
|
||||
let mounted = false;
|
||||
let slowLoadTimer = 0;
|
||||
|
||||
const getElement = (id) => document.getElementById(id);
|
||||
const setBootstrapCopy = (title, detail, state) => {
|
||||
if (mounted) return;
|
||||
const bootstrap = getElement("mobile-editor-bootstrap");
|
||||
const titleElement = getElement("mobile-editor-bootstrap-title");
|
||||
const detailElement = getElement("mobile-editor-bootstrap-detail");
|
||||
const retryButton = getElement("mobile-editor-bootstrap-retry");
|
||||
if (!bootstrap || !titleElement || !detailElement || !retryButton) return;
|
||||
bootstrap.dataset.state = state;
|
||||
titleElement.textContent = title;
|
||||
detailElement.textContent = detail;
|
||||
retryButton.hidden = state === "loading";
|
||||
};
|
||||
const showFailure = () => setBootstrapCopy(labels.failed, labels.failedDetail, "error");
|
||||
const onWindowError = (event) => {
|
||||
const target = event.target;
|
||||
const failedCriticalResource = target instanceof HTMLScriptElement
|
||||
|| (target instanceof HTMLLinkElement && (target.rel === "stylesheet" || target.rel === "modulepreload"));
|
||||
if (target === window || failedCriticalResource) showFailure();
|
||||
};
|
||||
const onUnhandledRejection = () => showFailure();
|
||||
const returnToPreviousPage = () => {
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(location.hash.slice(1));
|
||||
const returnTo = params.get("returnTo");
|
||||
location.replace(returnTo && returnTo.startsWith("/") ? returnTo : "/");
|
||||
};
|
||||
const bindBootstrap = () => {
|
||||
setBootstrapCopy(labels.loading, labels.loadingDetail, "loading");
|
||||
const backButton = getElement("mobile-editor-bootstrap-back");
|
||||
const retryButton = getElement("mobile-editor-bootstrap-retry");
|
||||
if (backButton) {
|
||||
backButton.textContent = labels.back;
|
||||
backButton.addEventListener("click", returnToPreviousPage);
|
||||
}
|
||||
if (retryButton) {
|
||||
retryButton.textContent = labels.retry;
|
||||
retryButton.addEventListener("click", () => location.reload());
|
||||
}
|
||||
};
|
||||
|
||||
window.edgeeverMobileEditorBootstrap = {
|
||||
markMounted() {
|
||||
mounted = true;
|
||||
window.clearTimeout(slowLoadTimer);
|
||||
window.removeEventListener("error", onWindowError, true);
|
||||
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
||||
},
|
||||
showFailure,
|
||||
};
|
||||
window.addEventListener("error", onWindowError, true);
|
||||
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
||||
document.addEventListener("DOMContentLoaded", bindBootstrap, { once: true });
|
||||
slowLoadTimer = window.setTimeout(() => {
|
||||
setBootstrapCopy(labels.delayed, labels.loadingDetail, "delayed");
|
||||
}, slowLoadDelayMs);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mobile-editor-root"></div>
|
||||
<div id="mobile-editor-root">
|
||||
<main id="mobile-editor-bootstrap" data-state="loading" role="status" aria-live="polite">
|
||||
<section class="mobile-editor-bootstrap-card">
|
||||
<div class="mobile-editor-bootstrap-spinner" aria-hidden="true"></div>
|
||||
<h1 id="mobile-editor-bootstrap-title" class="mobile-editor-bootstrap-title">正在打开编辑器…</h1>
|
||||
<p id="mobile-editor-bootstrap-detail" class="mobile-editor-bootstrap-detail">笔记内容是安全的,网络较慢时可能需要稍等片刻。</p>
|
||||
<div class="mobile-editor-bootstrap-actions">
|
||||
<button id="mobile-editor-bootstrap-back" class="mobile-editor-bootstrap-action" type="button">返回</button>
|
||||
<button id="mobile-editor-bootstrap-retry" class="mobile-editor-bootstrap-action" data-primary="true" type="button" hidden>重试</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="/src/mobile-edit.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1295,7 +1295,7 @@ const RichEditorPane = ({
|
||||
editable: Boolean(memo && !effectiveReadOnly && hydratedEditorMemoId === memo.id),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "edgeever-note-rich-editor prose prose-slate max-w-none focus:outline-none min-h-[300px] px-4 py-3 sm:px-7",
|
||||
class: "edgeever-note-rich-editor prose prose-slate max-w-none focus:outline-none min-h-[240px] px-4 py-3 sm:px-7 lg:min-h-[180px]",
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
const { selection } = view.state;
|
||||
@@ -4615,7 +4615,8 @@ const RichEditorPane = ({
|
||||
editorTheme === "minimal-emerald" ||
|
||||
editorTheme === "outline-emerald" ||
|
||||
editorTheme === "wechat-green" ||
|
||||
editorTheme === "modern-mint"
|
||||
editorTheme === "modern-mint" ||
|
||||
editorTheme === "marxico"
|
||||
? editorTheme
|
||||
: "custom"
|
||||
}
|
||||
@@ -4628,7 +4629,8 @@ const RichEditorPane = ({
|
||||
editorTheme !== "minimal-emerald" &&
|
||||
editorTheme !== "outline-emerald" &&
|
||||
editorTheme !== "wechat-green" &&
|
||||
editorTheme !== "modern-mint"
|
||||
editorTheme !== "modern-mint" &&
|
||||
editorTheme !== "marxico"
|
||||
? {
|
||||
"--editor-theme-light-bg": customEditorTheme.light.background,
|
||||
"--editor-theme-light-text": customEditorTheme.light.text,
|
||||
@@ -4662,6 +4664,7 @@ const RichEditorPane = ({
|
||||
editorTheme !== "outline-emerald" &&
|
||||
editorTheme !== "wechat-green" &&
|
||||
editorTheme !== "modern-mint" &&
|
||||
editorTheme !== "marxico" &&
|
||||
customEditorTheme.customCss && (
|
||||
<style
|
||||
data-theme-custom-css
|
||||
@@ -4674,7 +4677,7 @@ const RichEditorPane = ({
|
||||
"flex gap-8 transition-all duration-200",
|
||||
useMarkdownSourceEditor
|
||||
? "h-full min-h-0 flex-1 items-stretch px-0 py-0"
|
||||
: "min-h-full items-start px-6 py-6 sm:px-10",
|
||||
: "min-h-full items-start px-6 py-4 sm:px-10",
|
||||
desktopFocusMode
|
||||
? "mx-auto w-full max-w-[1400px] justify-center"
|
||||
: editorContentAlignment === "center"
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
type MobileEditorSaveState,
|
||||
} from "@/lib/mobile-editor-standalone";
|
||||
import { getMemoUpdateQueueId, isMemoUpdateAlreadyApplied, queueMemoUpdate, shouldQueueMemoSaveError } from "@/lib/sync-queue";
|
||||
import { EdgeEverCodeBlock, codeBlockLowlight } from "@/lib/code-block";
|
||||
import { createMarkdownImagePasteRule } from "@/lib/markdown-image-paste";
|
||||
import { ThemeBlock } from "./ThemeBlock";
|
||||
import { EditorTagPicker } from "./EditorTagPicker";
|
||||
@@ -194,10 +193,9 @@ export const MobileStandaloneTiptapEditor = ({
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({ codeBlock: false }),
|
||||
StarterKit,
|
||||
TaskList,
|
||||
TaskItem.configure({ nested: true }),
|
||||
EdgeEverCodeBlock.configure({ lowlight: codeBlockLowlight, defaultLanguage: "plaintext" }),
|
||||
MergeDivider,
|
||||
...createEdgeEverMathematics(),
|
||||
ThemeBlock,
|
||||
|
||||
@@ -55,6 +55,7 @@ export const EDITOR_THEME_NAMES = [
|
||||
"outline-emerald",
|
||||
"wechat-green",
|
||||
"modern-mint",
|
||||
"marxico",
|
||||
"custom",
|
||||
] as const;
|
||||
export type EditorThemeName = string;
|
||||
|
||||
@@ -140,7 +140,7 @@ export const PreferenceCard = ({
|
||||
}, []);
|
||||
|
||||
const activeCustom = customEditorThemes.find((t) => t.id === editorTheme);
|
||||
const isPreset = editorTheme === "default" || editorTheme === "minimal-emerald" || editorTheme === "outline-emerald" || editorTheme === "wechat-green" || editorTheme === "modern-mint";
|
||||
const isPreset = editorTheme === "default" || editorTheme === "minimal-emerald" || editorTheme === "outline-emerald" || editorTheme === "wechat-green" || editorTheme === "modern-mint" || editorTheme === "marxico";
|
||||
|
||||
const handleEditClick = () => {
|
||||
if (activeCustom) {
|
||||
@@ -281,6 +281,7 @@ export const PreferenceCard = ({
|
||||
<SelectItem value="outline-emerald">{t("settings.editorThemes.outline-emerald")}</SelectItem>
|
||||
<SelectItem value="wechat-green">{t("settings.editorThemes.wechat-green")}</SelectItem>
|
||||
<SelectItem value="modern-mint">{t("settings.editorThemes.modern-mint")}</SelectItem>
|
||||
<SelectItem value="marxico">{t("settings.editorThemes.marxico")}</SelectItem>
|
||||
{!isMobile && (
|
||||
<>
|
||||
{customEditorThemes.length > 0 && <div className="my-1 border-t border-slate-100" />}
|
||||
|
||||
@@ -21,6 +21,15 @@ describe("dark theme contracts", () => {
|
||||
expect(css).toContain('[class~="text-emerald-700"]');
|
||||
});
|
||||
|
||||
test("Marxico keeps note content legible in dark mode", () => {
|
||||
const css = readFileSync(new URL("../styles/editor-themes/marxico.css", import.meta.url), "utf8");
|
||||
|
||||
expect(css).toContain(':root.dark .edgeever-editor[data-editor-theme="marxico"]');
|
||||
expect(css).toContain("color: var(--editor-theme-text);");
|
||||
expect(contrastRatio("#dce3ea", "#19222b")).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio("#e7edf2", "#19222b")).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
|
||||
test("appearance changes stay out of the editor React render path", () => {
|
||||
const editorPane = readFileSync(new URL("../components/EditorPane.tsx", import.meta.url), "utf8");
|
||||
const editorThemeCss = readFileSync(new URL("../styles/editor-themes/base.css", import.meta.url), "utf8");
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const mobileEditorHtml = readFileSync(new URL("../../mobile-edit.html", import.meta.url), "utf8");
|
||||
const mobileEditorEntry = readFileSync(new URL("../mobile-edit.tsx", import.meta.url), "utf8");
|
||||
const mobileEditorComponent = readFileSync(
|
||||
new URL("../components/MobileStandaloneTiptapEditor.tsx", import.meta.url),
|
||||
"utf8"
|
||||
);
|
||||
const viteConfig = readFileSync(new URL("../../vite.config.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("standalone mobile editor bootstrap", () => {
|
||||
test("keeps a usable loading and recovery surface before React mounts", () => {
|
||||
const bootstrapIndex = mobileEditorHtml.indexOf('id="mobile-editor-bootstrap"');
|
||||
const moduleIndex = mobileEditorHtml.indexOf('type="module" src="/src/mobile-edit.tsx"');
|
||||
|
||||
expect(bootstrapIndex).toBeGreaterThan(0);
|
||||
expect(moduleIndex).toBeGreaterThan(bootstrapIndex);
|
||||
expect(mobileEditorHtml).toContain("edgeeverMobileEditorBootstrap");
|
||||
expect(mobileEditorHtml).toContain("showFailure");
|
||||
expect(mobileEditorHtml).toContain('id="mobile-editor-bootstrap-back"');
|
||||
expect(mobileEditorHtml).toContain('id="mobile-editor-bootstrap-retry"');
|
||||
});
|
||||
|
||||
test("replaces bootstrap failures with a React recovery boundary after startup", () => {
|
||||
expect(mobileEditorEntry).toContain("MobileEditorErrorBoundary");
|
||||
expect(mobileEditorEntry).toContain("onUncaughtError");
|
||||
expect(mobileEditorEntry).toContain("edgeeverMobileEditorBootstrap?.markMounted()");
|
||||
});
|
||||
|
||||
test("does not load desktop syntax highlighting on the mobile editor critical path", () => {
|
||||
expect(mobileEditorComponent).toContain("StarterKit,");
|
||||
expect(mobileEditorComponent).not.toContain("codeBlock: false");
|
||||
expect(mobileEditorComponent).not.toContain('from "@/lib/code-block"');
|
||||
expect(mobileEditorComponent).not.toContain("codeBlockLowlight");
|
||||
expect(viteConfig).toContain(
|
||||
'test: /node_modules[\\\\/](?:lowlight|highlight\\.js)[\\\\/]/'
|
||||
);
|
||||
expect(viteConfig).not.toContain(
|
||||
"lowlight|highlight\\.js|@tiptap[\\\\/]extension-code-block-lowlight"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,66 @@ import { initializeTheme } from "@/components/ThemeProvider";
|
||||
import "./i18n";
|
||||
import "./styles/mobile-markdown-editor.css";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
edgeeverMobileEditorBootstrap?: {
|
||||
markMounted: () => void;
|
||||
showFailure: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type MobileEditorErrorBoundaryState = {
|
||||
failed: boolean;
|
||||
};
|
||||
|
||||
const returnToPreviousPage = () => {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
const returnTo = params.get("returnTo");
|
||||
window.location.replace(returnTo?.startsWith("/") ? returnTo : "/");
|
||||
};
|
||||
|
||||
class MobileEditorErrorBoundary extends React.Component<React.PropsWithChildren, MobileEditorErrorBoundaryState> {
|
||||
state: MobileEditorErrorBoundaryState = { failed: false };
|
||||
|
||||
static getDerivedStateFromError(): MobileEditorErrorBoundaryState {
|
||||
return { failed: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
console.error("Mobile editor crashed", error);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.failed) return this.props.children;
|
||||
const english = navigator.language.toLowerCase().startsWith("en");
|
||||
return (
|
||||
<main className="mobile-editor-fatal" role="alert">
|
||||
<section className="mobile-editor-fatal-card">
|
||||
<h1>{english ? "The editor encountered an error" : "编辑器出现异常"}</h1>
|
||||
<p>
|
||||
{english
|
||||
? "Your note is safe. Retry the editor or return to the note list."
|
||||
: "笔记内容是安全的。你可以重试打开编辑器,或先返回笔记列表。"}
|
||||
</p>
|
||||
<div className="mobile-editor-fatal-actions">
|
||||
<button type="button" onClick={returnToPreviousPage}>
|
||||
{english ? "Back" : "返回"}
|
||||
</button>
|
||||
<button className="primary" type="button" onClick={() => window.location.reload()}>
|
||||
{english ? "Retry" : "重试"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.getElementById("mobile-editor-root");
|
||||
|
||||
if (!root) {
|
||||
@@ -18,10 +78,26 @@ const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, staleTime: 15_000 } },
|
||||
});
|
||||
|
||||
createRoot(root).render(
|
||||
const MobileEditorApp = () => {
|
||||
React.useEffect(() => {
|
||||
window.edgeeverMobileEditorBootstrap?.markMounted();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MobileEditorErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MobileStandaloneTiptapEditor />
|
||||
</QueryClientProvider>
|
||||
</MobileEditorErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
createRoot(root, {
|
||||
onUncaughtError(error, errorInfo) {
|
||||
console.error("Uncaught mobile editor error", error, errorInfo.componentStack);
|
||||
},
|
||||
}).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MobileStandaloneTiptapEditor />
|
||||
</QueryClientProvider>
|
||||
<MobileEditorApp />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -3,5 +3,5 @@
|
||||
@import "./outline-emerald.css";
|
||||
@import "./wechat-green.css";
|
||||
@import "./modern-mint.css";
|
||||
|
||||
@import "./marxico.css";
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
/* Marxico's default reading theme, mapped from its live computed styles. */
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) {
|
||||
--editor-theme-bg: #ffffff;
|
||||
--editor-theme-text: #2c3f51;
|
||||
--editor-theme-muted: #668099;
|
||||
--editor-theme-heading: #2c3f51;
|
||||
--editor-theme-accent: #1980e6;
|
||||
--editor-theme-soft: rgb(102 128 153 / 5%);
|
||||
--editor-theme-border: rgb(102 128 153 / 10%);
|
||||
--editor-theme-code-bg: #f9f2f4;
|
||||
--editor-theme-code-text: #c7254e;
|
||||
--editor-theme-table-stripe: #f9f9f9;
|
||||
--editor-theme-table-hover: #f5f5f5;
|
||||
--editor-theme-font: "Helvetica Neue", Arial, "Hiragino Sans GB", STHeiti, "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, Song, sans-serif;
|
||||
--editor-theme-radius: 0;
|
||||
color-scheme: light;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror {
|
||||
max-width: 960px;
|
||||
padding: 0 35px 56px;
|
||||
background: #ffffff;
|
||||
color: #2c3f51;
|
||||
font-family: var(--editor-theme-font);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: normal;
|
||||
tab-size: 4;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror p {
|
||||
margin: 0 0 1.1em;
|
||||
padding: 0;
|
||||
color: #2c3f51;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h1,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h2,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h3,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h4,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h5,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h6 {
|
||||
margin: 1.2em 0 0.6em;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #2c3f51;
|
||||
font-family: var(--editor-theme-font);
|
||||
font-weight: 700;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h1 {
|
||||
font-size: 41.6px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h2 {
|
||||
font-size: 34.4px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h3 {
|
||||
font-size: 27.2px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h4 {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h5 {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h6 {
|
||||
font-size: 13.6px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h2 {
|
||||
font-size: 1.85rem;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror strong,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror b {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror em {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror a:not(.edgeever-attachment-link) {
|
||||
color: #1980e6;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
transition: background-color 150ms ease-in-out, color 150ms ease-in-out, border-color 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror a:not(.edgeever-attachment-link):hover {
|
||||
color: #0f4d8a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror ul:not([data-type="taskList"]),
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror ol {
|
||||
margin: 0 0 1.1em;
|
||||
padding-left: 2em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror ul:not([data-type="taskList"]) li,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror ol li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror li::marker {
|
||||
color: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror blockquote {
|
||||
margin: 0 0 1.1em;
|
||||
border: 0;
|
||||
border-left: 10px solid rgb(102 128 153 / 7.5%);
|
||||
border-radius: 0 5px 5px 0;
|
||||
background: rgb(102 128 153 / 5%);
|
||||
padding: 15px 20px;
|
||||
color: #2c3f51;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror blockquote p:last-child,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror blockquote ul:last-child,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror blockquote ol:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror code {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: #f9f2f4;
|
||||
padding: 2px 4px;
|
||||
color: #c7254e;
|
||||
font-family: "Source Code Pro", Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.6;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror pre {
|
||||
margin: 0 0 1.1em;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
padding: 0;
|
||||
color: #333333;
|
||||
font-family: "Source Code Pro", Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.6;
|
||||
text-align: start;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror pre code,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-code-source {
|
||||
display: block;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: #23241f;
|
||||
padding: 1.3em 2em;
|
||||
color: #f8f8f2;
|
||||
font-family: "Source Code Pro", Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-code-copy-button {
|
||||
border-color: rgb(255 255 255 / 18%);
|
||||
background: rgb(35 36 31 / 88%);
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror hr:not([data-edgeever-merge-divider]) {
|
||||
height: 0;
|
||||
margin: 2em 0;
|
||||
border: 0;
|
||||
border-top: 1px solid rgb(102 128 153 / 10%);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror img {
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .tableWrapper {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 0 0 20px;
|
||||
overflow-x: auto;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror table {
|
||||
width: auto;
|
||||
min-width: 0 !important;
|
||||
margin: 0;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
table-layout: auto;
|
||||
color: #2c3f51;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror table col {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror th,
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror td {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
border: 1px solid #dddddd;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
color: #2c3f51;
|
||||
font-family: var(--editor-theme-font);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror th {
|
||||
border-bottom-width: 2px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror tbody tr:nth-child(even) td {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror tbody tr:nth-child(odd) td {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror tbody tr:hover td {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block {
|
||||
--theme-block-bg: rgb(102 128 153 / 5%);
|
||||
--theme-block-border: rgb(102 128 153 / 10%);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Marxico's source palette is light-only. Keep its typography in dark mode,
|
||||
* but remap every hard-coded reading color so new and existing notes remain legible.
|
||||
*/
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) {
|
||||
--editor-theme-bg: #19222b;
|
||||
--editor-theme-text: #dce3ea;
|
||||
--editor-theme-muted: #99a7b5;
|
||||
--editor-theme-heading: #e7edf2;
|
||||
--editor-theme-accent: #68b7ff;
|
||||
--editor-theme-soft: #202d38;
|
||||
--editor-theme-border: #344451;
|
||||
--editor-theme-code-bg: #2b2027;
|
||||
--editor-theme-code-text: #f0a6bd;
|
||||
--editor-theme-table-stripe: #1d2832;
|
||||
--editor-theme-table-hover: #24323e;
|
||||
color-scheme: dark;
|
||||
background: var(--editor-theme-bg) !important;
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror {
|
||||
background: var(--editor-theme-bg);
|
||||
color: var(--editor-theme-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror p {
|
||||
color: var(--editor-theme-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h1,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h2,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h3,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h4,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h5,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror h6 {
|
||||
color: var(--editor-theme-heading);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror a:not(.edgeever-attachment-link) {
|
||||
color: var(--editor-theme-accent);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror a:not(.edgeever-attachment-link):hover {
|
||||
color: #9acfff;
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror blockquote {
|
||||
border-left-color: var(--editor-theme-border);
|
||||
background: var(--editor-theme-soft);
|
||||
color: var(--editor-theme-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror code {
|
||||
background: var(--editor-theme-code-bg);
|
||||
color: var(--editor-theme-code-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror pre {
|
||||
background: #202830;
|
||||
color: var(--editor-theme-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror hr:not([data-edgeever-merge-divider]) {
|
||||
border-top-color: var(--editor-theme-border);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror table,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror th,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror td {
|
||||
color: var(--editor-theme-text);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror th,
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror td {
|
||||
border-color: var(--editor-theme-border);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror tbody tr:nth-child(odd) td {
|
||||
background: var(--editor-theme-table-stripe);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror tbody tr:hover td {
|
||||
background: var(--editor-theme-table-hover);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block {
|
||||
--theme-block-bg: var(--editor-theme-soft);
|
||||
--theme-block-border: var(--editor-theme-border);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const PRESET_THEME_FILES = [
|
||||
"outline-emerald.css",
|
||||
"wechat-green.css",
|
||||
"modern-mint.css",
|
||||
"marxico.css",
|
||||
];
|
||||
|
||||
const readStyle = (relativePath) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||||
@@ -52,7 +53,7 @@ describe("editor typography contract", () => {
|
||||
});
|
||||
|
||||
test("does not let preset themes override body rhythm", () => {
|
||||
for (const filename of PRESET_THEME_FILES) {
|
||||
for (const filename of PRESET_THEME_FILES.filter((filename) => filename !== "marxico.css")) {
|
||||
const source = readStyle(`./editor-themes/${filename}`);
|
||||
const editorRules = declarationsForSelector(source, ".ProseMirror");
|
||||
const paragraphRules = declarationsForSelector(source, ".ProseMirror p");
|
||||
@@ -67,4 +68,27 @@ describe("editor typography contract", () => {
|
||||
expect(listRules).not.toMatch(/(?:line-height|margin)(?:-[a-z]+)?\s*:/);
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps the Marxico preset aligned with the source theme", () => {
|
||||
const source = readStyle("./editor-themes/marxico.css");
|
||||
const themeRules = declarationsForSelector(source, '[data-editor-theme="marxico"]:not([data-editor-theme="default"])');
|
||||
const headingRules = declarationsForSelector(source, ".ProseMirror h1");
|
||||
const quoteRules = declarationsForSelector(source, ".ProseMirror blockquote");
|
||||
const inlineCodeRules = declarationsForSelector(source, ".ProseMirror code");
|
||||
const paragraphRules = declarationsForSelector(source, ".ProseMirror p");
|
||||
const preCodeRules = declarationsForSelector(source, ".ProseMirror pre code");
|
||||
const tableRules = declarationsForSelector(source, ".ProseMirror table");
|
||||
|
||||
expect(themeRules).toMatch(/--editor-theme-text\s*:\s*#2c3f51/);
|
||||
expect(themeRules).toMatch(/--editor-theme-accent\s*:\s*#1980e6/);
|
||||
expect(headingRules).toMatch(/font-size\s*:\s*41\.6px/);
|
||||
expect(quoteRules).toMatch(/border-left\s*:\s*10px solid/);
|
||||
expect(inlineCodeRules).toMatch(/color\s*:\s*#c7254e/);
|
||||
expect(paragraphRules).toMatch(/font-size\s*:\s*16px/);
|
||||
expect(paragraphRules).toMatch(/margin\s*:\s*0 0 1\.1em/);
|
||||
expect(preCodeRules).toMatch(/background\s*:\s*#23241f/);
|
||||
expect(preCodeRules).toMatch(/padding\s*:\s*1\.3em 2em/);
|
||||
expect(tableRules).toMatch(/width\s*:\s*auto/);
|
||||
expect(tableRules).toMatch(/min-width\s*:\s*0 !important/);
|
||||
});
|
||||
});
|
||||
@@ -173,55 +173,61 @@ html[data-edgeever-environment="demo"] body::after {
|
||||
|
||||
:root.dark {
|
||||
color-scheme: dark;
|
||||
--background: 222 47% 11%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 217 33% 17%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 217 33% 17%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 142 71% 45%;
|
||||
--primary-foreground: 222 47% 11%;
|
||||
--secondary: 217 33% 24%;
|
||||
--secondary-foreground: 142 71% 75%;
|
||||
--muted: 217 33% 24%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
--accent: 217 33% 24%;
|
||||
--accent-foreground: 142 71% 75%;
|
||||
--background: 210 26% 9%;
|
||||
--foreground: 210 20% 89%;
|
||||
--card: 211 22% 14%;
|
||||
--card-foreground: 210 20% 89%;
|
||||
--popover: 211 23% 15%;
|
||||
--popover-foreground: 210 20% 89%;
|
||||
--primary: 160 76% 36%;
|
||||
--primary-foreground: 150 33% 97%;
|
||||
--secondary: 210 20% 18%;
|
||||
--secondary-foreground: 160 35% 72%;
|
||||
--muted: 210 17% 18%;
|
||||
--muted-foreground: 210 12% 62%;
|
||||
--accent: 210 18% 20%;
|
||||
--accent-foreground: 160 40% 75%;
|
||||
--destructive: 0 72% 51%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 215 28% 28%;
|
||||
--input: 215 28% 28%;
|
||||
--ring: 142 71% 45%;
|
||||
--slate-50-rgb: 15 23 42;
|
||||
--slate-100-rgb: 30 41 59;
|
||||
--slate-200-rgb: 51 65 85;
|
||||
--slate-300-rgb: 71 85 105;
|
||||
--slate-400-rgb: 100 116 139;
|
||||
--slate-500-rgb: 148 163 184;
|
||||
--slate-600-rgb: 203 213 225;
|
||||
--slate-700-rgb: 226 232 240;
|
||||
--slate-800-rgb: 241 245 249;
|
||||
--slate-900-rgb: 248 250 252;
|
||||
--dark-surface-rgb: 30 41 59;
|
||||
--brand-green: #4ade80;
|
||||
--brand-green-hover: #22c55e;
|
||||
--brand-green-muted: #14532d;
|
||||
--brand-green-soft: #052e16;
|
||||
--brand-green-border: #166534;
|
||||
--brand-green-text: #86efac;
|
||||
--brand-green-rgb: 74 222 128;
|
||||
--brand-green-hover-rgb: 34 197 94;
|
||||
--brand-green-50-rgb: 20 83 45;
|
||||
--brand-green-100-rgb: 22 101 52;
|
||||
--brand-green-200-rgb: 34 133 67;
|
||||
--brand-green-300-rgb: 74 222 128;
|
||||
--brand-green-400-rgb: 134 239 172;
|
||||
--brand-green-500-rgb: 74 222 128;
|
||||
--brand-green-600-rgb: 34 197 94;
|
||||
--brand-green-700-rgb: 134 239 172;
|
||||
--brand-green-800-rgb: 187 247 208;
|
||||
--brand-green-900-rgb: 220 252 231;
|
||||
--brand-green-950-rgb: 240 253 244;
|
||||
--destructive-foreground: 210 20% 96%;
|
||||
--border: 210 16% 24%;
|
||||
--input: 210 16% 24%;
|
||||
--ring: 160 76% 36%;
|
||||
--workspace-canvas: #10161c;
|
||||
--workspace-sidebar: #131a21;
|
||||
--workspace-memo-list: #161e26;
|
||||
--workspace-editor: #19222b;
|
||||
--workspace-divider: #2a3540;
|
||||
--workspace-selection: #23323c;
|
||||
--slate-50-rgb: 16 22 28;
|
||||
--slate-100-rgb: 23 31 39;
|
||||
--slate-200-rgb: 39 50 61;
|
||||
--slate-300-rgb: 56 69 82;
|
||||
--slate-400-rgb: 80 95 110;
|
||||
--slate-500-rgb: 136 151 165;
|
||||
--slate-600-rgb: 174 187 199;
|
||||
--slate-700-rgb: 200 210 219;
|
||||
--slate-800-rgb: 220 227 234;
|
||||
--slate-900-rgb: 235 240 244;
|
||||
--dark-surface-rgb: 25 34 43;
|
||||
--brand-green: #16a06e;
|
||||
--brand-green-hover: #24b986;
|
||||
--brand-green-muted: #183a31;
|
||||
--brand-green-soft: #122a24;
|
||||
--brand-green-border: #286451;
|
||||
--brand-green-text: #68d6b0;
|
||||
--brand-green-rgb: 22 160 110;
|
||||
--brand-green-hover-rgb: 36 185 134;
|
||||
--brand-green-50-rgb: 18 42 36;
|
||||
--brand-green-100-rgb: 24 58 49;
|
||||
--brand-green-200-rgb: 40 100 81;
|
||||
--brand-green-300-rgb: 58 181 139;
|
||||
--brand-green-400-rgb: 104 214 176;
|
||||
--brand-green-500-rgb: 22 160 110;
|
||||
--brand-green-600-rgb: 36 185 134;
|
||||
--brand-green-700-rgb: 104 214 176;
|
||||
--brand-green-800-rgb: 159 229 204;
|
||||
--brand-green-900-rgb: 202 242 225;
|
||||
--brand-green-950-rgb: 232 249 241;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -407,33 +413,42 @@ html[data-edgeever-extension-theme] .edgeever-workspace-selection {
|
||||
}
|
||||
|
||||
:root.dark body {
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
background: var(--workspace-canvas);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-shell {
|
||||
background: var(--workspace-canvas);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-sidebar {
|
||||
background: var(--workspace-sidebar);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-memo-list {
|
||||
background: var(--workspace-memo-list);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-shell,
|
||||
:root.dark .edgeever-workspace-sidebar,
|
||||
:root.dark .edgeever-workspace-memo-list,
|
||||
:root.dark .edgeever-workspace-editor {
|
||||
background: #0f172a;
|
||||
background: var(--workspace-editor);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-sidebar,
|
||||
:root.dark .edgeever-workspace-memo-list {
|
||||
border-color: #334155;
|
||||
border-color: var(--workspace-divider);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-sidebar-footer {
|
||||
background: rgb(15 23 42 / 88%);
|
||||
background: color-mix(in srgb, var(--workspace-sidebar) 90%, transparent);
|
||||
}
|
||||
|
||||
:root.dark .edgeever-workspace-selection,
|
||||
:root.dark .edgeever-workspace-selection-desktop {
|
||||
background: #1e293b;
|
||||
background: var(--workspace-selection) !important;
|
||||
}
|
||||
|
||||
:root.dark .ProseMirror {
|
||||
color: #f8fafc;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -1445,19 +1460,19 @@ textarea {
|
||||
:root.dark [class~="bg-slate-900"],
|
||||
:root.dark [class*="bg-slate-900/"] { background-color: rgb(var(--slate-900-rgb)) !important; }
|
||||
|
||||
:root.dark [class~="text-slate-50"] { color: #f8fafc !important; }
|
||||
:root.dark [class~="text-slate-100"] { color: #f1f5f9 !important; }
|
||||
:root.dark [class~="text-slate-200"] { color: #e2e8f0 !important; }
|
||||
:root.dark [class~="text-slate-300"] { color: #cbd5e1 !important; }
|
||||
:root.dark [class~="text-slate-350"] { color: #aebdce !important; }
|
||||
:root.dark [class~="text-slate-50"] { color: #ebf0f4 !important; }
|
||||
:root.dark [class~="text-slate-100"] { color: #e2e8ed !important; }
|
||||
:root.dark [class~="text-slate-200"] { color: #d4dce3 !important; }
|
||||
:root.dark [class~="text-slate-300"] { color: #c4ced7 !important; }
|
||||
:root.dark [class~="text-slate-350"] { color: #aab7c3 !important; }
|
||||
:root.dark [class~="text-slate-400"],
|
||||
:root.dark [class*="text-slate-400/"] { color: rgb(148 163 184 / var(--dark-utility-alpha, 1)) !important; }
|
||||
:root.dark [class~="text-slate-500"] { color: #cbd5e1 !important; }
|
||||
:root.dark [class*="text-slate-400/"] { color: rgb(130 145 161 / var(--dark-utility-alpha, 1)) !important; }
|
||||
:root.dark [class~="text-slate-500"] { color: #99a7b5 !important; }
|
||||
:root.dark [class~="text-slate-600"],
|
||||
:root.dark [class~="text-slate-650"] { color: #e2e8f0 !important; }
|
||||
:root.dark [class~="text-slate-700"] { color: #e2e8f0 !important; }
|
||||
:root.dark [class~="text-slate-800"] { color: #f1f5f9 !important; }
|
||||
:root.dark [class~="text-slate-900"] { color: #f8fafc !important; }
|
||||
:root.dark [class~="text-slate-650"] { color: #b7c2cd !important; }
|
||||
:root.dark [class~="text-slate-700"] { color: #c8d2db !important; }
|
||||
:root.dark [class~="text-slate-800"] { color: #dce3ea !important; }
|
||||
:root.dark [class~="text-slate-900"] { color: #e7edf2 !important; }
|
||||
|
||||
:root.dark [class~="border-slate-50"],
|
||||
:root.dark [class*="border-slate-50/"] { border-color: rgb(var(--slate-50-rgb)) !important; }
|
||||
|
||||
@@ -123,6 +123,85 @@ input {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-card {
|
||||
width: min(100%, 420px);
|
||||
border: 1px solid #fecdd3;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 14px 36px rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-card h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-card p {
|
||||
margin: 10px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-actions button {
|
||||
min-height: 40px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 9px;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.mobile-editor-fatal-actions button.primary {
|
||||
border-color: #16a06e;
|
||||
background: #16a06e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:root.dark .mobile-editor-fatal {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
:root.dark .mobile-editor-fatal-card {
|
||||
border-color: #7f1d1d;
|
||||
background: #19222b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
:root.dark .mobile-editor-fatal-card p {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:root.dark .mobile-editor-fatal-actions button {
|
||||
border-color: #475569;
|
||||
background: #1e293b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
:root.dark .mobile-editor-fatal-actions button.primary {
|
||||
border-color: #16a06e;
|
||||
background: #16a06e;
|
||||
}
|
||||
|
||||
.mobile-editor-title,
|
||||
.mobile-editor-tags {
|
||||
width: 100%;
|
||||
|
||||
@@ -303,13 +303,15 @@ export default defineConfig({
|
||||
groups: [
|
||||
{
|
||||
name: "vendor-code-highlight",
|
||||
test: /node_modules[\\/](?:lowlight|highlight\.js|@tiptap[\\/]extension-code-block-lowlight)[\\/]/,
|
||||
test: /node_modules[\\/](?:lowlight|highlight\.js)[\\/]/,
|
||||
priority: 50,
|
||||
// lowlight registers highlight.js languages through a cyclic
|
||||
// module graph. Splitting this group by size can evaluate a
|
||||
// language before its constructor is initialized in packaged
|
||||
// file:// desktop builds, leaving the entire window blank.
|
||||
// Keep the graph atomic and load it lazily instead.
|
||||
// file:// desktop builds, leaving the entire window blank. Keep
|
||||
// that graph atomic, but leave TipTap's lightweight adapter in
|
||||
// the regular extension group so plain mobile code blocks do not
|
||||
// inherit the highlighter as a startup dependency.
|
||||
},
|
||||
{
|
||||
name: "vendor-react",
|
||||
|
||||
@@ -218,6 +218,7 @@ export const enUS = {
|
||||
"outline-emerald": "Outline Emerald",
|
||||
"wechat-green": "WeChat Classic Green",
|
||||
"modern-mint": "Modern Mint",
|
||||
marxico: "Marxico Classic",
|
||||
custom: "Custom",
|
||||
},
|
||||
customEditorTheme: {
|
||||
|
||||
@@ -218,6 +218,7 @@ export const zhCN = {
|
||||
"outline-emerald": "Outline 品牌绿",
|
||||
"wechat-green": "微信经典绿",
|
||||
"modern-mint": "极客薄荷绿",
|
||||
marxico: "马克飞象经典",
|
||||
custom: "自定义",
|
||||
},
|
||||
customEditorTheme: {
|
||||
|
||||
@@ -120,12 +120,23 @@ describe("mobile app scope", () => {
|
||||
)?.[0];
|
||||
|
||||
expect(createMemoSource).toMatch(/<LocalTiptapEditor\s+autoFocus\s/);
|
||||
expect(createMemoSource).toContain("scheduleBodyKeyboard(60)");
|
||||
expect(createMemoSource).toContain("scheduleBodyKeyboard(180, false)");
|
||||
expect(titleInput).toBeDefined();
|
||||
expect(titleInput).not.toContain("autoFocus");
|
||||
expect(createMemoSource).not.toContain("scheduleTitleFocus");
|
||||
});
|
||||
|
||||
test("keeps editor startup recoverable and avoids competing autofocus paths", () => {
|
||||
expect(workspaceSource).toContain("MOBILE_EDITOR_STARTUP_TIMEOUT_MS");
|
||||
expect(workspaceSource).toContain("MobileEditorStartupOverlay");
|
||||
expect(workspaceSource).toContain("key={editorStartup.attempt}");
|
||||
expect(localTiptapEditorSource).toContain("autofocus: false");
|
||||
expect(localTiptapEditorSource).toContain('import("mermaid/dist/mermaid.min.js")');
|
||||
expect(localTiptapEditorSource).toContain('import("beautiful-mermaid")');
|
||||
expect(localTiptapEditorSource).toContain('import("html-to-image")');
|
||||
expect(localTiptapEditorSource).not.toContain('import "mermaid/dist/mermaid.min.js"');
|
||||
});
|
||||
|
||||
test("declares iOS privacy strings and full-screen phone-on-iPad presentation", () => {
|
||||
const infoPlist = appJson.expo.ios?.infoPlist ?? {};
|
||||
expect(appJson.expo.ios?.supportsTablet).toBe(false);
|
||||
|
||||
Reference in new issue
Block a user