feat(editor): refine note image share themes with typography affinity and curated layout

This commit is contained in:
tianma-if committed 2026-10-16 00:10:18 +08:00
1 parent 5334b25fb3
commit 80038dd2d1
12 files changed
+1040 -120

No files matched your search

+15
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
parseDiagramDocument,
parseInfographicDocument,
parsePosterDocument,
parseTableDocument,
} from "@edgeever/shared";
import { computeDiagramLayoutResult } from "../../../packages/shared/src/diagram-layout.ts";
@@ -22,6 +23,8 @@ describe("demo seed catalog", () => {
expect(memoIds.size).toBe(DEMO_SEED_MEMOS.length);
expect(memoIds.has("memo_demo_overview")).toBe(true);
expect(memoIds.has("memo_demo_overview_en")).toBe(true);
expect(memoIds.has("memo_demo_poster")).toBe(true);
expect(memoIds.has("memo_demo_poster_en")).toBe(true);
expect(memoIds.has("memo_demo_infographic")).toBe(true);
expect(memoIds.has("memo_demo_infographic_en")).toBe(true);
expect(memoIds.has("memo_demo_table")).toBe(true);
@@ -64,6 +67,18 @@ describe("demo seed catalog", () => {
}
});
test("seeds one editable example for poster notes in each language", () => {
for (const suffix of ["", "_en"]) {
const posterMemo = DEMO_SEED_MEMOS.find((c) => c.id === `memo_demo_poster${suffix}`);
expect(posterMemo).toBeDefined();
const poster = parsePosterDocument(posterMemo?.markdown);
expect(poster).toBeDefined();
expect(poster?.templateId).toBe("editorial");
expect(poster?.elements.length).toBeGreaterThan(0);
expect(poster?.copy?.title).toBeDefined();
}
});
test("seeds one editable example for every native diagram kind in each language", () => {
for (const suffix of ["", "_en"]) {
const examples = [
+633 -59
View File
@@ -1,9 +1,11 @@
import {
serializeDiagramDocument,
serializeInfographicDocument,
serializePosterDocument,
serializeTableDocument,
type DiagramDocument,
type InfographicDocument,
type PosterDocument,
type TableDocument,
} from "@edgeever/shared";
import {
@@ -239,14 +241,14 @@ data
sequences
- label v1.0 架构奠基
desc 经典三栏美学、Serverless D1 存储与双向同步游标
- label v1.30 原生客户端
desc macOS 桌面端、Android (Expo) 与 iOS (SwiftUI) 全端覆盖
- label v1.50 AI 原生赋能
desc 原生 MCP 协议端点 (/mcp) 与端侧直连行内 AI 助手
- label v1.70 生产力矩阵
desc 浏览器剪藏扩展、可视化流程图、架构图与思维导图
- label v1.88 现代知识工作台
desc AI 智能信息图、多维结构化表格与全场景自建部署`;
- label v1.50 智能体与全端
desc 原生 MCP 协议端点 (/mcp)、双端原生移动客户端与剪藏矩阵
- label v1.88 结构化知识台
desc AI 智能信息图、多维结构化表格与可视化图表体系
- label v1.100 双协议与生态
desc 桌面端 ACP 深度驱动本地 Agent、知识库独立发布与全平台客户端
- label v1.120 视觉表现与排版
desc 原生海报笔记与画布排版、微信聊天完整归档与全场景自建部署`;
export const DEMO_INFOGRAPHIC_SYNTAX_EN = `infographic sequence-timeline-rounded-rect-node
data
@@ -254,14 +256,14 @@ data
sequences
- label v1.0 Foundation
desc Classic three-pane layout, Serverless D1 storage & sync cursors
- label v1.30 Native Apps
desc macOS Desktop, Android (Expo) & iOS (SwiftUI) native clients
- label v1.50 AI Native
desc Native MCP endpoint (/mcp) & inline private AI writing assistant
- label v1.70 Productivity
desc Web Clipper extension, visual flowcharts, architectures & mind maps
- label v1.88 Modern Workbench
desc AI-driven infographics, structured tables & flexible self-hosting`;
- label v1.50 Agents & Mobile
desc Native MCP endpoint (/mcp), native mobile apps & clipping matrix
- label v1.88 Structured Workspace
desc AI-driven infographics, structured tables & visual diagram suite
- label v1.100 Dual Protocols & Web
desc Desktop ACP for local agents, public notebook site publishing & full platform apps
- label v1.120 Visual Canvas & Layout
desc Native poster notes & canvas layout, WeChat chat archiving & flexible self-hosting`;
export const DEMO_INFOGRAPHIC_DOCUMENT_ZH: InfographicDocument = {
schemaVersion: 1,
@@ -273,6 +275,484 @@ export const DEMO_INFOGRAPHIC_DOCUMENT_EN: InfographicDocument = {
syntax: DEMO_INFOGRAPHIC_SYNTAX_EN,
};
export const DEMO_POSTER_DOCUMENT_ZH: PosterDocument = {
schemaVersion: 1,
width: 1080,
height: 1440,
background: "#f4efe4",
templateId: "editorial",
copy: {
badge: "EDGEEVER · 知识艺术",
title: "极客的下一代\n数字第二大脑",
subtitle: "重拾经典三栏工作流,融入 AI 智能体与无损开放数据资产。",
},
elements: [
{
id: "accent-bar",
name: "accent-bar",
type: "rect",
x: 96,
y: 110,
width: 120,
height: 12,
fill: "#16a06e",
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "badge-text",
name: "badge-text",
type: "text",
role: "badge",
text: "EDGEEVER · 知识艺术",
x: 96,
y: 150,
width: 888,
height: 40,
rotation: 0,
opacity: 1,
locked: false,
fill: "#0f766e",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "heading",
name: "heading",
type: "text",
role: "title",
text: "极客的下一代\n数字第二大脑",
x: 96,
y: 220,
width: 888,
height: 220,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 84,
fontFamily: "serif",
fontWeight: "bold",
textAlign: "left",
lineHeight: 1.25,
},
{
id: "subtitle",
name: "subtitle",
type: "text",
role: "subtitle",
text: "重拾经典三栏工作流,融入 AI 智能体与无损开放数据资产。",
x: 96,
y: 470,
width: 888,
height: 80,
rotation: 0,
opacity: 1,
locked: false,
fill: "#3f3f46",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
},
{
id: "card-1-bg",
name: "card-1-bg",
type: "rect",
x: 96,
y: 590,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-1-title",
name: "card-1-title",
type: "text",
text: "01 · 原生 MCP / ACP 双协议支持",
x: 136,
y: 630,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-1-body",
name: "card-1-body",
type: "text",
text: "原生对接 Claude、Cursor、Codex 等外部 Agent,桌面端无缝协同本地智能体。",
x: 136,
y: 695,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
{
id: "card-2-bg",
name: "card-2-bg",
type: "rect",
x: 96,
y: 840,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-2-title",
name: "card-2-title",
type: "text",
text: "02 · 零成本 Serverless 与自由部署",
x: 136,
y: 880,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-2-body",
name: "card-2-body",
type: "text",
text: "跑在 Cloudflare 免费额度内或一键 Docker 自建,标准 SQLite 存储,导出无阻碍。",
x: 136,
y: 945,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
{
id: "card-3-bg",
name: "card-3-bg",
type: "rect",
x: 96,
y: 1090,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-3-title",
name: "card-3-title",
type: "text",
text: "03 · 全平台客户端与独立知识库",
x: 136,
y: 1130,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-3-body",
name: "card-3-body",
type: "text",
text: "macOS、Windows、Linux、iOS 与 Android 覆盖,支持一键将笔记本发布为知识库网站。",
x: 136,
y: 1195,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
],
};
export const DEMO_POSTER_DOCUMENT_EN: PosterDocument = {
schemaVersion: 1,
width: 1080,
height: 1440,
background: "#f4efe4",
templateId: "editorial",
copy: {
badge: "EDGEEVER · KNOWLEDGE ART",
title: "Next-Gen Second Brain\nfor Geeks & Creators",
subtitle: "Revive the beloved three-pane workflow with native AI agents and total data ownership.",
},
elements: [
{
id: "accent-bar",
name: "accent-bar",
type: "rect",
x: 96,
y: 110,
width: 120,
height: 12,
fill: "#16a06e",
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "badge-text",
name: "badge-text",
type: "text",
role: "badge",
text: "EDGEEVER · KNOWLEDGE ART",
x: 96,
y: 150,
width: 888,
height: 40,
rotation: 0,
opacity: 1,
locked: false,
fill: "#0f766e",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "heading",
name: "heading",
type: "text",
role: "title",
text: "Next-Gen Second Brain\nfor Geeks & Creators",
x: 96,
y: 220,
width: 888,
height: 220,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 80,
fontFamily: "serif",
fontWeight: "bold",
textAlign: "left",
lineHeight: 1.25,
},
{
id: "subtitle",
name: "subtitle",
type: "text",
role: "subtitle",
text: "Revive the beloved three-pane workflow with native AI agents and total data ownership.",
x: 96,
y: 470,
width: 888,
height: 80,
rotation: 0,
opacity: 1,
locked: false,
fill: "#3f3f46",
fontSize: 30,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
},
{
id: "card-1-bg",
name: "card-1-bg",
type: "rect",
x: 96,
y: 590,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-1-title",
name: "card-1-title",
type: "text",
text: "01 · Native MCP & ACP Dual Protocols",
x: 136,
y: 630,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-1-body",
name: "card-1-body",
type: "text",
text: "Connect Claude, Cursor, and Codex via MCP, and drive local agents seamlessly via ACP on desktop.",
x: 136,
y: 695,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
{
id: "card-2-bg",
name: "card-2-bg",
type: "rect",
x: 96,
y: 840,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-2-title",
name: "card-2-title",
type: "text",
text: "02 · 100% Free Serverless & Self-Hosting",
x: 136,
y: 880,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-2-body",
name: "card-2-body",
type: "text",
text: "Run within Cloudflare free quotas or one-command Docker on your VPS/NAS with standard SQLite storage.",
x: 136,
y: 945,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
{
id: "card-3-bg",
name: "card-3-bg",
type: "rect",
x: 96,
y: 1090,
width: 888,
height: 220,
fill: "#ffffff",
radius: 20,
rotation: 0,
opacity: 1,
locked: false,
},
{
id: "card-3-title",
name: "card-3-title",
type: "text",
text: "03 · Full-Platform Apps & Knowledge Sites",
x: 136,
y: 1130,
width: 808,
height: 48,
rotation: 0,
opacity: 1,
locked: false,
fill: "#07130b",
fontSize: 32,
fontFamily: "sans-serif",
fontWeight: "bold",
textAlign: "left",
},
{
id: "card-3-body",
name: "card-3-body",
type: "text",
text: "Native coverage on macOS, Windows, Linux, iOS & Android; publish entire notebooks as standalone sites.",
x: 136,
y: 1195,
width: 808,
height: 70,
rotation: 0,
opacity: 1,
locked: false,
fill: "#71717a",
fontSize: 24,
fontFamily: "sans-serif",
fontWeight: "normal",
textAlign: "left",
lineHeight: 1.4,
},
],
};
export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
schemaVersion: 1,
fields: [
@@ -284,6 +764,50 @@ export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
{ id: "fld_docs", name: "规格文档", type: "attachment" },
],
records: [
{
id: "rec_poster",
cells: {
fld_feature: "原生海报笔记与画布排版",
fld_category: "图表与可视化",
fld_status: "已正式发布",
fld_verified: true,
fld_date: "2026-10-05",
fld_docs: [],
},
},
{
id: "rec_acp",
cells: {
fld_feature: "桌面端 ACP 本地智能体协同",
fld_category: "AI 智能体",
fld_status: "已正式发布",
fld_verified: true,
fld_date: "2026-10-08",
fld_docs: [],
},
},
{
id: "rec_notebook_site",
cells: {
fld_feature: "笔记本公开发布独立知识库",
fld_category: "数据与架构",
fld_status: "已正式发布",
fld_verified: true,
fld_date: "2026-10-10",
fld_docs: [],
},
},
{
id: "rec_platforms",
cells: {
fld_feature: "全平台客户端 (macOS / Win / Linux / iOS / Android)",
fld_category: "多端客户端",
fld_status: "已正式发布",
fld_verified: true,
fld_date: "2026-09-28",
fld_docs: [{ resourceId: "res_demo_attachment_bundle_zip", filename: "edgeever-attachment-demo.zip", mimeType: "application/zip", byteSize: 153 }],
},
},
{
id: "rec_infographic",
cells: {
@@ -298,7 +822,7 @@ export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
{
id: "rec_table",
cells: {
fld_feature: "多维结构化表格与表单",
fld_feature: "多维结构化表格与公开表单",
fld_category: "图表与可视化",
fld_status: "已正式发布",
fld_verified: true,
@@ -309,7 +833,7 @@ export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
{
id: "rec_mcp",
cells: {
fld_feature: "原生 MCP 端点 (/mcp)",
fld_feature: "原生 MCP 服务端点 (/mcp)",
fld_category: "AI 智能体",
fld_status: "已正式发布",
fld_verified: true,
@@ -320,7 +844,7 @@ export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
{
id: "rec_clipper",
cells: {
fld_feature: "Web Clipper 网页与推文剪藏",
fld_feature: "Web Clipper 剪藏与微信聊天归档",
fld_category: "多端客户端",
fld_status: "已正式发布",
fld_verified: true,
@@ -328,17 +852,6 @@ export const DEMO_TABLE_DOCUMENT_ZH: TableDocument = {
fld_docs: [],
},
},
{
id: "rec_windows",
cells: {
fld_feature: "Windows x64 桌面端 Preview",
fld_category: "多端客户端",
fld_status: "已正式发布",
fld_verified: true,
fld_date: "2026-09-26",
fld_docs: [{ resourceId: "res_demo_attachment_bundle_zip", filename: "edgeever-attachment-demo.zip", mimeType: "application/zip", byteSize: 153 }],
},
},
],
view: {
filters: [],
@@ -357,6 +870,50 @@ export const DEMO_TABLE_DOCUMENT_EN: TableDocument = {
{ id: "fld_docs", name: "Documents", type: "attachment" },
],
records: [
{
id: "rec_poster",
cells: {
fld_feature: "Native Poster Notes & Canvas Layout",
fld_category: "Visuals & Data",
fld_status: "Released",
fld_verified: true,
fld_date: "2026-10-05",
fld_docs: [],
},
},
{
id: "rec_acp",
cells: {
fld_feature: "Desktop ACP Local Agent Integration",
fld_category: "AI Agents",
fld_status: "Released",
fld_verified: true,
fld_date: "2026-10-08",
fld_docs: [],
},
},
{
id: "rec_notebook_site",
cells: {
fld_feature: "Notebook Knowledge Base Web Publishing",
fld_category: "Data & Architecture",
fld_status: "Released",
fld_verified: true,
fld_date: "2026-10-10",
fld_docs: [],
},
},
{
id: "rec_platforms",
cells: {
fld_feature: "Full-Platform Apps (macOS / Win / Linux / iOS / Android)",
fld_category: "Cross-Platform",
fld_status: "Released",
fld_verified: true,
fld_date: "2026-09-28",
fld_docs: [{ resourceId: "res_demo_attachment_bundle_zip", filename: "edgeever-attachment-demo.zip", mimeType: "application/zip", byteSize: 153 }],
},
},
{
id: "rec_infographic",
cells: {
@@ -371,7 +928,7 @@ export const DEMO_TABLE_DOCUMENT_EN: TableDocument = {
{
id: "rec_table",
cells: {
fld_feature: "Structured Table & Forms",
fld_feature: "Structured Tables & Public Forms",
fld_category: "Visuals & Data",
fld_status: "Released",
fld_verified: true,
@@ -393,7 +950,7 @@ export const DEMO_TABLE_DOCUMENT_EN: TableDocument = {
{
id: "rec_clipper",
cells: {
fld_feature: "Web Clipper Page & Post Clips",
fld_feature: "Web Clipper & WeChat Chat Archiving",
fld_category: "Cross-Platform",
fld_status: "Released",
fld_verified: true,
@@ -401,17 +958,6 @@ export const DEMO_TABLE_DOCUMENT_EN: TableDocument = {
fld_docs: [],
},
},
{
id: "rec_windows",
cells: {
fld_feature: "Windows x64 Desktop Preview",
fld_category: "Cross-Platform",
fld_status: "Released",
fld_verified: true,
fld_date: "2026-09-26",
fld_docs: [{ resourceId: "res_demo_attachment_bundle_zip", filename: "edgeever-attachment-demo.zip", mimeType: "application/zip", byteSize: 153 }],
},
},
],
view: {
filters: [],
@@ -440,11 +986,11 @@ export const DEMO_SEED_MEMOS_ZH = [
| **源代码** | 闭源 | 核心闭源,文件开放 | **整栈开源 (AGPL-3.0)** |
| **云端托管成本** | 商业订阅高昂($10+/月) | 官方云同步收费($5+/月) | **100% 永久免费 (Cloudflare 免费额度 / Docker 自建)** |
| **数据资产所有权** | 专有格式封闭,导出困难 | 本地 Markdown,移动端同步繁琐 | **完全自主掌控 (D1 SQLite 数据库 / R2 / 无损 ZIP 导出)** |
| **写作与表达矩阵** | 仅富文本,排版易错乱 | 纯 Markdown,缺乏沉浸所见即所得 | **双视图自由切换 (所见即所得 ⇄ 源码) + 原生架构图/流程图/思维导图/AI信息图/多维结构化表格** |
| **写作与表达矩阵** | 仅富文本,排版易错乱 | 纯 Markdown,缺乏沉浸所见即所得 | **双视图自由切换 (所见即所得 ⇄ 源码) + 原生架构图/流程图/思维导图/AI信息图/多维结构化表格/原生海报笔记** |
| **自媒体排版分发** | 无样式优化,格式丢失 | 需借助外部排版扩展或工具 | **一键复制到微信公众号 / Substack / Medium / WordPress** |
| **社交卡片分享** | 截图粗糙或无排版设计 | 需第三方插件实现 | **内置 8 款精美海报主题,支持自定义字体与 PNG/JPEG 导出** |
| **AI 原生集成生态** | 封闭收费,仅限特定功能 | 需配置复杂的第三方插件 | **原生内置 MCP 服务端点 (\`/mcp\`) 与行内 AI 智能体协同** |
| **全平台多端覆盖** | 限制免费设备登录数量 | 多端同步与移动端体验门槛高 | **Web / PWA / Android / iOS / macOS / Windows Preview** |
| **AI 原生集成生态** | 封闭收费,仅限特定功能 | 需配置复杂的第三方插件 | **原生内置 MCP 服务端点 (\`/mcp\`)、桌面端 ACP 本地智能体协同与行内 AI 助手** |
| **全平台多端覆盖** | 限制免费设备登录数量 | 多端同步与移动端体验门槛高 | **Web / PWA / Android / iOS / macOS / Windows / Linux AppImage** |
---
@@ -457,7 +1003,7 @@ flowchart TD
subgraph MultiClients["全端客户端矩阵"]
C1["Web / PWA (离线草稿 & 增量同步)"]
C2["macOS 桌面端 (Electron + Rust Sidecar)"]
C3["Windows 桌面端 (Preview + 上下文集成)"]
C3["Windows / Linux 桌面端 (ACP 智能体集成)"]
C4["Android 原生 App (Expo + SQLite)"]
C5["iOS 原生 App (SwiftUI + GRDB)"]
C6["浏览器剪藏插件 (Chrome / Edge / Firefox)"]
@@ -642,14 +1188,23 @@ sequenceDiagram
> 💡 **快速体验小贴士**:
> 1. 点击右上角 **\`</>\`** 体验丝滑的双视图切换;
> 2. 浏览侧边栏中的 **AI 信息图** 与 **多维结构化表格** 示例,感受下一代知识库形态;
> 2. 浏览侧边栏中的 **AI 信息图**、**多维结构化表格** 与 **视觉排版海报** 示例,感受下一代知识库形态;
> 3. 按下 **\`Cmd/Ctrl + E\`** 试一试阅读保护模式,锁定只读避免误改内容;
> 4. 按下 **\`Cmd/Ctrl + O\`** 呼出快速切换器 (Quick Switcher);
> 5. 点击顶部 **微信图标**,体验一键带格式排版复制;
> 6. 点击 **“分享为卡片”**,挑选一款你心仪的社交海报风格并导出;
> 7. 随时在侧边栏或设置中点击 **“恢复 Demo 数据”**,一键重置演示工作区。
> 5. 体验知识库发布功能:右键笔记本可一键公开发布为带目录树的独立静态知识库网站;
> 6. 点击顶部 **微信图标**,体验一键带格式排版复制;
> 7. 点击 **“分享为卡片”**,挑选一款你心仪的社交海报风格并导出;
> 8. 随时在侧边栏或设置中点击 **“恢复 Demo 数据”**,一键重置演示工作区。
`,
},
{
id: "memo_demo_poster",
notebookId: "nb_demo_features",
title: "视觉排版:EdgeEver 极客第二大脑设计海报",
tags: ["poster", "design", "visual-layout", "branding"],
isPinned: false,
markdown: serializePosterDocument(DEMO_POSTER_DOCUMENT_ZH),
},
{
id: "memo_demo_infographic",
notebookId: "nb_demo_features",
@@ -709,6 +1264,20 @@ export const DEMO_SEED_REVISIONS = [
markdown:
"## Welcome to EdgeEver (Initial Draft)\n\n- Classic Evernote 3-pane layout & Serverless self-hosted\n- Visual table editing & Markdown source toggle\n- Native MCP & AI agent synergy",
},
{
id: "rev_demo_poster_1",
memoId: "memo_demo_poster",
revision: 1,
title: "视觉排版:EdgeEver 极客第二大脑设计海报",
markdown: serializePosterDocument(DEMO_POSTER_DOCUMENT_ZH),
},
{
id: "rev_demo_poster_1_en",
memoId: "memo_demo_poster_en",
revision: 1,
title: "Visual Design: EdgeEver Geek Second Brain Poster Note",
markdown: serializePosterDocument(DEMO_POSTER_DOCUMENT_EN),
},
{
id: "rev_demo_infographic_1",
memoId: "memo_demo_infographic",
@@ -770,11 +1339,11 @@ export const DEMO_MEMO_ENGLISH = {
| **Source Code** | Closed source | Closed-source core, open files | **Full-stack open source (AGPL-3.0)** |
| **Hosting Cost** | Costly commercial subscriptions ($10+/mo) | Official cloud sync subscription ($5+/mo) | **100% Free Forever (Cloudflare Free Tier / Docker Self-Hosted)** |
| **Data Ownership** | Proprietary lock-in, hard to export | Local Markdown, cumbersome mobile sync | **Full Ownership (D1 SQLite, R2 Storage, Lossless ZIP Archives)** |
| **Writing & Expressive Matrix** | Rich text only, fragile formatting | Plain Markdown, lacks WYSIWYG flow | **Seamless Dual-View (WYSIWYG ⇄ Source) + Native Architecture/Flowchart/Mind-Map/AI Infographic/Structured Table** |
| **Writing & Expressive Matrix** | Rich text only, fragile formatting | Plain Markdown, lacks WYSIWYG flow | **Seamless Dual-View (WYSIWYG ⇄ Source) + Native Architecture/Flowchart/Mind-Map/AI Infographic/Structured Table/Poster Notes** |
| **Publishing Ready** | No typography styling, loses format | Requires 3rd-party plugins or scripts | **One-Click Rich Copy for WeChat, Substack, Medium & WordPress** |
| **Social Poster Cards** | Basic screenshots without styling | Requires external tools or plugins | **8 Built-in Exquisite Poster Themes with Typography Options & PNG/JPEG Export** |
| **Native AI Ecosystem** | Paywalled, limited features | Heavy plugin setup required | **Native MCP Server (\`/mcp\`) & Inline AI Agent Synergy** |
| **Cross-Platform Matrix** | Restricts active free devices | Complex setup across platforms | **Web / PWA / Android / iOS / macOS / Windows Preview** |
| **Native AI Ecosystem** | Paywalled, limited features | Heavy plugin setup required | **Native MCP Server (\`/mcp\`), Desktop ACP Local Agent Integration & Inline AI** |
| **Cross-Platform Matrix** | Restricts active free devices | Complex setup across platforms | **Web / PWA / Android / iOS / macOS / Windows / Linux AppImage** |
---
@@ -787,7 +1356,7 @@ flowchart TD
subgraph MultiClients["Client Matrix"]
C1["Web / PWA (Offline Drafts & Sync Queue)"]
C2["macOS Desktop (Electron + Rust Sidecar)"]
C3["Windows Desktop (Preview + Context Menu)"]
C3["Windows / Linux Desktop (ACP Agent Integration)"]
C4["Android Native App (Expo + SQLite)"]
C5["iOS Native App (SwiftUI + GRDB)"]
C6["Web Clipper (Chrome / Edge / Firefox)"]
@@ -972,14 +1541,19 @@ Export your entire library at any time from **Profile → Import and export**. T
> 💡 **Quick Exploration Tips**:
> 1. Click **\`</>\`** in the top right to try seamless dual-view toggling;
> 2. Explore the new **AI Infographic** and **Structured Table** examples in the sidebar;
> 2. Explore the new **AI Infographic**, **Structured Table**, and **Visual Poster** examples in the sidebar;
> 3. Press **\`Cmd/Ctrl + E\`** to test reading protection mode against accidental edits;
> 4. Press **\`Cmd/Ctrl + O\`** to summon the Quick Switcher;
> 5. Click the **WeChat Icon** to copy formatted rich text ready for publishing;
> 6. Click **"Share as Card"** to export a stunning social poster card;
> 7. Click **"Reset Demo Data"** in settings or the sidebar anytime to restore the demo workspace.
> 5. Try Notebook Web Publishing: right-click any notebook to publish it as a standalone knowledge base site;
> 6. Click the **WeChat Icon** to copy formatted rich text ready for publishing;
> 7. Click **"Share as Card"** to export a stunning social poster card;
> 8. Click **"Reset Demo Data"** in settings or the sidebar anytime to restore the demo workspace.
`,
},
memo_demo_poster: {
title: "Visual Design: EdgeEver Geek Second Brain Poster Note",
markdown: serializePosterDocument(DEMO_POSTER_DOCUMENT_EN),
},
memo_demo_infographic: {
title: "Evolution Milestones: EdgeEver Architecture & Release History",
markdown: serializeInfographicDocument(DEMO_INFOGRAPHIC_DOCUMENT_EN),
@@ -219,17 +219,19 @@ const MOBILE_THEME_OPTIONS: Array<{
labelEn: string;
previewBg: string;
}> = [
{ id: "paper", labelZh: "纸间便签", labelEn: "Paper Memo", previewBg: "#fbf9f4" },
{ id: "press", labelZh: "人文报刊", labelEn: "Press Journal", previewBg: "#f7f7f5" },
{ id: "highlight", labelZh: "社交高光", labelEn: "Highlight", previewBg: "#fefce8" },
{ id: "slate", labelZh: "纯白", labelEn: "Pure White", previewBg: "#ffffff" },
{ id: "mint", labelZh: "薄荷", labelEn: "Mint", previewBg: "#ecfdf5" },
{ id: "aurora", labelZh: "极光渐变", labelEn: "Aurora", previewBg: "#a7f3d0" },
{ id: "sunset", labelZh: "暮色晚霞", labelEn: "Sunset", previewBg: "#fde68a" },
{ id: "editorial", labelZh: "黑白编辑", labelEn: "Black & White", previewBg: "#f1f3f5" },
{ id: "terminal", labelZh: "极客终端", labelEn: "Terminal", previewBg: "#0d1117" },
{ id: "film", labelZh: "书页", labelEn: "Book Page", previewBg: "#f4ede4" },
];
const MOBILE_FONT_OPTIONS: Array<{ id: NoteImageFontStyle; labelZh: string; labelEn: string }> = [
{ id: "auto", labelZh: "跟随主题", labelEn: "Follow Theme" },
{ id: "kai", labelZh: "文楷手书", labelEn: "Handwritten Kai" },
{ id: "serif", labelZh: "文艺衬线", labelEn: "Serif" },
{ id: "sans", labelZh: "现代无衬线", labelEn: "Sans" },
{ id: "mono", labelZh: "极客等宽", labelEn: "Mono" },
@@ -519,11 +521,11 @@ export const MemoDetailModal = ({
const [imageExportStage, setImageExportStage] = useState<"prepare" | "render" | "transfer">("prepare");
const [imageShareOptionsOpen, setImageShareOptionsOpen] = useState(false);
const [imageShareFormat, setImageShareFormat] = useState<"jpeg" | "png">("png");
const [imageShareTheme, setImageShareTheme] = useState<NoteImageTheme>("mint");
const [imageShareTheme, setImageShareTheme] = useState<NoteImageTheme>("paper");
const [imageShareFontStyle, setImageShareFontStyle] = useState<NoteImageFontStyle>("auto");
const [imageShareFontSize, setImageShareFontSize] = useState<NoteImageFontSize>("lg");
const [imageShareCardWidth, setImageShareCardWidth] = useState<NoteImageCardWidth>("wide");
const [imageShareTitle, setImageShareTitle] = useState(true);
const [imageShareTitle, setImageShareTitle] = useState(false);
const [imageShareNotebook, setImageShareNotebook] = useState(false);
const [imageShareTags, setImageShareTags] = useState(false);
const [imageShareUpdatedAt, setImageShareUpdatedAt] = useState(true);
@@ -33,8 +33,9 @@ export type ShareNoteImageSource = Omit<DownloadNoteImageOptions, "background" |
const FONT_OPTIONS: Array<{ id: NoteImageFontStyle; labelKey: string }> = [
{ id: "auto", labelKey: "editor.imageShare.fontStyles.auto" },
{ id: "sans", labelKey: "editor.imageShare.fontStyles.sans" },
{ id: "kai", labelKey: "editor.imageShare.fontStyles.kai" },
{ id: "serif", labelKey: "editor.imageShare.fontStyles.serif" },
{ id: "sans", labelKey: "editor.imageShare.fontStyles.sans" },
{ id: "mono", labelKey: "editor.imageShare.fontStyles.mono" },
];
@@ -61,11 +62,11 @@ export const ShareNoteImageDialog = ({
}) => {
const { t } = useTranslation();
const [format, setFormat] = useState<NoteImageFormat>("png");
const [theme, setTheme] = useState<NoteImageTheme>("mint");
const [theme, setTheme] = useState<NoteImageTheme>("paper");
const [fontStyle, setFontStyle] = useState<NoteImageFontStyle>("auto");
const [fontSize, setFontSize] = useState<NoteImageFontSize>("lg");
const [cardWidth, setCardWidth] = useState<NoteImageCardWidth>("wide");
const [showTitle, setShowTitle] = useState(true);
const [showTitle, setShowTitle] = useState(false);
const [showNotebook, setShowNotebook] = useState(false);
const [showTags, setShowTags] = useState(false);
const [showUpdatedAt, setShowUpdatedAt] = useState(true);
@@ -89,11 +90,11 @@ export const ShareNoteImageDialog = ({
useEffect(() => {
if (!open) return;
setFormat("png");
setTheme("mint");
setTheme("paper");
setFontStyle("auto");
setFontSize("lg");
setCardWidth("wide");
setShowTitle(true);
setShowTitle(false);
setShowNotebook(false);
setShowTags(false);
setShowUpdatedAt(true);
@@ -296,12 +297,13 @@ export const ShareNoteImageDialog = ({
{t("editor.imageShare.fontStyle")}
</legend>
<div className="grid grid-cols-2 gap-1.5">
{FONT_OPTIONS.map((item) => (
{FONT_OPTIONS.map((item, idx) => (
<button
key={item.id}
aria-pressed={fontStyle === item.id}
className={cn(
"h-8 rounded-md border text-xs font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-slate-900/20",
idx === 0 && "col-span-2",
fontStyle === item.id
? "border-slate-900 bg-slate-100 text-slate-950 font-semibold"
: "border-slate-200 bg-card text-slate-600 hover:bg-slate-50",
+5 -5
View File
@@ -68,7 +68,7 @@ describe("note image export helpers", () => {
expect(midnightCss).toContain("JetBrains Mono");
});
test("generates editorial theme with brand logo", () => {
test("generates press theme when editorial is requested", () => {
const markup = buildNoteImageCardMarkup({
title: "Editorial Note",
notebook: "Work",
@@ -82,7 +82,7 @@ describe("note image export helpers", () => {
showBranding: true,
});
expect(markup).toContain('data-theme="editorial"');
expect(markup).toContain('data-theme="press"');
expect(markup).toContain("edgeever-brand-logo");
expect(markup).not.toContain("edgeever-meta-notebook");
@@ -92,11 +92,11 @@ describe("note image export helpers", () => {
fontSize: "lg",
cardWidth: "standard",
});
expect(editorialCss).toContain('data-theme="editorial"');
expect(editorialCss).toContain('data-theme="press"');
expect(editorialCss).toContain("edgeever-brand-logo");
});
test("generates film theme with brand logo", () => {
test("generates paper theme when film is requested", () => {
const markup = buildNoteImageCardMarkup({
title: "Poem Note",
bodyHtml: "<p>Kodak film aesthetic</p>",
@@ -106,7 +106,7 @@ describe("note image export helpers", () => {
showBranding: true,
});
expect(markup).toContain('data-theme="film"');
expect(markup).toContain('data-theme="paper"');
expect(markup).toContain("edgeever-brand-logo");
});
});
+1 -1
View File
@@ -138,7 +138,7 @@ export const createNoteImage = async ({
fontStyle = "auto",
fontSize = "lg",
cardWidth = "wide",
showTitle = true,
showTitle = false,
showNotebook = false,
showTags = false,
showUpdatedAt = true,
+5 -1
View File
@@ -2432,6 +2432,9 @@ export const enUS = {
previewBody: "Ideas worth sharing",
previewQuote: "Keep what matters.",
themes: {
paper: "Paper Memo",
press: "Press Journal",
highlight: "Social Highlight",
slate: "Pure White",
aurora: "Aurora",
sunset: "Sunset",
@@ -2450,8 +2453,9 @@ export const enUS = {
fontStyle: "Typography",
fontStyles: {
auto: "Follow Theme",
sans: "Modern Sans",
kai: "Handwritten Kai",
serif: "Editorial Serif",
sans: "Modern Sans",
mono: "Geek Mono",
},
fontSize: "Font Size",
+5 -1
View File
@@ -2421,6 +2421,9 @@ export const ja = {
previewBody: "共有したいアイデア",
previewQuote: "大切なことを残す。",
themes: {
paper: "メモ用紙",
press: "新聞・雑誌",
highlight: "ハイライト",
slate: "ピュアホワイト",
aurora: "オーロラ",
sunset: "夕焼け",
@@ -2439,8 +2442,9 @@ export const ja = {
fontStyle: "書体",
fontStyles: {
auto: "テーマに合わせる",
sans: "モダンゴシック",
kai: "手書き・楷書",
serif: "本文明朝",
sans: "モダンゴシック",
mono: "ギーク等幅",
},
fontSize: "文字サイズ",
+5 -1
View File
@@ -2421,6 +2421,9 @@ export const pl = {
previewBody: "Pomysły warte udostępnienia",
previewQuote: "Zachowaj to, co ważne.",
themes: {
paper: "Notatka papierowa",
press: "Prasa i felieton",
highlight: "Wyróżnienie",
slate: "Czysta biel",
aurora: "Zorza",
sunset: "Zachód słońca",
@@ -2439,8 +2442,9 @@ export const pl = {
fontStyle: "Typografia",
fontStyles: {
auto: "Zgodnie z motywem",
sans: "Nowoczesna bezszeryfowa",
kai: "Pismo odręczne / Kaligrafia",
serif: "Redakcyjna szeryfowa",
sans: "Nowoczesna bezszeryfowa",
mono: "Geekowska o stałej szerokości",
},
fontSize: "Rozmiar czcionki",
+5 -1
View File
@@ -2430,6 +2430,9 @@ export const zhCN = {
previewBody: "值得分享的想法",
previewQuote: "留住重要的事。",
themes: {
paper: "纸间便签",
press: "人文报刊",
highlight: "社交高光",
slate: "纯白",
aurora: "极光渐变",
sunset: "暮色晚霞",
@@ -2448,8 +2451,9 @@ export const zhCN = {
fontStyle: "字体风格",
fontStyles: {
auto: "跟随主题",
sans: "现代无衬线",
kai: "文楷手书",
serif: "文艺衬线",
sans: "现代无衬线",
mono: "极客等宽",
},
fontSize: "字号大小",
+100 -20
View File
@@ -14,19 +14,20 @@ import {
describe("note-image-card shared module", () => {
test("resolves theme correctly with fallbacks", () => {
expect(resolveTheme(undefined, "editorial")).toBe("editorial");
expect(resolveTheme(undefined, "editorial")).toBe("press");
expect(resolveTheme(undefined, "terminal")).toBe("terminal");
expect(resolveTheme(undefined, "film")).toBe("film");
expect(resolveTheme(undefined, "notepad")).toBe("editorial");
expect(resolveTheme(undefined, "xuan")).toBe("editorial");
expect(resolveTheme(undefined, "polaroid")).toBe("film");
expect(resolveTheme(undefined, "film")).toBe("paper");
expect(resolveTheme(undefined, "notepad")).toBe("press");
expect(resolveTheme(undefined, "xuan")).toBe("press");
expect(resolveTheme(undefined, "polaroid")).toBe("paper");
expect(resolveTheme("warm", undefined)).toBe("sunset");
expect(resolveTheme(undefined, undefined)).toBe("mint");
expect(resolveTheme(undefined, undefined)).toBe("paper");
});
test("retired themes resolve to active choices in both legacy inputs", () => {
expect(NOTE_IMAGE_THEME_OPTIONS).toHaveLength(7);
for (const [legacy, active] of [["midnight", "terminal"], ["lavender", "mint"]]) {
expect(NOTE_IMAGE_THEME_OPTIONS).toHaveLength(8);
expect(NOTE_IMAGE_THEME_OPTIONS).toEqual(["paper", "press", "highlight", "slate", "mint", "aurora", "sunset", "terminal"]);
for (const [legacy, active] of [["midnight", "terminal"], ["lavender", "mint"], ["editorial", "press"], ["film", "paper"]]) {
expect(NOTE_IMAGE_THEME_OPTIONS).not.toContain(legacy);
expect(resolveTheme(undefined, legacy)).toBe(active);
expect(resolveTheme(legacy)).toBe(active);
@@ -40,9 +41,25 @@ describe("note-image-card shared module", () => {
test("theme typography pairs headings and body while explicit fonts override both", () => {
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
const auto = resolveNoteImageTypography(theme);
expect(auto.bodyFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[theme === "film" || theme === "sunset" ? "serif" : "sans"]);
expect(auto.headingFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[theme === "editorial" ? "serif" : theme === "terminal" ? "mono" : theme === "film" || theme === "sunset" ? "serif" : "sans"]);
for (const fontStyle of ["sans", "serif", "mono"]) {
const expectedBody = theme === "paper"
? "kai"
: theme === "terminal"
? "mono"
: theme === "film" || theme === "sunset" || theme === "press"
? "serif"
: "sans";
const expectedHeading = theme === "paper"
? "kai"
: theme === "terminal"
? "mono"
: theme === "editorial" || theme === "film" || theme === "sunset" || theme === "press"
? "serif"
: "sans";
expect(auto.bodyFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[expectedBody]);
expect(auto.headingFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[expectedHeading]);
for (const fontStyle of ["sans", "serif", "mono", "kai"]) {
const manual = resolveNoteImageTypography(theme, fontStyle);
expect(manual.bodyFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[fontStyle]);
expect(manual.headingFamily).toBe(NOTE_IMAGE_FONT_FAMILIES[fontStyle]);
@@ -58,8 +75,18 @@ describe("note-image-card shared module", () => {
expect(markup).not.toContain("edgeever-terminal-header");
});
test("defaults to hiding note title unless explicitly enabled", () => {
const defaultMarkup = buildNoteImageCardMarkup({ title: "Hidden Title", bodyHtml: "<p>Content</p>" });
expect(defaultMarkup).not.toContain("edgeever-card-title");
expect(defaultMarkup).not.toContain("Hidden Title");
const visibleMarkup = buildNoteImageCardMarkup({ title: "Visible Title", bodyHtml: "<p>Content</p>", showTitle: true });
expect(visibleMarkup).toContain("edgeever-card-title");
expect(visibleMarkup).toContain("Visible Title");
});
test("retains valid colors for current themes and legacy slate exports", () => {
const expectedThemes = ["slate", "aurora", "sunset", "midnight", "editorial", "terminal", "film", "mint", "lavender"];
const expectedThemes = ["paper", "press", "highlight", "slate", "aurora", "sunset", "midnight", "editorial", "terminal", "film", "mint", "lavender"];
for (const theme of expectedThemes) {
expect(NOTE_IMAGE_THEMES[theme]).toBeDefined();
expect(NOTE_IMAGE_BACKGROUND_COLORS[theme]).toBeDefined();
@@ -92,7 +119,7 @@ describe("note-image-card shared module", () => {
expect(markup).not.toContain("edgeever-meta-tag");
});
test("generates editorial theme card with distinct borders and styling", () => {
test("generates press styling when legacy editorial theme is requested", () => {
const markup = buildNoteImageCardMarkup({
title: "Editorial Dispatch",
bodyHtml: "<p>Deep analytical reporting on modern engineering.</p>",
@@ -102,7 +129,7 @@ describe("note-image-card shared module", () => {
author: "@tianma",
showAuthor: true,
});
expect(markup).toContain('data-theme="editorial"');
expect(markup).toContain('data-theme="press"');
expect(markup).toContain("@tianma");
const css = generateCardCss({
@@ -111,8 +138,8 @@ describe("note-image-card shared module", () => {
fontSize: "md",
cardWidth: "standard",
});
expect(css).toContain('data-theme="editorial"');
expect(css).toContain("box-shadow: 6px 6px 0px #111827");
expect(css).toContain('data-theme="press"');
expect(css).toContain("border-top: 3.5px solid #111412");
});
test("generates terminal header for the terminal theme", () => {
@@ -135,7 +162,7 @@ describe("note-image-card shared module", () => {
expect(css).toContain('data-theme="terminal"');
});
test("generates film theme card with custom author and reading time", () => {
test("generates paper styling when legacy film theme is requested", () => {
const markup = buildNoteImageCardMarkup({
title: "Memories in Summer",
notebook: "Journal",
@@ -150,7 +177,7 @@ describe("note-image-card shared module", () => {
});
expect(markup).toContain("edgeever-grain-overlay");
expect(markup).toContain('data-theme="film"');
expect(markup).toContain('data-theme="paper"');
expect(markup).toContain("@tianma");
expect(markup).toContain("edgeever-footer-author");
expect(markup).toContain("edgeever-meta-reading");
@@ -161,8 +188,8 @@ describe("note-image-card shared module", () => {
fontSize: "md",
cardWidth: "standard",
});
expect(css).toContain('data-theme="film"');
expect(css).toContain("edgeever-grain-overlay");
expect(css).toContain('data-theme="paper"');
expect(css).toContain("background-color: #fbf9f4");
});
test("calculates reading stats for mixed Chinese and English text", () => {
@@ -171,6 +198,59 @@ describe("note-image-card shared module", () => {
expect(stats.minutes).toBeGreaterThanOrEqual(1);
});
test("generates paper theme with warm background and borderless structure", () => {
const markup = buildNoteImageCardMarkup({
title: "Daily Reflections",
bodyHtml: "<p>Thoughts on a quiet morning.</p>",
theme: "paper",
});
expect(markup).toContain('data-theme="paper"');
const css = generateCardCss({
theme: "paper",
fontStyle: "sans",
fontSize: "md",
cardWidth: "standard",
});
expect(css).toContain('.edgeever-image-wrapper[data-theme="paper"]');
expect(css).toContain("background-color: #fbf9f4");
expect(css).toContain("radial-gradient");
});
test("generates press theme with classic double rule and editorial styling", () => {
const markup = buildNoteImageCardMarkup({
title: "The Architecture of Thought",
bodyHtml: "<blockquote>A good system makes right paths easy.</blockquote>",
theme: "press",
fontStyle: "serif",
});
expect(markup).toContain('data-theme="press"');
const css = generateCardCss({
theme: "press",
fontStyle: "serif",
fontSize: "md",
cardWidth: "standard",
});
expect(css).toContain('.edgeever-image-card[data-theme="press"]');
expect(css).toContain("border-top: 3.5px solid #111412");
});
test("generates highlight theme with vivid marker styles on strong text", () => {
const markup = buildNoteImageCardMarkup({
title: "Key Takeaways",
bodyHtml: "<p>Remember to <strong>focus on what matters</strong>.</p>",
theme: "highlight",
});
expect(markup).toContain('data-theme="highlight"');
const css = generateCardCss({
theme: "highlight",
fontStyle: "sans",
fontSize: "md",
cardWidth: "standard",
});
expect(css).toContain('.edgeever-image-card[data-theme="highlight"]');
expect(css).toContain("background: linear-gradient(104deg, rgba(254, 240, 138");
});
test("sanitizes filenames and guards against reserved Windows names", () => {
expect(buildImageExportBasename("CON", "fallback")).toBe("_CON");
expect(buildImageExportBasename("My Note/Title?*", "fallback")).toBe("My Note-Title--");
+252 -21
View File
@@ -1,8 +1,8 @@
export type NoteImageFormat = "jpeg" | "png";
export type NoteImageTheme = "slate" | "aurora" | "sunset" | "midnight" | "editorial" | "terminal" | "film" | "mint" | "lavender";
export type NoteImageTheme = "paper" | "press" | "highlight" | "slate" | "aurora" | "sunset" | "midnight" | "editorial" | "terminal" | "film" | "mint" | "lavender";
export type NoteImageBackground = NoteImageTheme | "warm" | "notepad" | "xuan" | "polaroid";
export type NoteImageFontStyle = "auto" | "sans" | "serif" | "mono";
export const NOTE_IMAGE_THEME_OPTIONS = ["slate", "mint", "aurora", "sunset", "editorial", "film", "terminal"] as const;
export type NoteImageFontStyle = "auto" | "sans" | "serif" | "mono" | "kai";
export const NOTE_IMAGE_THEME_OPTIONS = ["paper", "press", "highlight", "slate", "mint", "aurora", "sunset", "terminal"] as const;
export type NoteImageFontSize = "sm" | "md" | "lg";
export type NoteImageCardWidth = "compact" | "standard" | "wide";
@@ -38,6 +38,9 @@ export const planNativeNoteImageRender = (width: number, height: number) => {
const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
export const NOTE_IMAGE_BACKGROUND_COLORS: Record<NoteImageBackground, string> = {
paper: "#fbf9f4",
press: "#f7f7f5",
highlight: "#fefce8",
slate: "#ffffff",
aurora: "#a7f3d0",
sunset: "#fde68a",
@@ -80,6 +83,78 @@ export type ThemeStyleConfig = {
};
export const NOTE_IMAGE_THEMES: Record<NoteImageTheme, ThemeStyleConfig> = {
paper: {
canvasBg: "#fbf9f4",
cardBg: "#fbf9f4",
cardBorder: "none",
cardShadow: "none",
textColor: "#2c2925",
headingColor: "#1a1816",
metaColor: "#8c8275",
metaBg: "rgba(140, 130, 117, 0.12)",
metaBorder: "rgba(140, 130, 117, 0.22)",
accentColor: "#b45309",
accentSubtle: "rgba(180, 83, 9, 0.15)",
linkBg: "rgba(180, 83, 9, 0.08)",
dividerColor: "rgba(140, 130, 117, 0.3)",
codeBg: "rgba(140, 130, 117, 0.12)",
codeBorder: "rgba(140, 130, 117, 0.25)",
codeColor: "#2c2925",
quoteBg: "rgba(255, 255, 255, 0.65)",
quoteBorder: "#a89f91",
tableThBg: "rgba(140, 130, 117, 0.12)",
tableBorder: "rgba(140, 130, 117, 0.25)",
brandText: "#2c2925",
brandMark: "#8c8275",
},
press: {
canvasBg: "linear-gradient(180deg, #f7f7f5 0%, #ededeb 100%)",
cardBg: "#ffffff",
cardBorder: "1px solid #e2e2e0",
cardShadow: "0 12px 32px -4px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(0, 0, 0, 0.02)",
textColor: "#1f2421",
headingColor: "#111412",
metaColor: "#64748b",
metaBg: "#f8fafc",
metaBorder: "#e2e8f0",
accentColor: "#b91c1c",
accentSubtle: "rgba(185, 28, 28, 0.12)",
linkBg: "rgba(185, 28, 28, 0.06)",
dividerColor: "#111412",
codeBg: "#f8fafc",
codeBorder: "#e2e8f0",
codeColor: "#0f172a",
quoteBg: "#fcfcfb",
quoteBorder: "#111412",
tableThBg: "#f1f5f9",
tableBorder: "#e2e8f0",
brandText: "#111412",
brandMark: "#b91c1c",
},
highlight: {
canvasBg: "linear-gradient(135deg, #fef9c3 0%, #fef08a 35%, #e0e7ff 100%)",
cardBg: "#ffffff",
cardBorder: "1px solid rgba(254, 240, 138, 0.6)",
cardShadow: "0 20px 48px -12px rgba(234, 179, 8, 0.18), 0 4px 16px rgba(0, 0, 0, 0.04)",
textColor: "#1e293b",
headingColor: "#0f172a",
metaColor: "#475569",
metaBg: "rgba(254, 240, 138, 0.45)",
metaBorder: "rgba(250, 204, 21, 0.5)",
accentColor: "#ca8a04",
accentSubtle: "rgba(202, 138, 4, 0.16)",
linkBg: "rgba(202, 138, 4, 0.08)",
dividerColor: "rgba(254, 215, 170, 0.7)",
codeBg: "#fefce8",
codeBorder: "#fef08a",
codeColor: "#854d0e",
quoteBg: "#fefce8",
quoteBorder: "#eab308",
tableThBg: "#fef9c3",
tableBorder: "#fef08a",
brandText: "#0f172a",
brandMark: "#ca8a04",
},
slate: {
canvasBg: "#ffffff",
cardBg: "#ffffff",
@@ -301,25 +376,61 @@ export const NOTE_IMAGE_THEMES: Record<NoteImageTheme, ThemeStyleConfig> = {
};
export const NOTE_IMAGE_FONT_FAMILIES: Record<Exclude<NoteImageFontStyle, "auto">, string> = {
sans: 'system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
serif: '"Songti SC", "Noto Serif SC", "Source Han Serif SC", "STSong", "Hiragino Mincho ProN", Georgia, "Times New Roman", serif',
mono: '"JetBrains Mono", "SF Mono", "Fira Code", Menlo, Monaco, Consolas, "Liberation Mono", monospace',
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif',
serif: 'Georgia, "Nimbus Roman No9 L", "Songti SC", "Noto Serif SC", "Source Han Serif SC", "Source Han Serif CN", "STSong", "AR PL New Sung", "SimSun", "Hiragino Mincho ProN", serif',
mono: '"JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", Menlo, Monaco, Consolas, "Liberation Mono", monospace',
kai: '"LXGW WenKai", "霞鹜文楷", "Kaiti SC", "STKaiti", "KaiTi", "KaiTi_GB2312", "BiauKai", "楷体", "STFangsong", "FangSong", serif',
};
/** Theme typography is shared by previews, Web exports and native WebViews. */
export const resolveNoteImageTypography = (theme: NoteImageTheme, fontStyle: NoteImageFontStyle = "auto") => {
const resolvedTheme = resolveTheme(undefined, theme);
const bodyStyle = fontStyle === "auto"
? (resolvedTheme === "film" || resolvedTheme === "sunset" ? "serif" : "sans")
: fontStyle;
const headingStyle = fontStyle === "auto"
? (resolvedTheme === "editorial" ? "serif" : resolvedTheme === "terminal" ? "mono" : bodyStyle)
: bodyStyle;
let bodyStyle: Exclude<NoteImageFontStyle, "auto">;
let headingStyle: Exclude<NoteImageFontStyle, "auto">;
if (fontStyle !== "auto") {
bodyStyle = fontStyle;
headingStyle = fontStyle;
} else {
switch (resolvedTheme) {
case "paper":
bodyStyle = "kai";
headingStyle = "kai";
break;
case "press":
bodyStyle = "serif";
headingStyle = "serif";
break;
case "terminal":
bodyStyle = "mono";
headingStyle = "mono";
break;
case "film":
case "sunset":
bodyStyle = "serif";
headingStyle = "serif";
break;
case "editorial":
bodyStyle = "sans";
headingStyle = "serif";
break;
case "highlight":
case "slate":
case "mint":
case "aurora":
default:
bodyStyle = "sans";
headingStyle = "sans";
break;
}
}
return {
bodyFamily: NOTE_IMAGE_FONT_FAMILIES[bodyStyle],
headingFamily: NOTE_IMAGE_FONT_FAMILIES[headingStyle],
headingWeight: headingStyle === "serif" ? 700 : 720,
lineHeightAdjustment: bodyStyle === "serif" ? 0.08 : bodyStyle === "mono" ? 0.02 : 0,
headingWeight: headingStyle === "serif" ? 720 : headingStyle === "kai" ? 650 : 700,
lineHeightAdjustment: bodyStyle === "serif" ? 0.08 : bodyStyle === "kai" ? 0.06 : bodyStyle === "mono" ? 0.02 : 0,
};
};
@@ -383,14 +494,18 @@ export const NOTE_IMAGE_FONT_SIZES: Record<
export const resolveTheme = (background?: NoteImageBackground, theme?: NoteImageTheme): NoteImageTheme => {
if (theme === "midnight") return "terminal";
if (theme === "lavender") return "mint";
if (theme === "editorial") return "press";
if (theme === "film") return "paper";
if (theme === ("notepad" as unknown as NoteImageTheme) || theme === ("xuan" as unknown as NoteImageTheme)) return "press";
if (theme === ("polaroid" as unknown as NoteImageTheme)) return "paper";
if (theme && NOTE_IMAGE_THEMES[theme]) return theme;
if (theme === ("notepad" as unknown as NoteImageTheme) || theme === ("xuan" as unknown as NoteImageTheme)) return "editorial";
if (theme === ("polaroid" as unknown as NoteImageTheme)) return "film";
if (background === "warm") return "sunset";
if (background === "notepad" || background === "xuan") return "editorial";
if (background === "polaroid") return "film";
if (background === "notepad" || background === "xuan") return "press";
if (background === "polaroid") return "paper";
if (background === "editorial") return "press";
if (background === "film") return "paper";
if (background && NOTE_IMAGE_THEMES[background as NoteImageTheme]) return resolveTheme(undefined, background as NoteImageTheme);
return "mint";
return "paper";
};
export const buildImageExportBasename = (title: string, fallback: string) => {
@@ -430,7 +545,7 @@ export const buildNoteImageCardMarkup = ({
bodyHtml,
theme = "mint",
fontStyle = "auto",
showTitle = true,
showTitle = false,
showNotebook = false,
showTags = false,
showUpdatedAt = true,
@@ -531,7 +646,7 @@ export const buildNoteImageCardMarkup = ({
</footer>`
: "";
return `<div class="edgeever-image-wrapper">
return `<div class="edgeever-image-wrapper" data-theme="${theme}">
<div class="edgeever-grain-overlay" aria-hidden="true"></div>
<article class="edgeever-image-card" data-theme="${theme}">
${tearStripHtml}
@@ -1029,6 +1144,122 @@ export const generateCardCss = ({
color: var(--ee-brand-text);
}
${theme === "paper" ? `
.edgeever-image-wrapper[data-theme="paper"] {
background-color: #fbf9f4;
background-image: radial-gradient(#d5cdc0 0.85px, transparent 0.85px);
background-size: 20px 20px;
padding: 32px 28px;
}
.edgeever-image-wrapper[data-theme="paper"] .edgeever-grain-overlay {
opacity: 0.03;
}
.edgeever-image-card[data-theme="paper"] {
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
padding: 16px 8px 12px;
}
.edgeever-image-card[data-theme="paper"] .edgeever-card-title {
font-size: calc(var(--ee-title-size) * 0.94);
font-weight: 700;
letter-spacing: -0.015em;
}
.edgeever-image-card[data-theme="paper"] .edgeever-card-divider {
height: 1px;
background: none;
border-top: 1px dashed rgba(140, 130, 117, 0.4);
margin: 0 0 20px;
}
.edgeever-image-card[data-theme="paper"] blockquote {
border-left: 3px solid #b4a796;
background: rgba(255, 255, 255, 0.65);
border-radius: 6px;
padding: 12px 16px;
}
.edgeever-image-card[data-theme="paper"] blockquote::before {
display: none;
}
` : ""}
${theme === "press" ? `
.edgeever-image-card[data-theme="press"] {
border: 1px solid #d4d4d8;
border-radius: 2px;
box-shadow: 0 10px 30px -4px rgba(0, 0, 0, 0.05);
}
.edgeever-image-card[data-theme="press"] .edgeever-card-header {
border-top: 3.5px solid #111412;
padding-top: 14px;
position: relative;
}
.edgeever-image-card[data-theme="press"] .edgeever-card-header::before {
content: "";
position: absolute;
top: 4.5px;
left: 0;
right: 0;
height: 1px;
background: #111412;
}
.edgeever-image-card[data-theme="press"] .edgeever-card-title {
font-family: var(--ee-heading-font-family);
letter-spacing: -0.01em;
margin-top: 10px;
}
.edgeever-image-card[data-theme="press"] .edgeever-card-divider {
height: 1px;
background: #111412;
margin: 16px 0 24px;
}
.edgeever-image-card[data-theme="press"] blockquote {
border-left: 2px solid #111412;
background: #fafaf9;
border-radius: 0;
padding: 16px 20px 16px 30px;
font-style: normal;
}
.edgeever-image-card[data-theme="press"] blockquote::before {
content: "“";
font-family: Georgia, "Songti SC", serif;
font-size: 46px;
top: -14px;
left: 6px;
color: #b91c1c;
opacity: 0.45;
}
.edgeever-image-card[data-theme="press"] .edgeever-card-body h1 {
border-left: 3px solid #b91c1c;
}
` : ""}
${theme === "highlight" ? `
.edgeever-image-card[data-theme="highlight"] {
border-radius: 22px;
border: 1px solid rgba(253, 224, 71, 0.7);
box-shadow: 0 20px 50px -12px rgba(202, 138, 4, 0.16), 0 2px 8px rgba(0, 0, 0, 0.03);
}
.edgeever-image-card[data-theme="highlight"] strong,
.edgeever-image-card[data-theme="highlight"] b {
background: linear-gradient(104deg, rgba(254, 240, 138, 0.25) 1%, rgba(254, 240, 138, 0.8) 4%, rgba(254, 240, 138, 0.7) 96%);
padding: 0.05em 0.35em;
border-radius: 4px;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.edgeever-image-card[data-theme="highlight"] mark {
background: linear-gradient(104deg, rgba(250, 204, 21, 0.35) 1%, rgba(250, 204, 21, 0.9) 3%, rgba(250, 204, 21, 0.8) 98%);
font-weight: 600;
}
.edgeever-image-card[data-theme="highlight"] .edgeever-card-title {
font-weight: 800;
}
.edgeever-image-card[data-theme="highlight"] .edgeever-card-body h1 {
border-left: 4px solid #eab308;
}
` : ""}
${theme === "editorial" ? `
.edgeever-image-card[data-theme="editorial"] {
border: 2px solid #111827;