Unify editor text spacing across themes
This commit is contained in:
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: {
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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;";
|
||||
});
|
||||
|
||||
@@ -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*:/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user