Unify editor text spacing across themes

This commit is contained in:
tianma-if committed 2026-08-04 12:30:40 +08:00
1 parent ddefcffb41
commit 8ffd4062fa
14 files changed
+83 -81

No files matched your search

@@ -9,7 +9,7 @@ import { TableKit } from "@tiptap/extension-table";
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import * as Clipboard from "expo-clipboard";
import { getImageReferrerPolicy, getResourceIdFromUrl, type TiptapDoc } from "@edgeever/shared";
import { MEMO_CONTENT_STYLE, getImageReferrerPolicy, getResourceIdFromUrl, type TiptapDoc } from "@edgeever/shared";
import {
DEFAULT_IMAGE_WIDTH_PERCENT,
IMAGE_WIDTH_PRESETS,
@@ -1374,8 +1374,9 @@ const getEditorStyles = (theme: "light" | "dark") => `
.edgeever-editor-toolbar button:disabled { opacity: 0.38; }
.tiptap { min-height: 100%; outline: none; }
.edgeever-editor-shell > div:last-child { min-height: 0; flex: 1; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.edgeever-editor-content { min-height: 100%; padding: 18px 12px 32px; font-size: 17px; line-height: 1.7; word-break: break-word; caret-color: #0f766e; }
.edgeever-editor-content { min-height: 100%; padding: 18px 12px 32px; font-size: 17px; line-height: ${MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize}; word-break: break-word; caret-color: #0f766e; }
.edgeever-editor-content > :first-child { margin-top: 0; }
.edgeever-editor-content p { margin: 0 0 ${MEMO_CONTENT_STYLE.body.paragraphSpacing}px; padding: 0; line-height: ${MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize}; }
.edgeever-editor-content p.is-editor-empty:first-child::before { float: left; height: 0; color: #94a3b8; content: attr(data-placeholder); pointer-events: none; }
.edgeever-editor-content h1, .edgeever-editor-content h2, .edgeever-editor-content h3 { line-height: 1.3; }
.edgeever-editor-content blockquote { margin-left: 0; padding-left: 14px; border-left: 3px solid #5eead4; color: ${theme === "dark" ? "#cbd5e1" : "#475569"}; }
@@ -705,7 +705,7 @@ const detailMarkdownStyles = StyleSheet.create({
marginVertical: 8,
},
paragraph: {
marginBottom: 10,
marginBottom: MEMO_CONTENT_STYLE.body.paragraphSpacing,
marginTop: 0,
},
strong: {
+1
View File
@@ -3393,6 +3393,7 @@ const RichEditorPane = ({
style={{
"--editor-body-font-size": `${MEMO_CONTENT_STYLE.body.fontSize}px`,
"--editor-body-line-height": String(MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize),
"--editor-paragraph-spacing": `${MEMO_CONTENT_STYLE.body.paragraphSpacing}px`,
"--memo-content-divider-color": MEMO_CONTENT_STYLE.divider.color[resolvedTheme],
"--memo-content-divider-spacing": `${MEMO_CONTENT_STYLE.divider.marginVertical}px`,
...(editorTheme !== "default" &&
+23 -8
View File
@@ -1,16 +1,21 @@
import type { Editor } from "@tiptap/react";
import { MEMO_CONTENT_STYLE } from "@edgeever/shared";
import { marked } from "marked";
import { parseCustomCssToStyles } from "@/lib/css-sandbox";
const BODY_LINE_HEIGHT = MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize;
const BODY_FONT_SIZE = `${MEMO_CONTENT_STYLE.body.fontSize}px`;
const PARAGRAPH_SPACING = `${MEMO_CONTENT_STYLE.body.paragraphSpacing}px`;
const WECHAT_STYLES: Record<string, string> = {
p: "margin: 0 0 1em; line-height: 1.75; font-size: 16px; color: #333;",
p: `margin: 0 0 ${PARAGRAPH_SPACING}; padding: 0; line-height: ${BODY_LINE_HEIGHT}; font-size: ${BODY_FONT_SIZE}; color: #333;`,
h1: "margin: 1.2em 0 0.6em; font-size: 24px; line-height: 1.35; font-weight: 700; color: #1f2937;",
h2: "margin: 1.1em 0 0.55em; font-size: 21px; line-height: 1.4; font-weight: 700; color: #1f2937;",
h3: "margin: 1em 0 0.5em; font-size: 18px; line-height: 1.45; font-weight: 700; color: #1f2937;",
blockquote: "margin: 1em 0; padding: 0.6em 1em; border-left: 4px solid #10b981; background: #f0fdf4; color: #4b5563; line-height: 1.75;",
ul: "margin: 0 0 1em; padding-left: 1.6em; line-height: 1.75;",
ol: "margin: 0 0 1em; padding-left: 1.6em; line-height: 1.75;",
li: "margin: 0.25em 0; line-height: 1.75;",
blockquote: `margin: 1em 0; padding: 0.6em 1em; border-left: 4px solid #10b981; background: #f0fdf4; color: #4b5563; line-height: ${BODY_LINE_HEIGHT};`,
ul: `margin: 0 0 1em; padding-left: 1.6em; line-height: ${BODY_LINE_HEIGHT};`,
ol: `margin: 0 0 1em; padding-left: 1.6em; line-height: ${BODY_LINE_HEIGHT};`,
li: `margin: 0.25em 0; line-height: ${BODY_LINE_HEIGHT};`,
a: "color: #059669; text-decoration: underline;",
strong: "font-weight: 700;",
em: "font-style: italic;",
@@ -64,7 +69,7 @@ const applyInlineStyles = (
const customStyles = customCss ? parseCustomCssToStyles(customCss) : null;
root.style.cssText = `font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 16px; line-height: 1.75; color: ${textColor}; background-color: ${bgColors}; word-break: break-word;`;
root.style.cssText = `font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: ${BODY_FONT_SIZE}; line-height: ${BODY_LINE_HEIGHT}; color: ${textColor}; background-color: ${bgColors}; word-break: break-word;`;
root.querySelectorAll<HTMLElement>("*").forEach((element) => {
const tagName = element.tagName.toLowerCase();
@@ -72,11 +77,11 @@ const applyInlineStyles = (
if (customColors) {
if (tagName === "p") {
style = `margin: 0 0 1em; line-height: 1.75; font-size: 16px; color: ${textColor};`;
style = `margin: 0 0 ${PARAGRAPH_SPACING}; padding: 0; line-height: ${BODY_LINE_HEIGHT}; font-size: ${BODY_FONT_SIZE}; color: ${textColor};`;
} else if (tagName === "h1" || tagName === "h2" || tagName === "h3") {
style = style.replace("color: #1f2937;", `color: ${textColor};`);
} else if (tagName === "blockquote") {
style = `margin: 1em 0; padding: 0.6em 1em; border-left: 4px solid ${accent}; background: ${soft}; color: ${textColor}; line-height: 1.75;`;
style = `margin: 1em 0; padding: 0.6em 1em; border-left: 4px solid ${accent}; background: ${soft}; color: ${textColor}; line-height: ${BODY_LINE_HEIGHT};`;
} else if (tagName === "a") {
style = `color: ${accent}; text-decoration: underline;`;
} else if (tagName === "code") {
@@ -99,6 +104,16 @@ const applyInlineStyles = (
if (style) element.style.cssText = `${style}${element.style.cssText}`;
});
root.querySelectorAll<HTMLElement>("p").forEach((paragraph) => {
paragraph.style.margin = `0 0 ${PARAGRAPH_SPACING}`;
paragraph.style.padding = "0";
paragraph.style.lineHeight = String(BODY_LINE_HEIGHT);
});
root.querySelectorAll<HTMLElement>("ul, ol, li, blockquote").forEach((bodyBlock) => {
bodyBlock.style.lineHeight = String(BODY_LINE_HEIGHT);
});
root.querySelectorAll<HTMLElement>("pre code").forEach((element) => {
element.style.cssText = "padding: 0; background: transparent; color: inherit; font-family: Menlo, Consolas, monospace; font-size: 13px; white-space: pre-wrap;";
});
+5 -2
View File
@@ -20,6 +20,7 @@
background: var(--editor-theme-bg);
color: var(--editor-theme-text);
font-family: var(--editor-theme-font, inherit);
line-height: var(--editor-body-line-height);
}
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h1,
@@ -82,7 +83,9 @@
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror p {
line-height: 1.8;
margin: 0 0 var(--editor-paragraph-spacing);
padding: 0;
line-height: var(--editor-body-line-height);
}
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror strong {
@@ -107,7 +110,7 @@
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror ul,
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror ol {
line-height: 1.75;
line-height: var(--editor-body-line-height);
}
.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror li::marker {
@@ -27,15 +27,10 @@
}
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror {
font-size: 1rem;
line-height: 1.75;
letter-spacing: -0.003em;
}
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror p {
margin: 0;
padding: 0.4rem 0;
line-height: 1.75;
color: var(--editor-theme-text);
}
@@ -94,17 +89,6 @@
padding: 0.6rem 1rem;
}
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror ul,
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror ol {
margin: 0.5rem 0;
padding-left: 1.4rem;
}
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror li {
margin: 0.2rem 0;
line-height: 1.7;
}
.edgeever-editor[data-editor-theme="minimal-emerald"]:not([data-editor-theme="default"]) .ProseMirror li::marker {
color: var(--editor-theme-accent);
font-weight: 600;
@@ -27,15 +27,10 @@
}
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror {
font-size: 1rem;
line-height: 1.75;
letter-spacing: -0.003em;
}
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror p {
margin: 0;
padding: 0.45rem 0;
line-height: 1.75;
color: var(--editor-theme-text);
}
@@ -99,17 +94,6 @@
padding: 1rem 1.25rem;
}
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror ul,
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror ol {
margin: 1.0rem 0;
padding-left: 1.75rem;
}
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror li {
margin: 0.5rem 0;
line-height: 1.75;
}
.edgeever-editor[data-editor-theme="modern-mint"]:not([data-editor-theme="default"]) .ProseMirror li::marker {
color: var(--editor-theme-accent);
font-weight: 600;
@@ -27,15 +27,10 @@
}
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror {
font-size: 1rem;
line-height: 1.7;
letter-spacing: -0.005em;
}
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror p {
margin: 0;
padding: 0.45rem 0;
line-height: 1.7;
color: var(--editor-theme-text);
}
@@ -96,17 +91,6 @@
padding: 0.6rem 1rem;
}
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror ul,
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror ol {
margin: 0.5rem 0;
padding-left: 1.4rem;
}
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror li {
margin: 0.2rem 0;
line-height: 1.65;
}
.edgeever-editor[data-editor-theme="outline-emerald"]:not([data-editor-theme="default"]) .ProseMirror li::marker {
color: var(--editor-theme-accent);
font-weight: 600;
@@ -27,15 +27,10 @@
}
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror {
font-size: 1rem;
line-height: 1.75;
letter-spacing: -0.003em;
}
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror p {
margin: 0;
padding: 0.45rem 0;
line-height: 1.75;
color: var(--editor-theme-text);
}
@@ -107,17 +102,6 @@
padding: 1rem 1.25rem;
}
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror ul,
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror ol {
margin: 1rem 0;
padding-left: 1.75rem;
}
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror li {
margin: 0.5rem 0;
line-height: 1.75;
}
.edgeever-editor[data-editor-theme="wechat-green"]:not([data-editor-theme="default"]) .ProseMirror li::marker {
color: var(--editor-theme-accent);
font-weight: 600;
@@ -0,0 +1,42 @@
import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { MEMO_CONTENT_STYLE } from "@edgeever/shared";
const PRESET_THEME_FILES = [
"minimal-emerald.css",
"outline-emerald.css",
"wechat-green.css",
"modern-mint.css",
];
const readStyle = (relativePath) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
const declarationsForSelector = (source, selectorSuffix) =>
[...source.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
.filter(([, selectors]) => selectors.split(",").some((selector) => selector.trim().endsWith(selectorSuffix)))
.map(([, , declarations]) => declarations)
.join("\n");
describe("editor typography contract", () => {
test("keeps the shared body rhythm compact", () => {
expect(MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize).toBe(1.6);
expect(MEMO_CONTENT_STYLE.body.paragraphSpacing).toBe(6);
});
test("does not let preset themes override body rhythm", () => {
for (const filename of PRESET_THEME_FILES) {
const source = readStyle(`./editor-themes/${filename}`);
const editorRules = declarationsForSelector(source, ".ProseMirror");
const paragraphRules = declarationsForSelector(source, ".ProseMirror p");
const listRules = [
declarationsForSelector(source, ".ProseMirror ul"),
declarationsForSelector(source, ".ProseMirror ol"),
declarationsForSelector(source, ".ProseMirror li"),
].join("\n");
expect(editorRules).not.toMatch(/(?:font-size|line-height)\s*:/);
expect(paragraphRules).not.toMatch(/(?:line-height|margin|padding)(?:-[a-z]+)?\s*:/);
expect(listRules).not.toMatch(/(?:line-height|margin)(?:-[a-z]+)?\s*:/);
}
});
});
+2 -1
View File
@@ -41,7 +41,7 @@
--dark-surface-rgb: 255 255 255;
--editor-body-font-size: 0.9375rem;
--editor-body-line-height: 1.6;
--editor-paragraph-spacing: 0.45rem;
--editor-paragraph-spacing: 0.35rem;
--memo-content-divider-color: rgb(var(--brand-green-300-rgb));
--memo-content-divider-spacing: 1.5rem;
--editor-outline-width: 300px;
@@ -286,6 +286,7 @@ textarea {
.ProseMirror p {
line-height: var(--editor-body-line-height);
margin: 0 0 var(--editor-paragraph-spacing);
padding: 0;
}
.ProseMirror a.edgeever-note-link,
@@ -397,7 +397,7 @@ input {
padding: 18px 12px 32px;
color: #020617;
font-size: 17px;
line-height: 1.7;
line-height: 1.6;
-webkit-user-select: text;
user-select: text;
caret-color: auto;
@@ -405,7 +405,9 @@ input {
}
.edgeever-mobile-tiptap-content p {
margin: 0 0 0.85em;
margin: 0 0 6px;
padding: 0;
line-height: 1.6;
}
.edgeever-mobile-tiptap-content h1,
@@ -3,7 +3,7 @@ import { MEMO_CONTENT_STYLE } from "./memo-content-style.ts";
describe("shared memo content style", () => {
test("matches the established mobile PWA reading scale", () => {
expect(MEMO_CONTENT_STYLE.body).toEqual({ fontSize: 15, lineHeight: 24 });
expect(MEMO_CONTENT_STYLE.body).toEqual({ fontSize: 15, lineHeight: 24, paragraphSpacing: 6 });
expect(MEMO_CONTENT_STYLE.divider).toEqual({
color: { dark: "#4ade80", light: "#66ca80" },
marginVertical: 24,
@@ -2,6 +2,7 @@ export const MEMO_CONTENT_STYLE = {
body: {
fontSize: 15,
lineHeight: 24,
paragraphSpacing: 6,
},
divider: {
color: {