From dfdcfa11e482a8dcae59e03d43b55d14379bb196 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 13 Sep 2026 18:56:14 +0800 Subject: [PATCH] feat(web): give paper themes independent palettes and tidy theme UX Each paper layout now carries its own colors, ornaments, and reading rhythm instead of mixing with native green. Drop Classic Blue & White, unify preset names, move theme switching fully into the editor toolbar, and keep settings for custom palettes only. --- README.md | 6 +- README.zh-CN.md | 6 +- apps/api/src/demo-seed-data.ts | 4 +- apps/web/src/components/EditorToolbar.tsx | 6 +- apps/web/src/components/ThemeProvider.tsx | 19 +- .../settings/PreferenceCard.test.mjs | 19 +- .../components/settings/PreferenceCard.tsx | 121 +- apps/web/src/lib/custom-editor-theme.test.mjs | 9 + apps/web/src/lib/custom-editor-theme.ts | 16 +- apps/web/src/lib/dark-theme.test.mjs | 10 - apps/web/src/lib/publish-layout.test.mjs | 111 +- apps/web/src/lib/publish-layout.ts | 1511 ++++++++++++++--- apps/web/src/lib/publish-ornaments.test.mjs | 65 + apps/web/src/lib/publish-ornaments.ts | 115 ++ apps/web/src/lib/wechat-copy.ts | 44 + apps/web/src/styles/editor-themes/index.css | 1 - apps/web/src/styles/editor-themes/marxico.css | 390 ----- .../web/src/styles/editor-typography.test.mjs | 25 +- apps/web/src/styles/globals.css | 2 +- apps/web/src/styles/mermaid-layout.test.mjs | 6 - apps/web/src/styles/publish-layout.css | 597 +++++-- docs/demo-sample-note.md | 2 +- docs/demo-sample-note.zh-CN.md | 2 +- packages/shared/src/i18n/en-US.ts | 14 +- packages/shared/src/i18n/zh-CN.ts | 14 +- 25 files changed, 2108 insertions(+), 1007 deletions(-) create mode 100644 apps/web/src/lib/publish-ornaments.test.mjs create mode 100644 apps/web/src/lib/publish-ornaments.ts delete mode 100644 apps/web/src/styles/editor-themes/marxico.css diff --git a/README.md b/README.md index c0c2b6a0..ca4c823c 100644 --- a/README.md +++ b/README.md @@ -271,9 +271,9 @@ Web, PWA, and desktop upload memo edits after 30 seconds of inactivity and check - EdgeEver's note-taking product design was also informed by the publicly available product experiences of mature note-taking tools such as [Evernote](https://evernote.com/). The related features were independently designed and implemented by EdgeEver. - The product design of mind-map and visual-diagram notes was informed by the publicly available product experiences of [XMind](https://xmind.com/) and [ProcessOn](https://www.processon.com/). These features were independently designed and implemented by EdgeEver. -- 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 "Classic Blue & White" theme is inspired by the early [StackEdit](https://github.com/benweet/stackedit)/[Bootstrap](https://github.com/twbs/bootstrap) Markdown typography style, with Chinese typography details informed by [Marxico](https://maxiang.io/). +- The "Minimal Green" theme typography layout is inspired by [obsidian-minimal](https://github.com/kepano/obsidian-minimal). +- The "Ruled Green" theme typography layout is inspired by [Outline](https://github.com/outline/outline). +- Paper editor themes (Letter, Guide, Blueprint, Journal, Stance, Stub, Brief, Outline, Whitespace, and Grove) take editorial cues from the public typography presets on [墨格](https://moyufang.cn/editor). The implementations, assets, and copy are original to EdgeEver. ## Trademark and Brand Use diff --git a/README.zh-CN.md b/README.zh-CN.md index 665cc84a..3fa6c67e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -268,9 +268,9 @@ Web、PWA 与桌面端会在停止编辑 30 秒后上传笔记,并在页面可 - EdgeEver 的笔记产品设计也参考了 [Evernote(印象笔记)](https://evernote.com/) 等成熟笔记工具的公开产品体验。相关功能由 EdgeEver 独立设计与实现。 - 思维导图与可视化图表笔记的产品设计参考了 [XMind](https://xmind.com/) 和 [ProcessOn](https://www.processon.com/) 等图表工具的公开产品体验。相关功能由 EdgeEver 独立设计与实现。 -- “minimal品牌绿”主题排版架构借鉴于 [obsidian-minimal](https://github.com/kepano/obsidian-minimal)。 -- “Outline 品牌绿”主题排版架构借鉴于 [Outline](https://github.com/outline/outline)。 -- “经典蓝白”主题借鉴了早期 [StackEdit](https://github.com/benweet/stackedit)/[Bootstrap](https://github.com/twbs/bootstrap) 系 Markdown 排版风格,并参考[马克飞象](https://maxiang.io/)完善中文排版细节。 +- “极简绿”主题排版架构借鉴于 [obsidian-minimal](https://github.com/kepano/obsidian-minimal)。 +- “描边绿”主题排版架构借鉴于 [Outline](https://github.com/outline/outline)。 +- 暖笺、教程、蓝图、刊物、立场、存根、内参、纲要、留白、草木等纸稿编辑器主题,参考了 [墨格](https://moyufang.cn/editor) 公开排版方案的标题层级、章节数字与引言结构。实现、素材与文案均由 EdgeEver 独立完成。 ## 商标与品牌使用 diff --git a/apps/api/src/demo-seed-data.ts b/apps/api/src/demo-seed-data.ts index a84e5be5..f0fb9e8f 100644 --- a/apps/api/src/demo-seed-data.ts +++ b/apps/api/src/demo-seed-data.ts @@ -94,7 +94,7 @@ EdgeEver 将高效与优雅融入每一处交互细节,助你专注于思考 - **阅读保护模式 (Reading Protection)**:日常翻阅或查阅笔记时,按下 \`Cmd/Ctrl + E\` 即可一键开启只读保护,锁定当前编辑状态,避免沉浸阅读时误触键盘或意外改动笔记内容;再次按下即可随手切回编辑。 ### 🎭 精选排版主题与一键自媒体发布 -- **内置排版主题**:支持一键切换 \`WeChat Classic Green (微信经典绿)\`、\`Modern Mint (薄荷青)\`、\`Minimal Emerald (极简祖母绿)\`、\`Outline Emerald (大纲祖母绿)\` 等风格。 +- **内置排版主题**:支持一键切换 \`微信绿\`、\`薄荷绿\`、\`极简绿\`、\`描边绿\`,以及暖笺、教程、蓝图等纸稿风格。 - **一键排版复制**:专为内容创作者设计。点击顶部工具栏的**微信公众号图标**,系统自动将当前笔记转为内联 CSS 样式的优雅富文本,直接粘贴至微信公众号后台、Substack 或 WordPress,排版与代码高亮完美保真。 ### 🖼️ 8 套精美社交分享海报 (Poster Cards) @@ -313,7 +313,7 @@ EdgeEver is engineered to provide a distraction-free, elegant writing experience - **Reading Protection Mode**: When reading or reviewing notes, press \`Cmd/Ctrl + E\` to toggle read-only protection, locking the editor to prevent accidental edits while browsing; press it again to seamlessly resume editing. ### 🎭 Curated Typography Themes & Publishing Export -- **Preset Editor Themes**: Switch effortlessly between \`WeChat Classic Green\`, \`Modern Mint\`, \`Minimal Emerald\`, \`Outline Emerald\`, and more. +- **Preset Editor Themes**: Switch effortlessly between \`WeChat Green\`, \`Mint Green\`, \`Minimal Green\`, \`Ruled Green\`, and paper styles such as Letter, Guide, and Blueprint. - **One-Click Publishing Export**: Built for publishers and bloggers. Click the **WeChat Icon** in the top bar to format your note with inline CSS. Paste directly into WeChat Official Account editor, Substack, Medium, or WordPress while preserving layout and syntax highlighting. ### 🖼️ 8 Exquisite Social Poster Themes diff --git a/apps/web/src/components/EditorToolbar.tsx b/apps/web/src/components/EditorToolbar.tsx index 3e461429..d105167d 100644 --- a/apps/web/src/components/EditorToolbar.tsx +++ b/apps/web/src/components/EditorToolbar.tsx @@ -38,6 +38,7 @@ import { wrapIndentedParagraphInList } from "@/lib/editor-shortcuts"; import { EDITOR_THEME_NAMES, MARKDOWN_THEME_PREFERENCES, + localizeStoredCustomThemeName, useEditorTheme, useMarkdownTheme, } from "@/components/ThemeProvider"; @@ -413,7 +414,10 @@ export const EditorToolbar = ({ ))} {customEditorThemes.map((theme) => ( - {theme.name} + {localizeStoredCustomThemeName(theme.name, { + defaultName: t("settings.customEditorTheme.defaultName"), + newName: (index) => t("settings.customEditorTheme.newName", { n: index }), + })} ))} diff --git a/apps/web/src/components/ThemeProvider.tsx b/apps/web/src/components/ThemeProvider.tsx index d684a33d..8e6d9b46 100644 --- a/apps/web/src/components/ThemeProvider.tsx +++ b/apps/web/src/components/ThemeProvider.tsx @@ -12,6 +12,7 @@ export { DEFAULT_CUSTOM_DARK_COLORS, DEFAULT_CUSTOM_EDITOR_THEME, DEFAULT_CUSTOM_LIGHT_COLORS, + localizeStoredCustomThemeName, } from "@/lib/custom-editor-theme"; export type { CustomEditorTheme, ThemeColors } from "@/lib/custom-editor-theme"; @@ -98,6 +99,8 @@ export const isMarkdownLightTheme = (theme: MarkdownThemeName): boolean => export const EDITOR_THEME_NAMES = [ "default", + "minimal-emerald", + "outline-emerald", "letter", "guide", "blueprint", @@ -108,11 +111,8 @@ export const EDITOR_THEME_NAMES = [ "outline", "zen", "grove", - "minimal-emerald", - "outline-emerald", "wechat-green", "modern-mint", - "marxico", "custom", ] as const; export type EditorThemeName = string; @@ -216,7 +216,16 @@ export const resolveMarkdownTheme = ( : preference; export const getStoredEditorTheme = (): string => { - return readLocalStorageItem(EDITOR_THEME_STORAGE_KEY) || "default"; + const stored = readLocalStorageItem(EDITOR_THEME_STORAGE_KEY) || "default"; + if (stored !== "marxico") return stored; + if (typeof window !== "undefined") { + try { + window.localStorage.setItem(EDITOR_THEME_STORAGE_KEY, "default"); + } catch { + // Private mode / blocked storage — preference stays session-only. + } + } + return "default"; }; const normalizeCustomEditorTheme = (theme: CustomEditorTheme): CustomEditorTheme => ({ @@ -248,7 +257,7 @@ export const getStoredCustomEditorThemes = (): CustomEditorTheme[] => { if (oldTheme && typeof oldTheme.name === "string") { const migratedTheme: CustomEditorTheme = { id: "custom-migrated", - name: oldTheme.name || "My custom theme", + name: oldTheme.name || DEFAULT_CUSTOM_EDITOR_THEME.name, light: normalizeThemeColors({ background: oldTheme.background || DEFAULT_CUSTOM_LIGHT_COLORS.background, text: oldTheme.text || DEFAULT_CUSTOM_LIGHT_COLORS.text, diff --git a/apps/web/src/components/settings/PreferenceCard.test.mjs b/apps/web/src/components/settings/PreferenceCard.test.mjs index 090864bb..c187fbbb 100644 --- a/apps/web/src/components/settings/PreferenceCard.test.mjs +++ b/apps/web/src/components/settings/PreferenceCard.test.mjs @@ -25,21 +25,16 @@ describe("appearance preference", () => { }); describe("paper editor themes", () => { - test("offers the paper editor themes alongside existing presets", () => { + test("keeps theme switching in the editor toolbar instead of settings", () => { const preferenceCard = readFileSync(new URL("./PreferenceCard.tsx", import.meta.url), "utf8"); + const editorToolbar = readFileSync(new URL("../EditorToolbar.tsx", import.meta.url), "utf8"); expect(preferenceCard).not.toContain('t("settings.publishLayoutTitle")'); - expect(preferenceCard).toContain('value="letter"'); - expect(preferenceCard).toContain('t("settings.editorThemes.letter")'); - expect(preferenceCard).toContain('t("settings.editorThemes.guide")'); - expect(preferenceCard).toContain('t("settings.editorThemes.blueprint")'); - expect(preferenceCard).toContain('t("settings.editorThemes.journal")'); - expect(preferenceCard).toContain('t("settings.editorThemes.stance")'); - expect(preferenceCard).toContain('t("settings.editorThemes.stub")'); - expect(preferenceCard).toContain('t("settings.editorThemes.brief")'); - expect(preferenceCard).toContain('t("settings.editorThemes.outline")'); - expect(preferenceCard).toContain('t("settings.editorThemes.zen")'); - expect(preferenceCard).toContain('t("settings.editorThemes.grove")'); + expect(preferenceCard).not.toContain('t("settings.editorThemeTitle")'); + expect(preferenceCard).not.toContain('t("settings.markdownThemeTitle")'); + expect(preferenceCard).toContain('t("settings.customEditorTheme.settingsTitle")'); + expect(editorToolbar).toContain('t(`settings.editorThemes.${theme}`)'); + expect(editorToolbar).toContain("markdownThemePreference"); }); }); diff --git a/apps/web/src/components/settings/PreferenceCard.tsx b/apps/web/src/components/settings/PreferenceCard.tsx index 81bd00f5..9a1a659b 100644 --- a/apps/web/src/components/settings/PreferenceCard.tsx +++ b/apps/web/src/components/settings/PreferenceCard.tsx @@ -1,4 +1,4 @@ -import { AlignHorizontalJustifyCenter, ChartNoAxesCombined, FileCode2, Image, Keyboard, Languages, MousePointerClick, Palette, Sparkles, SunMoon } from "lucide-react"; +import { AlignHorizontalJustifyCenter, ChartNoAxesCombined, Image, Keyboard, Languages, MousePointerClick, Palette, Sparkles, SunMoon } from "lucide-react"; import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import type { EditorContentAlignment, ShortcutSettings } from "@/lib/app-helpers"; @@ -40,15 +40,13 @@ import { import { ShortcutSettingsItem } from "./ShortcutSettingsItem"; import { CustomEditorThemeDialog } from "./CustomEditorThemeDialog"; import { - MARKDOWN_THEME_PREFERENCES, MERMAID_THEME_PREFERENCES, useAppearanceTheme, useEditorTheme, - useMarkdownTheme, useMermaidTheme, DEFAULT_CUSTOM_LIGHT_COLORS, DEFAULT_CUSTOM_DARK_COLORS, - isNamedEditorTheme, + localizeStoredCustomThemeName, type CustomEditorTheme, type ThemePreference, } from "../ThemeProvider"; @@ -79,7 +77,6 @@ export const PreferenceCard = ({ } = useEditorTheme(); const { preference: appearancePreference, setPreference: setAppearancePreference } = useAppearanceTheme(); const { mermaidThemePreference, setMermaidTheme } = useMermaidTheme(); - const { markdownThemePreference, setMarkdownTheme } = useMarkdownTheme(); const [customThemeDialogOpen, setCustomThemeDialogOpen] = useState(false); const [editingTheme, setEditingTheme] = useState(null); const [activeLocalePreference, setActiveLocalePreference] = useState(() => getAppLocalePreference()); @@ -151,15 +148,20 @@ export const PreferenceCard = ({ }, []); const activeCustom = customEditorThemes.find((t) => t.id === editorTheme); - const isPreset = isNamedEditorTheme(editorTheme); + const customThemeLabel = (name: string) => + localizeStoredCustomThemeName(name, { + defaultName: t("settings.customEditorTheme.defaultName"), + newName: (index) => t("settings.customEditorTheme.newName", { n: index }), + }); const handleEditClick = () => { - if (activeCustom) { - setEditingTheme(activeCustom); + const target = activeCustom ?? customEditorThemes[0]; + if (target) { + setEditingTheme({ ...target, name: customThemeLabel(target.name) }); } else { const newTheme: CustomEditorTheme = { id: `custom-${Date.now()}`, - name: `New theme ${customEditorThemes.length + 1}`, + name: t("settings.customEditorTheme.newName", { n: customEditorThemes.length + 1 }), light: DEFAULT_CUSTOM_LIGHT_COLORS, dark: DEFAULT_CUSTOM_DARK_COLORS, }; @@ -279,99 +281,24 @@ export const PreferenceCard = ({ -
-
- -
-
{t("settings.editorThemeTitle")}
-
{t("settings.editorThemeDescription")}
+ {!isMobile && ( +
+
+ +
+
{t("settings.customEditorTheme.settingsTitle")}
+
{t("settings.customEditorTheme.settingsDescription")}
+
-
-
- - {!isMobile && ( +
- )} -
-
- -
-
- -
-
{t("settings.markdownThemeTitle")}
-
{t("settings.markdownThemeDescription")}
-
- -
-
+ )}
diff --git a/apps/web/src/lib/custom-editor-theme.test.mjs b/apps/web/src/lib/custom-editor-theme.test.mjs index cd2a0922..1d56e704 100644 --- a/apps/web/src/lib/custom-editor-theme.test.mjs +++ b/apps/web/src/lib/custom-editor-theme.test.mjs @@ -3,8 +3,10 @@ import { CUSTOM_EDITOR_THEME_FILE_SCHEMA, CUSTOM_EDITOR_THEME_FILE_VERSION, CustomEditorThemeFileError, + DEFAULT_CUSTOM_EDITOR_THEME_NAME, DEFAULT_CUSTOM_LIGHT_COLORS, DEFAULT_CUSTOM_DARK_COLORS, + localizeStoredCustomThemeName, normalizeThemeColors, customEditorThemeFileName, getEditorThemeContrastIssues, @@ -106,4 +108,11 @@ describe("custom editor theme files", () => { test("creates a filesystem-safe descriptive filename", () => { expect(customEditorThemeFileName(theme.name)).toBe("EdgeEver-Calm - Green.json"); }); + + test("localizes factory custom theme names without rewriting user names", () => { + const copy = { defaultName: "自定义主题", newName: (index) => `新主题 ${index}` }; + expect(localizeStoredCustomThemeName(DEFAULT_CUSTOM_EDITOR_THEME_NAME, copy)).toBe("自定义主题"); + expect(localizeStoredCustomThemeName("New theme 2", copy)).toBe("新主题 2"); + expect(localizeStoredCustomThemeName("Calm / Green", copy)).toBe("Calm / Green"); + }); }); diff --git a/apps/web/src/lib/custom-editor-theme.ts b/apps/web/src/lib/custom-editor-theme.ts index fe5d5a59..363c26ea 100644 --- a/apps/web/src/lib/custom-editor-theme.ts +++ b/apps/web/src/lib/custom-editor-theme.ts @@ -41,14 +41,28 @@ export const DEFAULT_CUSTOM_DARK_COLORS: ThemeColors = { border: "#44403c", }; +export const DEFAULT_CUSTOM_EDITOR_THEME_NAME = "My custom theme"; + export const DEFAULT_CUSTOM_EDITOR_THEME: CustomEditorTheme = { id: "custom-default", - name: "My custom theme", + name: DEFAULT_CUSTOM_EDITOR_THEME_NAME, light: DEFAULT_CUSTOM_LIGHT_COLORS, dark: DEFAULT_CUSTOM_DARK_COLORS, customCss: "", }; +const NEW_THEME_NAME_PATTERN = /^New theme (\d+)$/; + +export const localizeStoredCustomThemeName = ( + name: string, + copy: { defaultName: string; newName: (index: number) => string }, +) => { + if (name === DEFAULT_CUSTOM_EDITOR_THEME_NAME) return copy.defaultName; + const match = NEW_THEME_NAME_PATTERN.exec(name); + if (match) return copy.newName(Number(match[1])); + return name; +}; + export const CUSTOM_EDITOR_THEME_FILE_SCHEMA = "edgeever.editor-theme"; export const CUSTOM_EDITOR_THEME_FILE_VERSION = 1; export const MAX_CUSTOM_EDITOR_THEME_FILE_BYTES = 64 * 1024; diff --git a/apps/web/src/lib/dark-theme.test.mjs b/apps/web/src/lib/dark-theme.test.mjs index bf481429..21387acb 100644 --- a/apps/web/src/lib/dark-theme.test.mjs +++ b/apps/web/src/lib/dark-theme.test.mjs @@ -12,7 +12,6 @@ const BUILT_IN_EDITOR_THEMES = [ "outline-emerald", "wechat-green", "modern-mint", - "marxico", ]; const readDarkThemeTokens = (theme) => { @@ -107,15 +106,6 @@ describe("dark theme contracts", () => { } }); - test("Marxico keeps note content legible in dark mode", () => { - const { css, tokens } = readDarkThemeTokens("marxico"); - - expect(css).toContain(':root.dark .edgeever-editor[data-editor-theme="marxico"]'); - expect(css).toContain("color: var(--editor-theme-text);"); - expect(contrastRatio(tokens.text, "#191e1b")).toBeGreaterThanOrEqual(4.5); - expect(contrastRatio(tokens.heading, "#191e1b")).toBeGreaterThanOrEqual(4.5); - }); - test("automatic Mermaid themes follow the resolved appearance", () => { expect(resolveMermaidTheme("auto", "light")).toBe("zinc-light"); expect(resolveMermaidTheme("auto", "dark")).toBe("zinc-dark"); diff --git a/apps/web/src/lib/publish-layout.test.mjs b/apps/web/src/lib/publish-layout.test.mjs index 4d0c3165..0e553c95 100644 --- a/apps/web/src/lib/publish-layout.test.mjs +++ b/apps/web/src/lib/publish-layout.test.mjs @@ -3,6 +3,8 @@ import { readFileSync } from "node:fs"; import { DEFAULT_PUBLISH_LAYOUT, DEFAULT_PUBLISH_PALETTE, + PAPER_EDITOR_THEME_PALETTES, + PUBLISH_LAYOUT_CHROME, PUBLISH_LAYOUT_IDS, PUBLISH_LAYOUT_RHYTHM, PUBLISH_PALETTE_IDS, @@ -19,7 +21,7 @@ import { } from "./publish-layout.ts"; describe("publish layout catalog", () => { - test("keeps ten paper editor themes and eight palettes", () => { + test("keeps ten paper editor themes and a palette per layout", () => { expect(PUBLISH_LAYOUT_IDS).toEqual([ "letter", "guide", @@ -32,15 +34,46 @@ describe("publish layout catalog", () => { "zen", "grove", ]); - expect(PUBLISH_PALETTE_IDS).toHaveLength(8); + expect(PUBLISH_PALETTE_IDS).toEqual([ + "emerald", + "ink", + "dawn", + "clay", + "navy", + "slate", + "plum", + "forest", + "azure", + "amber", + "teal", + "violet", + "mist", + ]); expect(DEFAULT_PUBLISH_LAYOUT).toBe("letter"); expect(DEFAULT_PUBLISH_PALETTE).toBe("emerald"); expect(PUBLISH_PALETTES.emerald.accent).toBe("#16A06E"); + expect(PUBLISH_PALETTES.dawn.accent).toBe("#D9A521"); + expect(PUBLISH_PALETTES.azure.accent).toBe("#2F80ED"); + expect(PUBLISH_PALETTES.azure.surface).toBe("#F5F8FC"); + expect(PUBLISH_PALETTES.azure.surface).not.toBe("#F0FDF4"); + expect(PUBLISH_PALETTES.clay.accent).toBe("#E02D32"); }); test("maps paper editor themes to a fixed layout and palette", () => { expect(resolvePaperEditorTheme("letter")).toEqual({ layout: "letter", palette: "dawn" }); - expect(resolvePaperEditorTheme("guide")).toEqual({ layout: "guide", palette: "emerald" }); + expect(resolvePaperEditorTheme("guide")).toEqual({ layout: "guide", palette: "azure" }); + expect(PAPER_EDITOR_THEME_PALETTES).toEqual({ + letter: "dawn", + guide: "azure", + blueprint: "navy", + journal: "slate", + stance: "clay", + stub: "amber", + brief: "teal", + outline: "violet", + zen: "mist", + grove: "forest", + }); expect(resolvePaperEditorTheme("default")).toBeNull(); }); @@ -51,13 +84,18 @@ describe("publish layout catalog", () => { expect(resolveStoredPublishPalette("dawn")).toBe("dawn"); }); - test("numbers letter and blueprint chapters without CSS counters", () => { + test("numbers magazine chapters without CSS counters", () => { expect(formatChapterIndex(0)).toBe("01"); expect(planHeadingDecoration("h2", "letter", 0)).toEqual({ kind: "chapter", chapterLabel: "01" }); - expect(planHeadingDecoration("h2", "blueprint", 1)).toEqual({ kind: "chapter", chapterLabel: "02" }); + expect(planHeadingDecoration("h2", "guide", 0)).toEqual({ kind: "chapter", chapterLabel: "01" }); + expect(planHeadingDecoration("h2", "blueprint", 1)).toEqual({ kind: "none" }); expect(planHeadingDecoration("h2", "stub", 0)).toEqual({ kind: "chapter", chapterLabel: "01" }); - expect(planHeadingDecoration("h2", "guide", 0)).toEqual({ kind: "none" }); + expect(planHeadingDecoration("h2", "grove", 0)).toEqual({ kind: "none" }); expect(planHeadingDecoration("h1", "letter", 0)).toEqual({ kind: "title" }); + expect(PUBLISH_LAYOUT_CHROME.letter.chapter).toBe("aside-serif"); + expect(PUBLISH_LAYOUT_CHROME.letter.quote).toBe("grid-card"); + expect(PUBLISH_LAYOUT_CHROME.brief.quoteBanner).toBe("EDITOR'S NOTE"); + expect(PUBLISH_LAYOUT_CHROME.grove.small).toBe("slash"); }); }); @@ -67,24 +105,43 @@ describe("publish layout CSS", () => { const desktop = buildPublishStyles("letter", PUBLISH_PALETTES.emerald, "desktop"); expect(phone.p).toContain("font-size: 15px"); - expect(phone.p).toContain("line-height: 1.9"); - expect(phone.p).toContain("margin: 0 0 16px"); + expect(phone.p).toContain("line-height: 2"); + expect(phone.p).toContain("letter-spacing: 0.06em"); + expect(phone.p).toContain("margin: 0 0 28px"); expect(phone.h1).toContain("font-size: 24px"); expect(phone.h2).toContain("font-size: 20px"); expect(desktop.p).toContain("font-size: 15px"); - expect(desktop.p).toContain("line-height: 1.9"); + expect(desktop.p).toContain("line-height: 2"); }); test("uses a looser reading rhythm than the note editor contract", () => { - const letter = buildPublishStyles("letter", PUBLISH_PALETTES.emerald); - const guide = buildPublishStyles("guide", PUBLISH_PALETTES.emerald); + const letter = buildPublishStyles("letter", PUBLISH_PALETTES.dawn); + const blueprint = buildPublishStyles("blueprint", PUBLISH_PALETTES.navy); - expect(letter.p).toContain("line-height: 1.9"); - expect(letter.p).toContain("margin: 0 0 24px"); + expect(letter.p).toContain("line-height: 2"); + expect(letter.p).toContain("letter-spacing: 0.06em"); + expect(letter.p).toContain("margin: 0 0 28px"); expect(letter.h1).toContain("text-align: center"); - expect(guide.h2).toContain("border-left: 4px solid #16A06E"); - expect(letter.blockquote).toContain("border-radius: 12px"); - expect(letter.strong).toContain("color: #16A06E"); + expect(letter.h1).toContain("letter-spacing: 0.08em"); + expect(blueprint.h2).toContain("border-left: 3px solid #4271B3"); + expect(letter.blockquote).toContain("border-radius: 18px"); + expect(letter.strong).toContain("color: #D9A521"); + expect(letter.th).toContain("background: #FFF7DF"); + expect(letter.a).toContain("color: #7C5500"); + }); + + test("gives each paper theme its own line height and tracking", () => { + const stance = buildPublishStyles("stance", PUBLISH_PALETTES.clay); + const zen = buildPublishStyles("zen", PUBLISH_PALETTES.mist); + const stub = buildPublishStyles("stub", PUBLISH_PALETTES.amber); + + expect(PUBLISH_LAYOUT_RHYTHM.stance.lineHeight).toBe("1.68"); + expect(PUBLISH_LAYOUT_RHYTHM.zen.lineHeight).toBe("2.2"); + expect(stance.p).toContain("letter-spacing: 0em"); + expect(zen.p).toContain("letter-spacing: 0.1em"); + expect(stub.h1).toContain("letter-spacing: 0.14em"); + expect(new Set(Object.values(PUBLISH_LAYOUT_RHYTHM).map((rhythm) => rhythm.lineHeight)).size).toBe(10); + expect(new Set(Object.values(PUBLISH_LAYOUT_RHYTHM).map((rhythm) => rhythm.letterSpacing)).size).toBe(10); }); test("keeps WeChat-safe inline CSS for every layout and palette", () => { @@ -110,11 +167,12 @@ describe("copy pipeline wiring", () => { expect(source).toContain("ProseMirror-separator"); expect(source).toContain("findOriginalImage"); expect(source).toContain("svgToWeChatImage"); + expect(source).toContain("rasterizePublishOrnamentsForWeChat"); expect(source).toContain('fillStyle = "#ffffff"'); expect(source).toContain("image/jpeg"); expect(source).toContain("text-align: center"); expect(preferenceCard).not.toContain('t("settings.publishLayoutTitle")'); - expect(preferenceCard).toContain('t("settings.editorThemes.letter")'); + expect(preferenceCard).not.toContain('t("settings.editorThemeTitle")'); }); test("drives the rich editor rhythm from paper editor themes", () => { @@ -122,10 +180,19 @@ describe("copy pipeline wiring", () => { const css = readFileSync(new URL("../styles/publish-layout.css", import.meta.url), "utf8"); const vars = publishEditorCssVars("letter", "dawn"); - expect(PUBLISH_LAYOUT_RHYTHM.letter.lineHeight).toBe("1.9"); - expect(PUBLISH_LAYOUT_RHYTHM.letter.paragraphSpacing).toBe("24px"); - expect(vars["--editor-body-line-height"]).toBe("1.9"); - expect(vars["--publish-accent"]).toBe("#C49A3C"); + expect(PUBLISH_LAYOUT_RHYTHM.letter.lineHeight).toBe("2"); + expect(PUBLISH_LAYOUT_RHYTHM.letter.paragraphSpacing).toBe("28px"); + expect(vars["--editor-body-line-height"]).toBe("2"); + expect(vars["--editor-letter-spacing"]).toBe("0.06em"); + expect(vars["--editor-heading-letter-spacing"]).toBe("0.08em"); + expect(css).toContain("letter-spacing: var(--editor-letter-spacing)"); + expect(css).toContain("letter-spacing: var(--editor-heading-letter-spacing)"); + expect(vars["--publish-accent"]).toBe("#D9A521"); + expect(vars["--publish-accent-grid"]).toBe("rgba(217,165,33,0.14)"); + expect(vars["--brand-green-rgb"]).toBe("217 165 33"); + expect(vars["--publish-ornament-brush"]).toContain("url(\"data:image/svg+xml"); + expect(css).toContain("--editor-theme-accent: var(--publish-accent)"); + expect(css).toContain("--brand-green: var(--publish-accent)"); expect(editorPane).toContain("isPaperEditorTheme(editorTheme)"); expect(editorPane).toContain('data-paper-theme={isPaperEditorTheme(editorTheme) ? "true" : undefined}'); expect(editorPane).toContain("publishEditorCssVars"); @@ -135,7 +202,7 @@ describe("copy pipeline wiring", () => { expect(css).toContain('[data-editor-theme="letter"]'); expect(css).toContain('[data-editor-theme="stance"]'); expect(css).toContain('[data-editor-theme="grove"]'); - expect(css).not.toMatch(/\.ProseMirror p\s*\{[^}]*line-height\s*:/); + expect(css).toContain("line-height: var(--editor-body-line-height)"); }); test("phone preview markup applies the paper layout instead of raw editor chrome", () => { diff --git a/apps/web/src/lib/publish-layout.ts b/apps/web/src/lib/publish-layout.ts index 37a6b7be..cc6eb2e3 100644 --- a/apps/web/src/lib/publish-layout.ts +++ b/apps/web/src/lib/publish-layout.ts @@ -1,3 +1,10 @@ +import { + PUBLISH_LAYOUT_ORNAMENTS, + publishOrnamentCssVars, + publishOrnamentDataUri, + type PublishOrnamentId, +} from "./publish-ornaments"; + export const PAPER_EDITOR_THEME_IDS = [ "letter", "guide", @@ -22,6 +29,11 @@ export const PUBLISH_PALETTE_IDS = [ "slate", "plum", "forest", + "azure", + "amber", + "teal", + "violet", + "mist", ] as const; export type PublishPaletteId = (typeof PUBLISH_PALETTE_IDS)[number]; @@ -30,14 +42,14 @@ export const DEFAULT_PUBLISH_PALETTE: PublishPaletteId = "emerald"; export const PAPER_EDITOR_THEME_PALETTES = { letter: "dawn", - guide: "emerald", + guide: "azure", blueprint: "navy", journal: "slate", stance: "clay", - stub: "dawn", - brief: "forest", - outline: "ink", - zen: "slate", + stub: "amber", + brief: "teal", + outline: "violet", + zen: "mist", grove: "forest", } as const satisfies Record; @@ -58,6 +70,7 @@ export type PublishPalette = { accent: string; accentSoft: string; accentBorder: string; + accentGrid: string; text: string; textStrong: string; textMuted: string; @@ -69,12 +82,17 @@ export type PublishPalette = { bg: string; }; +const palette = ( + id: PublishPaletteId, + colors: Omit, +): PublishPalette => ({ id, ...colors }); + export const PUBLISH_PALETTES: Record = { - emerald: { - id: "emerald", + emerald: palette("emerald", { accent: "#16A06E", accentSoft: "#E8F6F0", accentBorder: "#B7E0CC", + accentGrid: "rgba(22,160,110,0.14)", text: "#2C3A34", textStrong: "#14241C", textMuted: "#5C6F66", @@ -84,12 +102,12 @@ export const PUBLISH_PALETTES: Record = { codeBg: "#F3F7F5", codeText: "#0F7A54", bg: "#FFFFFF", - }, - ink: { - id: "ink", + }), + ink: palette("ink", { accent: "#1A5C4A", accentSoft: "#E7F2EE", accentBorder: "#B5D2C8", + accentGrid: "rgba(26,92,74,0.14)", text: "#24332E", textStrong: "#12201B", textMuted: "#5A6C65", @@ -99,72 +117,72 @@ export const PUBLISH_PALETTES: Record = { codeBg: "#EEF4F1", codeText: "#164C3E", bg: "#FFFFFF", - }, - dawn: { - id: "dawn", - accent: "#C49A3C", - accentSoft: "#FBF6EA", - accentBorder: "#E8D7A8", - text: "#3D3426", - textStrong: "#2A2418", - textMuted: "#7A6E58", - surface: "#FFFCF6", - divider: "#EFE3C8", - link: "#8A6420", - codeBg: "#FBF6EA", - codeText: "#8A6420", + }), + dawn: palette("dawn", { + accent: "#D9A521", + accentSoft: "#FFF7DF", + accentBorder: "#EAD89F", + accentGrid: "rgba(217,165,33,0.14)", + text: "#3D2A1B", + textStrong: "#3A3020", + textMuted: "#74613C", + surface: "#FFFDF8", + divider: "#F0E3BD", + link: "#7C5500", + codeBg: "#FFFAF0", + codeText: "#7C5500", bg: "#FFFFFF", - }, - clay: { - id: "clay", - accent: "#C45C3E", - accentSoft: "#FBEFEA", - accentBorder: "#E8C4B6", - text: "#3D2E29", - textStrong: "#271C18", - textMuted: "#7A635B", - surface: "#FDF8F6", - divider: "#EEDDD6", - link: "#9A3E28", - codeBg: "#F8EEE9", - codeText: "#9A3E28", + }), + clay: palette("clay", { + accent: "#E02D32", + accentSoft: "#FFF1F1", + accentBorder: "#FFC2C2", + accentGrid: "rgba(224,45,50,0.14)", + text: "#374151", + textStrong: "#1C1917", + textMuted: "#374151", + surface: "#FFF7F7", + divider: "#FFD6D6", + link: "#B42318", + codeBg: "#FFF1F1", + codeText: "#B42318", bg: "#FFFFFF", - }, - navy: { - id: "navy", - accent: "#3B6B9A", - accentSoft: "#EAF1F8", - accentBorder: "#B7C9DC", - text: "#2C3640", - textStrong: "#162029", - textMuted: "#5C6A78", - surface: "#F5F8FB", - divider: "#D7E1EB", - link: "#2A5278", + }), + navy: palette("navy", { + accent: "#4271B3", + accentSoft: "#F3F4F7", + accentBorder: "#C8D0DB", + accentGrid: "rgba(66,113,179,0.14)", + text: "#3D4650", + textStrong: "#272D35", + textMuted: "#495464", + surface: "#F9FAFA", + divider: "#D7DDE5", + link: "#2F578A", codeBg: "#EEF3F8", - codeText: "#2A5278", + codeText: "#2F578A", bg: "#FFFFFF", - }, - slate: { - id: "slate", - accent: "#5A6B73", - accentSoft: "#EEF1F2", - accentBorder: "#C5CED2", - text: "#2F373B", - textStrong: "#1A2023", - textMuted: "#66747B", - surface: "#F7F8F9", - divider: "#DEE3E6", - link: "#3E4C53", - codeBg: "#F0F2F3", - codeText: "#3E4C53", + }), + slate: palette("slate", { + accent: "#89798B", + accentSoft: "#F5F4F6", + accentBorder: "#D4CDD5", + accentGrid: "rgba(137,121,139,0.14)", + text: "#5C505E", + textStrong: "#312A32", + textMuted: "#5C505E", + surface: "#FAFAFA", + divider: "#E0DBE1", + link: "#6B5C6D", + codeBg: "#F5F4F6", + codeText: "#6B5C6D", bg: "#FFFFFF", - }, - plum: { - id: "plum", + }), + plum: palette("plum", { accent: "#7A4E6E", accentSoft: "#F5EEF3", accentBorder: "#D4BCCB", + accentGrid: "rgba(122,78,110,0.14)", text: "#3A2E36", textStrong: "#241820", textMuted: "#746068", @@ -174,22 +192,97 @@ export const PUBLISH_PALETTES: Record = { codeBg: "#F4ECF1", codeText: "#5E3A54", bg: "#FFFFFF", - }, - forest: { - id: "forest", - accent: "#4A7C4E", - accentSoft: "#EAF3EB", - accentBorder: "#BDD4BF", - text: "#2E3A2F", - textStrong: "#172017", - textMuted: "#5E6E5F", - surface: "#F5F9F5", - divider: "#D6E4D7", - link: "#37633B", - codeBg: "#EEF4EE", - codeText: "#37633B", + }), + forest: palette("forest", { + accent: "#4D9D7F", + accentSoft: "#F3F6F5", + accentBorder: "#CAD8D3", + accentGrid: "rgba(77,157,127,0.14)", + text: "#4D6159", + textStrong: "#29332F", + textMuted: "#4D6159", + surface: "#FAFAFA", + divider: "#D9E3DF", + link: "#3B7A62", + codeBg: "#F3F6F5", + codeText: "#3B7A62", bg: "#FFFFFF", - }, + }), + azure: palette("azure", { + accent: "#2F80ED", + accentSoft: "#EEF6FF", + accentBorder: "#B9D7FF", + accentGrid: "rgba(47,128,237,0.14)", + text: "#374151", + textStrong: "#111827", + textMuted: "#4B5563", + surface: "#F5F8FC", + divider: "#D7E8FF", + link: "#1D64C7", + codeBg: "#EEF6FF", + codeText: "#1D64C7", + bg: "#FFFFFF", + }), + amber: palette("amber", { + accent: "#E3A321", + accentSoft: "#FFF8E8", + accentBorder: "#F4D58B", + accentGrid: "rgba(227,163,33,0.14)", + text: "#555555", + textStrong: "#1A1A1A", + textMuted: "#555555", + surface: "#FFFEF8", + divider: "#F7E3B4", + link: "#B07C12", + codeBg: "#FFF8E8", + codeText: "#B07C12", + bg: "#FFFFFF", + }), + teal: palette("teal", { + accent: "#22B8A7", + accentSoft: "#ECFBF8", + accentBorder: "#A8E5DD", + accentGrid: "rgba(34,184,167,0.14)", + text: "#4D4F46", + textStrong: "#23251D", + textMuted: "#4D4F46", + surface: "#EEEFE9", + divider: "#CDEFEA", + link: "#1A8F82", + codeBg: "#ECFBF8", + codeText: "#1A8F82", + bg: "#FFFFFF", + }), + violet: palette("violet", { + accent: "#7567F8", + accentSoft: "#F3F1FF", + accentBorder: "#CFC9FF", + accentGrid: "rgba(117,103,248,0.14)", + text: "#3F3F46", + textStrong: "#18181B", + textMuted: "#3F3F46", + surface: "#F4F4F5", + divider: "#E1DEFF", + link: "#5B4EE0", + codeBg: "#F3F1FF", + codeText: "#5B4EE0", + bg: "#FFFFFF", + }), + mist: palette("mist", { + accent: "#35A7B8", + accentSoft: "#EEF9FA", + accentBorder: "#B7E3E8", + accentGrid: "rgba(53,167,184,0.14)", + text: "#525252", + textStrong: "#26352D", + textMuted: "#526359", + surface: "#F7FAFA", + divider: "#D4EEF1", + link: "#2A8492", + codeBg: "#EEF9FA", + codeText: "#2A8492", + bg: "#FFFFFF", + }), }; export const isPublishLayoutId = (value: string | null | undefined): value is PublishLayoutId => @@ -246,6 +339,144 @@ export const writePublishPalettePreference = (palette: PublishPaletteId) => { export const formatChapterIndex = (index: number) => String(index + 1).padStart(2, "0"); export type PublishHeadingKind = "title" | "chapter" | "small" | "none"; +export type PublishChapterStyle = + | "none" + | "aside-serif" + | "aside-part" + | "badge" + | "badge-section" + | "stacked-section" + | "kicker" + | "issue"; +export type PublishQuoteStyle = + | "grid-card" + | "rule" + | "panel" + | "marks" + | "rule-thick" + | "banner" + | "labeled" + | "italic-center" + | "italic-rule"; +export type PublishSmallHeadingStyle = "bookmark" | "slash" | "bar" | "dash" | "plain"; +export type PublishTitleRule = "none" | "short" | "brush"; + +export type PublishLayoutChrome = { + kicker: string | null; + h1Align: "left" | "center"; + h1Rule: PublishTitleRule; + h1Vol: boolean; + chapter: PublishChapterStyle; + small: PublishSmallHeadingStyle; + quote: PublishQuoteStyle; + quoteBanner?: string; + strongAccent: boolean; +}; + +export const PUBLISH_LAYOUT_CHROME: Record = { + letter: { + kicker: null, + h1Align: "center", + h1Rule: "brush", + h1Vol: false, + chapter: "aside-serif", + small: "bookmark", + quote: "grid-card", + strongAccent: true, + }, + guide: { + kicker: "GUIDE · ARTICLE", + h1Align: "left", + h1Rule: "short", + h1Vol: false, + chapter: "aside-part", + small: "plain", + quote: "rule", + strongAccent: true, + }, + blueprint: { + kicker: null, + h1Align: "left", + h1Rule: "short", + h1Vol: true, + chapter: "none", + small: "bookmark", + quote: "panel", + strongAccent: true, + }, + journal: { + kicker: null, + h1Align: "left", + h1Rule: "short", + h1Vol: true, + chapter: "issue", + small: "dash", + quote: "italic-rule", + strongAccent: true, + }, + stance: { + kicker: "ARTICLE", + h1Align: "left", + h1Rule: "none", + h1Vol: false, + chapter: "badge-section", + small: "bar", + quote: "marks", + strongAccent: true, + }, + stub: { + kicker: "EDITION · FIELD NOTES", + h1Align: "left", + h1Rule: "short", + h1Vol: false, + chapter: "badge", + small: "bar", + quote: "rule-thick", + strongAccent: false, + }, + brief: { + kicker: "EDITION · FIELD NOTES", + h1Align: "left", + h1Rule: "none", + h1Vol: false, + chapter: "aside-part", + small: "plain", + quote: "banner", + quoteBanner: "EDITOR'S NOTE", + strongAccent: true, + }, + outline: { + kicker: "MINIMAL · ARTICLE", + h1Align: "left", + h1Rule: "none", + h1Vol: false, + chapter: "stacked-section", + small: "plain", + quote: "labeled", + quoteBanner: "QUOTE", + strongAccent: false, + }, + zen: { + kicker: "FIELD NOTES", + h1Align: "left", + h1Rule: "short", + h1Vol: false, + chapter: "kicker", + small: "bar", + quote: "italic-center", + strongAccent: false, + }, + grove: { + kicker: null, + h1Align: "left", + h1Rule: "none", + h1Vol: false, + chapter: "none", + small: "slash", + quote: "marks", + strongAccent: true, + }, +}; export const planHeadingDecoration = ( tag: string, @@ -254,7 +485,7 @@ export const planHeadingDecoration = ( ): { kind: PublishHeadingKind; chapterLabel?: string } => { if (tag === "h1") return { kind: "title" }; if (tag === "h3") return { kind: "small" }; - if (tag === "h2" && (layout === "letter" || layout === "blueprint" || layout === "stub" || layout === "outline")) { + if (tag === "h2" && PUBLISH_LAYOUT_CHROME[layout].chapter !== "none") { return { kind: "chapter", chapterLabel: formatChapterIndex(chapterIndex) }; } return { kind: "none" }; @@ -266,42 +497,144 @@ export type PublishRhythm = { fontSize: string; lineHeight: string; paragraphSpacing: string; + letterSpacing: string; + headingLetterSpacing: string; headingAlign: "left" | "center"; }; export const PHONE_ARTICLE_RHYTHM = { - fontSize: "15px", - lineHeight: "1.9", - paragraphSpacing: "16px", h1Size: "24px", h2Size: "20px", h3Size: "15px", } as const; -const resolvePublishRhythm = (layout: PublishLayoutId, surface: PublishSurface): PublishRhythm => { - const desktop = PUBLISH_LAYOUT_RHYTHM[layout]; - if (surface !== "phone") return desktop; - return { - fontSize: PHONE_ARTICLE_RHYTHM.fontSize, - lineHeight: PHONE_ARTICLE_RHYTHM.lineHeight, - paragraphSpacing: PHONE_ARTICLE_RHYTHM.paragraphSpacing, - headingAlign: desktop.headingAlign, - }; -}; +const resolvePublishRhythm = (layout: PublishLayoutId, _surface: PublishSurface): PublishRhythm => + PUBLISH_LAYOUT_RHYTHM[layout]; export const PUBLISH_LAYOUT_RHYTHM: Record = { - letter: { fontSize: "15px", lineHeight: "1.9", paragraphSpacing: "24px", headingAlign: "center" }, - guide: { fontSize: "15px", lineHeight: "1.8", paragraphSpacing: "16px", headingAlign: "left" }, - blueprint: { fontSize: "16px", lineHeight: "1.8", paragraphSpacing: "20px", headingAlign: "left" }, - journal: { fontSize: "16px", lineHeight: "1.9", paragraphSpacing: "26px", headingAlign: "left" }, - stance: { fontSize: "16px", lineHeight: "1.8", paragraphSpacing: "20px", headingAlign: "left" }, - stub: { fontSize: "15px", lineHeight: "1.9", paragraphSpacing: "32px", headingAlign: "left" }, - brief: { fontSize: "15px", lineHeight: "1.9", paragraphSpacing: "24px", headingAlign: "left" }, - outline: { fontSize: "16px", lineHeight: "1.8", paragraphSpacing: "22px", headingAlign: "left" }, - zen: { fontSize: "16px", lineHeight: "2", paragraphSpacing: "32px", headingAlign: "left" }, - grove: { fontSize: "15px", lineHeight: "1.9", paragraphSpacing: "24px", headingAlign: "left" }, + letter: { + fontSize: "15px", + lineHeight: "2", + paragraphSpacing: "28px", + letterSpacing: "0.06em", + headingLetterSpacing: "0.08em", + headingAlign: "center", + }, + guide: { + fontSize: "15px", + lineHeight: "1.72", + paragraphSpacing: "14px", + letterSpacing: "0.01em", + headingLetterSpacing: "0.02em", + headingAlign: "left", + }, + blueprint: { + fontSize: "16px", + lineHeight: "1.78", + paragraphSpacing: "18px", + letterSpacing: "0.02em", + headingLetterSpacing: "0.04em", + headingAlign: "left", + }, + journal: { + fontSize: "16px", + lineHeight: "1.95", + paragraphSpacing: "28px", + letterSpacing: "0.04em", + headingLetterSpacing: "0.06em", + headingAlign: "left", + }, + stance: { + fontSize: "16px", + lineHeight: "1.68", + paragraphSpacing: "16px", + letterSpacing: "0em", + headingLetterSpacing: "0.01em", + headingAlign: "left", + }, + stub: { + fontSize: "15px", + lineHeight: "1.88", + paragraphSpacing: "30px", + letterSpacing: "0.08em", + headingLetterSpacing: "0.14em", + headingAlign: "left", + }, + brief: { + fontSize: "15px", + lineHeight: "1.82", + paragraphSpacing: "22px", + letterSpacing: "0.03em", + headingLetterSpacing: "0.05em", + headingAlign: "left", + }, + outline: { + fontSize: "16px", + lineHeight: "1.76", + paragraphSpacing: "20px", + letterSpacing: "0.05em", + headingLetterSpacing: "0.1em", + headingAlign: "left", + }, + zen: { + fontSize: "16px", + lineHeight: "2.2", + paragraphSpacing: "36px", + letterSpacing: "0.1em", + headingLetterSpacing: "0.12em", + headingAlign: "left", + }, + grove: { + fontSize: "15px", + lineHeight: "1.98", + paragraphSpacing: "26px", + letterSpacing: "0.045em", + headingLetterSpacing: "0.06em", + headingAlign: "left", + }, }; +const hexToRgbChannels = (hex: string) => { + const value = hex.replace("#", ""); + if (!/^[0-9a-fA-F]{6}$/.test(value)) return "22 160 110"; + return [0, 2, 4].map((offset) => Number.parseInt(value.slice(offset, offset + 2), 16)).join(" "); +}; + +export const publishColorCssVars = ( + palette: PublishPalette, + host: "editor" | "article" = "editor", +): Record => ({ + "--publish-accent": palette.accent, + "--publish-accent-soft": palette.accentSoft, + "--publish-accent-border": palette.accentBorder, + "--publish-accent-grid": palette.accentGrid, + "--publish-text": palette.text, + "--publish-text-strong": palette.textStrong, + "--publish-text-muted": palette.textMuted, + "--publish-surface": palette.surface, + "--publish-divider": palette.divider, + "--publish-link": palette.link, + "--publish-code-bg": palette.codeBg, + "--publish-code-text": palette.codeText, + "--publish-bg": palette.bg, + "--brand-green-rgb": hexToRgbChannels(palette.accent), + "--brand-green-hover-rgb": hexToRgbChannels(palette.link), + "--brand-green-200-rgb": hexToRgbChannels(palette.accentBorder), + "--brand-green-300-rgb": hexToRgbChannels(palette.accent), + "--brand-green-600-rgb": hexToRgbChannels(palette.link), + "--memo-content-divider-color": palette.accentBorder, + ...(host === "article" + ? { + "--brand-green": palette.accent, + "--brand-green-hover": palette.link, + "--brand-green-muted": palette.accentSoft, + "--brand-green-soft": palette.surface, + "--brand-green-border": palette.accentBorder, + "--brand-green-text": palette.link, + } + : {}), +}); + export const publishEditorCssVars = ( layout: PublishLayoutId, paletteId: PublishPaletteId, @@ -313,18 +646,10 @@ export const publishEditorCssVars = ( "--editor-body-font-size": rhythm.fontSize, "--editor-body-line-height": rhythm.lineHeight, "--editor-paragraph-spacing": rhythm.paragraphSpacing, - "--publish-accent": palette.accent, - "--publish-accent-soft": palette.accentSoft, - "--publish-accent-border": palette.accentBorder, - "--publish-text": palette.text, - "--publish-text-strong": palette.textStrong, - "--publish-text-muted": palette.textMuted, - "--publish-surface": palette.surface, - "--publish-divider": palette.divider, - "--publish-link": palette.link, - "--publish-code-bg": palette.codeBg, - "--publish-code-text": palette.codeText, - "--publish-bg": palette.bg, + "--editor-letter-spacing": rhythm.letterSpacing, + "--editor-heading-letter-spacing": rhythm.headingLetterSpacing, + ...publishColorCssVars(palette), + ...publishOrnamentCssVars(), }; }; @@ -336,6 +661,102 @@ const css = (declarations: Record) => export type PublishTagStyles = Record; +const quoteStyleRules = ( + layout: PublishLayoutId, + palette: PublishPalette, + phone: boolean, +): Record => { + const chrome = PUBLISH_LAYOUT_CHROME[layout]; + const bottom = phone ? "14px" : "28px"; + const pad = phone ? "10px 12px" : "16px 18px"; + switch (chrome.quote) { + case "grid-card": + return { + margin: `0 0 ${bottom}`, + padding: phone ? "14px 14px 16px 18px" : "18px 16px 20px 22px", + border: `1px solid ${palette.accentBorder}`, + "border-radius": "18px", + "background-color": palette.surface, + "background-image": `linear-gradient(to right, ${palette.accentGrid} 1px, transparent 1px), linear-gradient(to bottom, ${palette.accentGrid} 1px, transparent 1px)`, + "background-size": "30px 30px", + color: palette.textMuted, + }; + case "panel": + return { + margin: `0 0 ${bottom}`, + padding: pad, + border: `1px solid ${palette.accentBorder}`, + "border-radius": "10px", + background: palette.surface, + color: palette.textMuted, + }; + case "marks": + return { + margin: `0 0 ${bottom}`, + padding: phone ? "12px 14px 14px" : "16px 18px 18px", + border: `1px solid ${palette.accentBorder}`, + "border-radius": "12px", + background: palette.accentSoft, + color: palette.textMuted, + }; + case "rule-thick": + return { + margin: `0 0 ${bottom}`, + padding: phone ? "8px 0 8px 14px" : "6px 0 6px 16px", + "border-left": `4px solid ${palette.accent}`, + background: "transparent", + color: palette.textMuted, + }; + case "banner": + return { + margin: "0", + padding: phone ? "12px 14px 14px" : "14px 16px 16px", + border: "0", + background: palette.accentSoft, + color: palette.textMuted, + }; + case "labeled": + return { + margin: `0 0 ${bottom}`, + padding: "0", + border: "0", + background: "transparent", + color: palette.textStrong, + "text-align": "center", + "font-weight": "600", + }; + case "italic-center": + return { + margin: `0 0 ${bottom}`, + padding: phone ? "4px 8px" : "4px 24px", + border: "0", + background: "transparent", + color: palette.textMuted, + "font-style": "italic", + "text-align": "center", + }; + case "italic-rule": + return { + margin: `0 0 ${bottom}`, + padding: phone ? "12px 4px" : "18px 8px", + "border-top": `1px solid ${palette.accentBorder}`, + "border-bottom": `1px solid ${palette.accentBorder}`, + background: palette.surface, + color: palette.textMuted, + "font-style": "italic", + }; + case "rule": + default: + return { + margin: `0 0 ${bottom}`, + padding: phone ? "4px 0 4px 12px" : "4px 0 4px 16px", + "border-left": `3px solid ${palette.accent}`, + background: "transparent", + color: palette.textMuted, + }; + } +}; + export const buildPublishStyles = ( layout: PublishLayoutId, palette: PublishPalette, @@ -343,132 +764,30 @@ export const buildPublishStyles = ( ): PublishTagStyles => { const rhythm = resolvePublishRhythm(layout, surface); const phone = surface === "phone"; - const align = rhythm.headingAlign; - const h1AlignRules = - layout === "letter" - ? { - "text-align": "center", - "border-bottom": `3px solid ${palette.accent}`, - "padding-bottom": "10px", - } - : layout === "blueprint" - ? { - "text-align": "left", - "border-top": `3px solid ${palette.accent}`, - "padding-top": "10px", - } - : layout === "stance" - ? { - "text-align": "left", - "border-bottom": `6px solid ${palette.accent}`, - "padding-bottom": "10px", - } - : layout === "stub" - ? { - "text-align": "left", - "letter-spacing": "0.12em", - "border-bottom": `1px dashed ${palette.accent}`, - "padding-bottom": "10px", - } - : layout === "outline" - ? { - "text-align": "left", - "letter-spacing": "0.06em", - "border-bottom": `1px solid ${palette.divider}`, - "padding-bottom": "10px", - } - : layout === "zen" - ? { "text-align": "left", "font-weight": "600" } - : { "text-align": align }; - + const chrome = PUBLISH_LAYOUT_CHROME[layout]; + const h1Weight = layout === "letter" || layout === "guide" || layout === "stance" ? "800" : "700"; const h2Rules = - layout === "guide" || layout === "blueprint" || layout === "stance" - ? { - "border-left": layout === "stance" ? `6px solid ${palette.accent}` : `4px solid ${palette.accent}`, - "padding-left": "12px", - } - : layout === "journal" || layout === "grove" - ? { - "border-bottom": `1px solid ${palette.divider}`, - "padding-bottom": "8px", - } - : layout === "stub" - ? { - "border-bottom": `1px dashed ${palette.accentBorder}`, - "padding-bottom": "8px", - } - : {}; - + layout === "blueprint" + ? { "border-left": `3px solid ${palette.accent}`, "padding-left": "12px" } + : layout === "grove" + ? { "display": "inline-block", "border-bottom": `2px solid ${palette.accent}`, "padding-bottom": "6px" } + : {}; const h3Rules = - layout === "letter" - ? { - "text-align": "center", - "border-bottom": `2px solid ${palette.accentBorder}`, - "padding-bottom": "8px", - } - : layout === "blueprint" || layout === "stance" - ? { - "border-bottom": `2px solid ${palette.accent}`, - "padding-bottom": "6px", - } - : layout === "stub" - ? { - "border-bottom": `1px dashed ${palette.accentBorder}`, - "padding-bottom": "6px", - } + chrome.small === "bookmark" + ? { "text-align": "center" } + : chrome.small === "bar" + ? { "border-left": `3px solid ${palette.accent}`, "padding-left": "10px" } + : chrome.small === "slash" + ? { "text-align": "center", color: palette.accent } : {}; - - const quoteRules = - layout === "letter" || layout === "grove" - ? { - margin: "0 0 28px", - padding: "16px 18px", - border: `1px solid ${palette.accentBorder}`, - "border-radius": layout === "grove" ? "16px" : "12px", - background: palette.surface, - color: palette.textMuted, - } - : layout === "journal" || layout === "zen" - ? { - margin: "0 0 28px", - padding: layout === "zen" ? "8px 0 8px 0" : "4px 0 4px 16px", - "border-left": layout === "zen" ? "0" : `2px solid ${palette.divider}`, - background: "transparent", - color: palette.textMuted, - "font-style": "italic", - } - : layout === "stub" - ? { - margin: "0 0 28px", - padding: "14px 16px", - border: `1px dashed ${palette.accent}`, - "border-radius": "2px", - background: palette.surface, - color: palette.textMuted, - } - : layout === "outline" - ? { - margin: "0 0 24px", - padding: "12px 16px", - border: `1px solid ${palette.divider}`, - background: "transparent", - color: palette.textMuted, - } - : { - margin: "0 0 24px", - padding: "12px 16px", - "border-left": layout === "stance" ? `6px solid ${palette.accent}` : `4px solid ${palette.accent}`, - background: palette.accentSoft, - color: palette.textMuted, - }; - - const quote = phone ? { ...quoteRules, margin: "0 0 14px", padding: "10px 12px" } : quoteRules; + const quote = quoteStyleRules(layout, palette, phone); return { root: css({ "font-family": "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif", "font-size": rhythm.fontSize, "line-height": rhythm.lineHeight, + "letter-spacing": rhythm.letterSpacing, color: palette.text, "background-color": palette.bg, "word-break": "break-word", @@ -477,26 +796,28 @@ export const buildPublishStyles = ( margin: `0 0 ${rhythm.paragraphSpacing}`, padding: "0", "line-height": rhythm.lineHeight, + "letter-spacing": rhythm.letterSpacing, "font-size": rhythm.fontSize, color: palette.text, }), h1: css({ - ...h1AlignRules, - margin: phone ? "0 0 12px" : "0 0 24px", + "text-align": chrome.h1Align, + margin: phone ? "0 0 12px" : "0 0 18px", "font-size": phone ? PHONE_ARTICLE_RHYTHM.h1Size : layout === "journal" || layout === "zen" ? "26px" : "24px", "line-height": phone ? "1.4" : "1.35", - "font-weight": "800", - ...(phone ? { "letter-spacing": "0.02em" } : {}), + "letter-spacing": rhythm.headingLetterSpacing, + "font-weight": h1Weight, color: palette.textStrong, }), h2: css({ margin: phone ? "18px 0 10px" : "28px 0 16px", "font-size": phone ? PHONE_ARTICLE_RHYTHM.h2Size : "20px", "line-height": "1.35", + "letter-spacing": rhythm.headingLetterSpacing, "font-weight": phone ? "600" : "700", color: palette.textStrong, "text-align": "left", @@ -506,29 +827,34 @@ export const buildPublishStyles = ( margin: phone ? "14px 0 8px" : "24px 0 12px", "font-size": phone ? PHONE_ARTICLE_RHYTHM.h3Size : layout === "letter" ? "15px" : "16px", "line-height": "1.45", + "letter-spacing": rhythm.headingLetterSpacing, "font-weight": "700", - color: palette.textStrong, + color: chrome.small === "slash" ? palette.accent : palette.textStrong, ...h3Rules, }), blockquote: css({ "line-height": rhythm.lineHeight, + "letter-spacing": rhythm.letterSpacing, ...quote, }), ul: css({ margin: "0 0 1em", "padding-left": "1.6em", "line-height": rhythm.lineHeight, + "letter-spacing": rhythm.letterSpacing, color: palette.text, }), ol: css({ margin: "0 0 1em", "padding-left": "1.6em", + "letter-spacing": rhythm.letterSpacing, "line-height": rhythm.lineHeight, color: palette.text, }), li: css({ margin: "0.25em 0", "line-height": rhythm.lineHeight, + "letter-spacing": rhythm.letterSpacing, color: palette.text, }), a: css({ @@ -537,7 +863,7 @@ export const buildPublishStyles = ( }), strong: css({ "font-weight": "700", - color: palette.accent, + color: chrome.strongAccent ? palette.accent : palette.textStrong, }), em: css({ "font-style": "italic", @@ -649,24 +975,512 @@ const themeBlockStyles = ( const isChromeElement = (element: HTMLElement) => element.getAttribute("data-ee-publish-chrome") === "true"; -const wrapHeading = (heading: HTMLElement) => { - if (heading.parentElement?.getAttribute("data-ee-publish-heading")) { - return heading.parentElement; - } - const document = heading.ownerDocument; - const wrapper = document.createElement("section"); - wrapper.setAttribute("data-ee-publish-heading", heading.tagName.toLowerCase()); - heading.parentNode?.insertBefore(wrapper, heading); - wrapper.appendChild(heading); +const SERIF_STACK = "Georgia, 'Times New Roman', serif"; + +const chromeNode = (doc: Document, tag: string, text: string, style: string) => { + const node = doc.createElement(tag); + node.setAttribute("data-ee-publish-chrome", "true"); + node.textContent = text; + node.style.cssText = style; + return node; +}; + +const chromeOrnament = (doc: Document, id: PublishOrnamentId, color: string, style: string) => { + const image = doc.createElement("img"); + image.setAttribute("data-ee-publish-chrome", "true"); + image.setAttribute("data-ee-publish-ornament", id); + image.alt = ""; + image.src = publishOrnamentDataUri(id, color); + image.style.cssText = style; + return image; +}; + +const wrapBlock = (element: HTMLElement, attr: string, value: string) => { + if (element.parentElement?.getAttribute(attr) === value) return element.parentElement; + const wrapper = element.ownerDocument.createElement("section"); + wrapper.setAttribute(attr, value); + element.parentNode?.insertBefore(wrapper, element); + wrapper.appendChild(element); return wrapper; }; +const wrapHeading = (heading: HTMLElement) => wrapBlock(heading, "data-ee-publish-heading", heading.tagName.toLowerCase()); + +const appendTitleRule = ( + wrapper: HTMLElement, + palette: PublishPalette, + phone: boolean, + kind: PublishTitleRule, + align: "left" | "center", + ornament?: PublishOrnamentId, +) => { + if (kind === "none") return; + if (ornament) { + wrapper.appendChild( + chromeOrnament( + wrapper.ownerDocument, + ornament, + palette.accent, + css({ + display: "block", + width: phone ? "112px" : "148px", + height: "auto", + margin: align === "center" ? "8px auto 0" : "8px 0 0", + padding: "0", + border: "0", + }), + ), + ); + return; + } + const bar = chromeNode( + wrapper.ownerDocument, + "p", + "", + css({ + width: kind === "brush" ? (align === "center" ? "42%" : "72px") : "42px", + height: "3px", + margin: align === "center" ? "8px auto 0" : "8px 0 0", + padding: "0", + border: "0", + background: palette.accent, + "font-size": "0", + "line-height": "0", + }), + ); + wrapper.appendChild(bar); + if (phone) bar.style.marginTop = "6px"; +}; + +const decorateTitle = ( + heading: HTMLElement, + chrome: PublishLayoutChrome, + palette: PublishPalette, + phone: boolean, + layout: PublishLayoutId, +) => { + const ornaments = PUBLISH_LAYOUT_ORNAMENTS[layout]; + const wrapper = wrapHeading(heading); + wrapper.style.cssText = css({ + margin: phone ? "0 0 16px" : "0 0 24px", + padding: "0", + }); + heading.style.margin = "0"; + if (chrome.kicker) { + wrapper.insertBefore( + chromeNode( + heading.ownerDocument, + "p", + chrome.kicker, + css({ + margin: "0 0 8px", + padding: "0", + color: palette.accent, + "font-size": "11px", + "font-weight": "600", + "letter-spacing": "0.16em", + "line-height": "1.4", + }), + ), + heading, + ); + } + if (ornaments?.titleEnd) { + const table = heading.ownerDocument.createElement("table"); + table.style.cssText = css({ + width: "auto", + "max-width": "100%", + "border-collapse": "collapse", + margin: chrome.h1Align === "center" ? "0 auto" : "0", + padding: "0", + border: "0", + }); + const row = heading.ownerDocument.createElement("tr"); + const titleCell = heading.ownerDocument.createElement("td"); + titleCell.style.cssText = "padding:0;border:0;vertical-align:middle;"; + titleCell.appendChild(heading); + const markCell = heading.ownerDocument.createElement("td"); + markCell.setAttribute("data-ee-publish-chrome", "true"); + markCell.style.cssText = "padding:0 0 0 8px;border:0;vertical-align:middle;width:1%;"; + markCell.appendChild( + chromeOrnament( + heading.ownerDocument, + ornaments.titleEnd, + palette.accent, + css({ + display: "block", + width: phone ? "22px" : "28px", + height: "auto", + margin: "0", + padding: "0", + border: "0", + }), + ), + ); + row.appendChild(titleCell); + row.appendChild(markCell); + table.appendChild(row); + wrapper.appendChild(table); + } + appendTitleRule(wrapper, palette, phone, chrome.h1Rule, chrome.h1Align, ornaments?.titleRule); + if (chrome.h1Vol) { + wrapper.appendChild( + chromeNode( + heading.ownerDocument, + "p", + "VOL. 01", + css({ + margin: "6px 0 0", + padding: "0", + color: palette.textMuted, + "font-size": "11px", + "font-weight": "600", + "letter-spacing": "0.14em", + }), + ), + ); + } +}; + +const asideNumberTable = ( + heading: HTMLElement, + number: string, + palette: PublishPalette, + phone: boolean, + options: { + serif?: boolean; + part?: boolean; + badge?: boolean; + mark?: PublishOrnamentId; + end?: PublishOrnamentId; + }, +) => { + const wrapper = wrapHeading(heading); + wrapper.style.cssText = css({ + margin: phone ? "18px 0 12px" : "28px 0 16px", + padding: "0", + }); + const doc = heading.ownerDocument; + const table = doc.createElement("table"); + table.style.cssText = "width:100%;border-collapse:collapse;margin:0;padding:0;border:0;"; + const row = doc.createElement("tr"); + const numberCell = doc.createElement("td"); + numberCell.setAttribute("data-ee-publish-chrome", "true"); + numberCell.style.cssText = css({ + width: options.badge ? "1%" : "1%", + padding: options.badge ? "0 10px 0 0" : "0 12px 0 0", + "vertical-align": "middle", + "white-space": "nowrap", + border: "0", + }); + const numberLabel = chromeNode( + doc, + "p", + number, + options.badge + ? css({ + margin: "0", + padding: phone ? "5px 8px" : "6px 8px", + color: "#ffffff", + background: palette.accent, + "border-radius": "6px", + "font-size": phone ? "13px" : "14px", + "font-weight": "700", + "line-height": "1", + "text-align": "center", + "white-space": "nowrap", + }) + : css({ + margin: "0", + padding: "0", + color: palette.accent, + "font-family": options.serif ? SERIF_STACK : "inherit", + "font-size": phone ? "28px" : options.serif ? "42px" : "34px", + "font-style": options.serif ? "italic" : "normal", + "font-weight": "700", + "line-height": "1", + "white-space": "nowrap", + }), + ); + numberCell.appendChild(numberLabel); + if (options.part) { + numberCell.appendChild( + chromeNode( + doc, + "p", + "PART", + css({ + margin: "4px 0 0", + padding: "0", + color: palette.accent, + "font-size": "10px", + "font-weight": "700", + "letter-spacing": "0.16em", + "line-height": "1", + }), + ), + ); + } + const titleCell = doc.createElement("td"); + titleCell.style.cssText = "vertical-align:middle;padding:0;border:0;"; + heading.style.margin = "0"; + heading.style.padding = "0"; + titleCell.appendChild(heading); + row.appendChild(numberCell); + if (options.mark) { + const markCell = doc.createElement("td"); + markCell.setAttribute("data-ee-publish-chrome", "true"); + markCell.style.cssText = "vertical-align:middle;padding:0 8px 0 0;border:0;width:1%;"; + markCell.appendChild( + chromeOrnament( + doc, + options.mark, + palette.accent, + css({ + display: "block", + width: phone ? "18px" : "22px", + height: "auto", + margin: "0", + padding: "0", + border: "0", + }), + ), + ); + row.appendChild(markCell); + } + row.appendChild(titleCell); + if (options.end) { + const endCell = doc.createElement("td"); + endCell.setAttribute("data-ee-publish-chrome", "true"); + endCell.style.cssText = "vertical-align:middle;padding:0 0 0 8px;border:0;width:1%;"; + endCell.appendChild( + chromeOrnament( + doc, + options.end, + palette.accent, + css({ + display: "block", + width: phone ? "22px" : "26px", + height: "auto", + margin: "0", + padding: "0", + border: "0", + }), + ), + ); + row.appendChild(endCell); + } + table.appendChild(row); + wrapper.appendChild(table); + return wrapper; +}; + +const decorateChapter = ( + heading: HTMLElement, + chapterLabel: string, + chrome: PublishLayoutChrome, + palette: PublishPalette, + phone: boolean, + layout: PublishLayoutId, +) => { + const stacked = (lines: Array<{ text: string; style: string }>) => { + const wrapper = wrapHeading(heading); + wrapper.style.cssText = css({ + margin: phone ? "18px 0 12px" : "28px 0 16px", + padding: "0", + }); + for (const line of lines) { + wrapper.insertBefore(chromeNode(heading.ownerDocument, "p", line.text, line.style), heading); + } + heading.style.margin = "6px 0 0"; + return wrapper; + }; + + const ornaments = PUBLISH_LAYOUT_ORNAMENTS[layout]; + switch (chrome.chapter) { + case "aside-serif": + return asideNumberTable(heading, chapterLabel, palette, phone, { + serif: true, + mark: ornaments?.chapterMark, + end: ornaments?.chapterEnd, + }); + case "aside-part": + return asideNumberTable(heading, chapterLabel, palette, phone, { serif: true, part: true }); + case "badge": + return asideNumberTable(heading, chapterLabel, palette, phone, { badge: true }); + case "badge-section": { + const wrapper = asideNumberTable(heading, chapterLabel, palette, phone, { badge: true }); + const titleCell = wrapper.querySelector("td:last-child"); + if (titleCell && heading.parentElement === titleCell) { + titleCell.insertBefore( + chromeNode( + heading.ownerDocument, + "p", + `SECTION ${chapterLabel}`, + css({ + margin: "0 0 4px", + padding: "0", + color: palette.accent, + "font-size": "11px", + "font-weight": "700", + "letter-spacing": "0.14em", + }), + ), + heading, + ); + } + return wrapper; + } + case "stacked-section": + return stacked([ + { + text: chapterLabel, + style: css({ + margin: "0", + padding: "0", + color: palette.accent, + "font-family": SERIF_STACK, + "font-size": phone ? "36px" : "52px", + "font-style": "italic", + "font-weight": "700", + "line-height": "0.9", + "white-space": "nowrap", + }), + }, + { + text: `SECTION ${chapterLabel}`, + style: css({ + margin: "6px 0 0", + padding: "0", + color: palette.textMuted, + "font-size": "11px", + "font-weight": "700", + "letter-spacing": "0.16em", + }), + }, + ]); + case "kicker": + return stacked([ + { + text: `${chapterLabel} · CHAPTER ONE`, + style: css({ + margin: "0", + padding: "0", + color: palette.textMuted, + "font-size": "11px", + "font-weight": "600", + "letter-spacing": "0.16em", + }), + }, + ]); + case "issue": + return stacked([ + { + text: `NO. ${chapterLabel}`, + style: css({ + margin: "0", + padding: "0", + color: palette.textMuted, + "font-size": "11px", + "font-weight": "600", + "letter-spacing": "0.16em", + }), + }, + ]); + default: + return wrapHeading(heading); + } +}; + +const decorateSmallHeading = ( + heading: HTMLElement, + chrome: PublishLayoutChrome, + palette: PublishPalette, + phone: boolean, +) => { + const wrapper = wrapHeading(heading); + wrapper.style.cssText = css({ + margin: phone ? "14px 0 8px" : "24px 0 12px", + padding: "0", + }); + heading.style.margin = "0"; + if (chrome.small === "bookmark") { + heading.style.textAlign = "center"; + const rule = heading.ownerDocument.createElement("table"); + rule.setAttribute("data-ee-publish-chrome", "true"); + rule.style.cssText = "width:160px;margin:10px auto 0;border-collapse:collapse;border:0;"; + const row = heading.ownerDocument.createElement("tr"); + const accent = heading.ownerDocument.createElement("td"); + accent.setAttribute("data-ee-publish-chrome", "true"); + accent.style.cssText = `width:28px;height:0;padding:0;border-top:3px solid ${palette.accent};`; + const rest = heading.ownerDocument.createElement("td"); + rest.setAttribute("data-ee-publish-chrome", "true"); + rest.style.cssText = `height:0;padding:0;border-top:1px solid ${palette.accentBorder};`; + row.appendChild(accent); + row.appendChild(rest); + rule.appendChild(row); + wrapper.appendChild(rule); + } + if (chrome.small === "slash") { + wrapper.style.textAlign = "center"; + heading.style.textAlign = "center"; + heading.style.color = palette.accent; + wrapper.insertBefore( + chromeNode( + heading.ownerDocument, + "p", + `//`, + css({ + display: "inline-block", + margin: "0 8px 0 0", + padding: "0", + color: palette.accent, + "font-weight": "800", + }), + ), + heading, + ); + wrapper.appendChild( + chromeNode( + heading.ownerDocument, + "p", + `//`, + css({ + display: "inline-block", + margin: "0 0 0 8px", + padding: "0", + color: palette.accent, + "font-weight": "800", + }), + ), + ); + heading.style.display = "inline"; + } + if (chrome.small === "dash") { + wrapper.insertBefore( + chromeNode( + heading.ownerDocument, + "span", + "— ", + css({ + color: palette.textMuted, + "font-weight": "600", + }), + ), + heading, + ); + heading.style.display = "inline"; + } +}; + const decorateHeadings = ( root: HTMLElement, layout: PublishLayoutId, palette: PublishPalette, surface: PublishSurface = "desktop", ) => { + const chrome = PUBLISH_LAYOUT_CHROME[layout]; + const phone = surface === "phone"; const headings = Array.from(root.querySelectorAll("h1, h2, h3")).filter((heading) => { if (heading.closest("table, pre, [data-edgeever-theme-block]")) return false; if (heading.closest("[data-ee-publish-heading]")) return false; @@ -678,28 +1492,182 @@ const decorateHeadings = ( const tag = heading.tagName.toLowerCase(); const plan = planHeadingDecoration(tag, layout, chapterIndex); if (plan.kind === "chapter") chapterIndex += 1; - if (plan.kind !== "chapter" || !plan.chapterLabel) continue; - - const wrapper = wrapHeading(heading); - wrapper.style.cssText = "margin: 0; padding: 0;"; - const label = heading.ownerDocument.createElement("p"); - label.setAttribute("data-ee-publish-chrome", "true"); - label.textContent = plan.chapterLabel; - label.style.cssText = css({ - margin: surface === "phone" ? "16px 0 0" : "28px 0 0", - padding: "0", - color: palette.accent, - "font-size": surface === "phone" ? "15px" : layout === "letter" ? "28px" : "13px", - "line-height": "1.1", - "font-weight": "700", - "font-style": layout === "letter" ? "italic" : "normal", - "letter-spacing": layout === "blueprint" ? "0.08em" : "0", - }); - wrapper.insertBefore(label, heading); - heading.style.margin = surface === "phone" ? "4px 0 10px" : "6px 0 16px"; + if (plan.kind === "title") { + decorateTitle(heading, chrome, palette, phone, layout); + continue; + } + if (plan.kind === "chapter" && plan.chapterLabel) { + decorateChapter(heading, plan.chapterLabel, chrome, palette, phone, layout); + continue; + } + if (plan.kind === "small") { + decorateSmallHeading(heading, chrome, palette, phone); + } + const headingRule = PUBLISH_LAYOUT_ORNAMENTS[layout]?.headingRule; + if (tag === "h2" && headingRule) { + const wrapper = wrapHeading(heading); + wrapper.appendChild( + chromeOrnament( + heading.ownerDocument, + headingRule, + palette.accent, + css({ + display: "block", + width: phone ? "72px" : "96px", + height: "auto", + margin: "6px 0 0", + padding: "0", + border: "0", + }), + ), + ); + } } }; +const decorateQuotes = ( + root: HTMLElement, + layout: PublishLayoutId, + palette: PublishPalette, + surface: PublishSurface = "desktop", +) => { + const chrome = PUBLISH_LAYOUT_CHROME[layout]; + const phone = surface === "phone"; + root.querySelectorAll("blockquote").forEach((quote) => { + if (quote.closest("[data-ee-publish-quote], table, pre, [data-edgeever-theme-block]")) return; + const wrapper = wrapBlock(quote, "data-ee-publish-quote", chrome.quote); + wrapper.style.cssText = css({ + margin: phone ? "0 0 14px" : "0 0 28px", + padding: "0", + }); + quote.style.margin = "0"; + const ornaments = PUBLISH_LAYOUT_ORNAMENTS[layout]; + if (ornaments?.quoteTape) { + const tapeRow = chromeNode( + quote.ownerDocument, + "p", + "", + css({ + margin: phone ? "0 8px -4px 0" : "0 12px -6px 0", + padding: "0", + "text-align": "right", + "font-size": "0", + "line-height": "0", + }), + ); + tapeRow.appendChild( + chromeOrnament( + quote.ownerDocument, + ornaments.quoteTape, + palette.accent, + css({ + display: "inline-block", + width: phone ? "42px" : "54px", + height: "auto", + margin: "0", + padding: "0", + border: "0", + transform: "rotate(12deg)", + }), + ), + ); + wrapper.insertBefore(tapeRow, quote); + } + if (chrome.quote === "banner" && chrome.quoteBanner) { + wrapper.style.overflow = "hidden"; + wrapper.style.borderRadius = "8px"; + wrapper.insertBefore( + chromeNode( + quote.ownerDocument, + "p", + chrome.quoteBanner, + css({ + margin: "0", + padding: phone ? "8px 12px" : "9px 16px", + color: "#ffffff", + background: palette.accent, + "font-size": "11px", + "font-weight": "700", + "letter-spacing": "0.16em", + }), + ), + quote, + ); + } + if (chrome.quote === "labeled" && chrome.quoteBanner) { + wrapper.insertBefore( + chromeNode( + quote.ownerDocument, + "p", + chrome.quoteBanner, + css({ + margin: "0 0 10px", + padding: "0", + color: palette.textMuted, + "font-size": "11px", + "font-weight": "700", + "letter-spacing": "0.18em", + "text-align": "center", + }), + ), + quote, + ); + } + if (ornaments?.quoteMark) { + wrapper.insertBefore( + chromeOrnament( + quote.ownerDocument, + ornaments.quoteMark, + palette.accent, + css({ + display: "block", + width: phone ? "22px" : "28px", + height: "auto", + margin: phone ? "0 0 2px 12px" : "0 0 4px 16px", + padding: "0", + border: "0", + }), + ), + quote, + ); + } else if (chrome.quote === "marks" || chrome.quote === "grid-card") { + wrapper.insertBefore( + chromeNode( + quote.ownerDocument, + "p", + "“", + css({ + margin: "0 0 4px", + padding: "0", + color: palette.accent, + "font-family": SERIF_STACK, + "font-size": phone ? "28px" : "34px", + "font-weight": "700", + "line-height": "1", + }), + ), + quote, + ); + } + if (chrome.quote === "italic-center") { + wrapper.appendChild( + chromeNode( + quote.ownerDocument, + "p", + "—", + css({ + margin: "8px 0 0", + padding: "0", + color: palette.accent, + "text-align": "center", + "letter-spacing": "0.2em", + }), + ), + ); + } + }); +}; + const applyThemeBlocks = ( root: HTMLElement, layout: PublishLayoutId, @@ -740,7 +1708,7 @@ export const applyPublishLayout = ( ) => { const palette = PUBLISH_PALETTES[paletteId] ?? PUBLISH_PALETTES[DEFAULT_PUBLISH_PALETTE]; const styles = buildPublishStyles(layout, palette, surface); - root.style.cssText = styles.root; + root.style.cssText = `${styles.root}; ${css(publishColorCssVars(palette, "article"))}`; root.querySelectorAll("*").forEach((element) => { if (isChromeElement(element)) return; @@ -764,6 +1732,7 @@ export const applyPublishLayout = ( }); decorateHeadings(root, layout, palette, surface); + decorateQuotes(root, layout, palette, surface); applyThemeBlocks(root, layout, palette, surface); }; diff --git a/apps/web/src/lib/publish-ornaments.test.mjs b/apps/web/src/lib/publish-ornaments.test.mjs new file mode 100644 index 00000000..d95221cf --- /dev/null +++ b/apps/web/src/lib/publish-ornaments.test.mjs @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { + PUBLISH_LAYOUT_ORNAMENTS, + PUBLISH_ORNAMENT_IDS, + publishOrnamentCssVars, + publishOrnamentDataUri, + renderPublishOrnamentSvg, +} from "./publish-ornaments.ts"; + +describe("publish ornaments", () => { + test("renders original tintable stationery marks", () => { + expect(PUBLISH_ORNAMENT_IDS).toEqual([ + "blossom", + "sprig", + "sun", + "tape", + "quote", + "brush", + "swoosh", + "leaf", + ]); + const svg = renderPublishOrnamentSvg("blossom", "#D9A521"); + expect(svg).toContain("#D9A521"); + expect(svg).not.toContain("{{color}}"); + expect(svg).toContain(" { + expect(PUBLISH_LAYOUT_ORNAMENTS.letter).toEqual({ + titleEnd: "sprig", + titleRule: "brush", + chapterMark: "blossom", + chapterEnd: "sun", + quoteMark: "quote", + quoteTape: "tape", + }); + expect(PUBLISH_LAYOUT_ORNAMENTS.grove.headingRule).toBe("swoosh"); + expect(PUBLISH_LAYOUT_ORNAMENTS.stub.quoteTape).toBe("tape"); + }); + + test("exposes mask URLs for the writing canvas", () => { + const vars = publishOrnamentCssVars(); + expect(vars["--publish-ornament-brush"]).toContain("url(\"data:image/svg+xml"); + expect(vars["--publish-ornament-sun"]).toContain("url(\"data:image/svg+xml"); + }); +}); + +describe("ornament wiring", () => { + test("paper layout and WeChat copy inject and rasterize ornaments", () => { + const layout = readFileSync(new URL("./publish-layout.ts", import.meta.url), "utf8"); + const wechat = readFileSync(new URL("./wechat-copy.ts", import.meta.url), "utf8"); + const css = readFileSync(new URL("../styles/publish-layout.css", import.meta.url), "utf8"); + + expect(layout).toContain("chromeOrnament"); + expect(layout).toContain("publishOrnamentCssVars"); + expect(layout).toContain("data-ee-publish-ornament"); + expect(wechat).toContain("rasterizePublishOrnamentsForWeChat"); + expect(wechat).toContain("image/png"); + expect(css).toContain("--publish-ornament-brush"); + expect(css).toContain("--publish-ornament-tape"); + expect(css).toContain("--publish-ornament-sprig"); + }); +}); diff --git a/apps/web/src/lib/publish-ornaments.ts b/apps/web/src/lib/publish-ornaments.ts new file mode 100644 index 00000000..a0bdd7ca --- /dev/null +++ b/apps/web/src/lib/publish-ornaments.ts @@ -0,0 +1,115 @@ +export const PUBLISH_ORNAMENT_IDS = [ + "blossom", + "sprig", + "sun", + "tape", + "quote", + "brush", + "swoosh", + "leaf", +] as const; +export type PublishOrnamentId = (typeof PUBLISH_ORNAMENT_IDS)[number]; + +const COLOR_TOKEN = "{{color}}"; + +const ORNAMENT_SVG: Record = { + blossom: ` + + + + + + + + + `, + sprig: ` + + + + + + `, + sun: ` + + + + + + + + + + + + `, + tape: ` + + + `, + quote: ` + + `, + brush: ` + + `, + swoosh: ` + + `, + leaf: ` + + + `, +}; + +const compactSvg = (source: string) => source.replace(/\s+/g, " ").trim(); + +export const renderPublishOrnamentSvg = (id: PublishOrnamentId, color: string) => { + const hex = /^#([0-9a-f]{6})$/i.test(color) ? color : "#111111"; + return compactSvg(ORNAMENT_SVG[id].replaceAll(COLOR_TOKEN, hex)); +}; + +export const publishOrnamentDataUri = (id: PublishOrnamentId, color: string) => + `data:image/svg+xml;charset=utf-8,${encodeURIComponent(renderPublishOrnamentSvg(id, color))}`; + +export const publishOrnamentMaskUri = (id: PublishOrnamentId) => publishOrnamentDataUri(id, "#111111"); + +export type PublishOrnamentPlacement = { + titleEnd?: PublishOrnamentId; + titleRule?: PublishOrnamentId; + chapterMark?: PublishOrnamentId; + chapterEnd?: PublishOrnamentId; + quoteMark?: PublishOrnamentId; + quoteTape?: PublishOrnamentId; + headingRule?: PublishOrnamentId; +}; + +export const PUBLISH_LAYOUT_ORNAMENTS: Record = { + letter: { + titleEnd: "sprig", + titleRule: "brush", + chapterMark: "blossom", + chapterEnd: "sun", + quoteMark: "quote", + quoteTape: "tape", + }, + grove: { + headingRule: "swoosh", + quoteMark: "quote", + titleEnd: "leaf", + }, + stub: { + quoteTape: "tape", + }, +}; + +export const publishOrnamentCssVars = () => ({ + "--publish-ornament-blossom": `url("${publishOrnamentMaskUri("blossom")}")`, + "--publish-ornament-sprig": `url("${publishOrnamentMaskUri("sprig")}")`, + "--publish-ornament-sun": `url("${publishOrnamentMaskUri("sun")}")`, + "--publish-ornament-tape": `url("${publishOrnamentMaskUri("tape")}")`, + "--publish-ornament-quote": `url("${publishOrnamentMaskUri("quote")}")`, + "--publish-ornament-brush": `url("${publishOrnamentMaskUri("brush")}")`, + "--publish-ornament-swoosh": `url("${publishOrnamentMaskUri("swoosh")}")`, + "--publish-ornament-leaf": `url("${publishOrnamentMaskUri("leaf")}")`, +}); diff --git a/apps/web/src/lib/wechat-copy.ts b/apps/web/src/lib/wechat-copy.ts index 4129900e..5759090f 100644 --- a/apps/web/src/lib/wechat-copy.ts +++ b/apps/web/src/lib/wechat-copy.ts @@ -231,6 +231,48 @@ const canvasToJpeg = (canvas: HTMLCanvasElement, quality = 0.86) => ); }); +const svgToTransparentPng = async (svg: string) => { + const objectUrl = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" })); + try { + const image = new Image(); + image.src = objectUrl; + await image.decode(); + const canvas = document.createElement("canvas"); + canvas.width = Math.max(1, image.naturalWidth); + canvas.height = Math.max(1, image.naturalHeight); + const context = canvas.getContext("2d"); + if (!context) throw new Error("Could not create ornament canvas"); + context.drawImage(image, 0, 0); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((png) => { + if (png) { + resolve(png); + return; + } + reject(new Error("Could not convert ornament to PNG")); + }, "image/png"); + }); + return blobToDataUrl(blob); + } finally { + URL.revokeObjectURL(objectUrl); + } +}; + +const rasterizePublishOrnamentsForWeChat = async (root: HTMLElement) => { + const images = Array.from(root.querySelectorAll("img[data-ee-publish-ornament]")); + await Promise.all(images.map(async (image) => { + const source = image.getAttribute("src")?.trim() ?? ""; + if (!source.startsWith("data:image/svg+xml")) return; + try { + const encoded = source.replace(/^data:image\/svg\+xml(?:;charset=utf-8)?,/, ""); + const svg = decodeURIComponent(encoded); + image.setAttribute("src", await svgToTransparentPng(svg)); + } catch { + // Keep the SVG data URI in the live preview; WeChat copy can drop a broken ornament. + } + })); +}; + const svgToWeChatImage = async (svg: string) => { const markup = prepareSvgMarkup(svg); const size = getSvgSize(markup); @@ -445,6 +487,7 @@ export const buildWeChatClipboardHtml = async (editor: Editor) => { await embedMermaidForWeChat(container, editor); const originalImages = Array.from(editor.view.dom.querySelectorAll("img")).filter(isContentImage); await embedImagesForWeChat(container, originalImages); + await rasterizePublishOrnamentsForWeChat(container); return container.outerHTML; }; @@ -457,5 +500,6 @@ export const copyMarkdownToWeChat = async (markdown: string) => { ); await embedMermaidForWeChat(container); await embedImagesForWeChat(container); + await rasterizePublishOrnamentsForWeChat(container); await copyHtmlToClipboard(container.outerHTML, container.textContent ?? ""); }; diff --git a/apps/web/src/styles/editor-themes/index.css b/apps/web/src/styles/editor-themes/index.css index c791a634..820e48aa 100644 --- a/apps/web/src/styles/editor-themes/index.css +++ b/apps/web/src/styles/editor-themes/index.css @@ -3,5 +3,4 @@ @import "./outline-emerald.css"; @import "./wechat-green.css"; @import "./modern-mint.css"; -@import "./marxico.css"; diff --git a/apps/web/src/styles/editor-themes/marxico.css b/apps/web/src/styles/editor-themes/marxico.css deleted file mode 100644 index a7698e7f..00000000 --- a/apps/web/src/styles/editor-themes/marxico.css +++ /dev/null @@ -1,390 +0,0 @@ -/* Classic StackEdit/Bootstrap-era Markdown typography, adapted for EdgeEver with Marxico as a Chinese presentation reference. */ - -.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 { - 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 pre code { - display: block; -} - -.edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-mermaid-code-block:not(.is-source-visible) .edgeever-mermaid-source { - display: none; -} - -.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: var(--workspace-editor); - --editor-theme-text: #d9e1dc; - --editor-theme-muted: #99a7a0; - --editor-theme-heading: #eef2ef; - --editor-theme-accent: #78b7e8; - --editor-theme-soft: #202a25; - --editor-theme-border: #39473f; - --editor-theme-code-bg: #2a2023; - --editor-theme-code-text: #f2b0c2; - --editor-theme-table-stripe: #1d2621; - --editor-theme-table-hover: #24312a; - 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: #acd5f4; -} - -: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: #151d19; - color: var(--editor-theme-text); -} - -:root.dark .edgeever-editor[data-editor-theme="marxico"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-code-copy-button { - border-color: var(--editor-theme-border); - background: #151d19; - 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); -} diff --git a/apps/web/src/styles/editor-typography.test.mjs b/apps/web/src/styles/editor-typography.test.mjs index 24bfcb60..0b844130 100644 --- a/apps/web/src/styles/editor-typography.test.mjs +++ b/apps/web/src/styles/editor-typography.test.mjs @@ -7,7 +7,6 @@ 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"); @@ -91,7 +90,7 @@ describe("editor typography contract", () => { }); test("does not let preset themes override body rhythm", () => { - for (const filename of PRESET_THEME_FILES.filter((filename) => filename !== "marxico.css")) { + for (const filename of PRESET_THEME_FILES) { const source = readStyle(`./editor-themes/${filename}`); const editorRules = declarationsForSelector(source, ".ProseMirror"); const paragraphRules = declarationsForSelector(source, ".ProseMirror p"); @@ -107,26 +106,4 @@ describe("editor typography contract", () => { } }); - 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/); - }); }); diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index aaf0c8ba..922e644e 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -834,7 +834,7 @@ textarea { font-size: 17px; font-weight: 650; line-height: 1.35; - color: #111; + color: var(--publish-text-strong, #111); } .edgeever-phone-preview .edgeever-phone-mermaid { diff --git a/apps/web/src/styles/mermaid-layout.test.mjs b/apps/web/src/styles/mermaid-layout.test.mjs index b49debb4..dc43b52d 100644 --- a/apps/web/src/styles/mermaid-layout.test.mjs +++ b/apps/web/src/styles/mermaid-layout.test.mjs @@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; const globals = readFileSync(new URL("./globals.css", import.meta.url), "utf8"); -const marxico = readFileSync(new URL("./editor-themes/marxico.css", import.meta.url), "utf8"); describe("Mermaid editor layout", () => { test("caps wide-screen diagram blocks without narrowing compact viewports", () => { @@ -17,9 +16,4 @@ describe("Mermaid editor layout", () => { ); }); - test("keeps Mermaid source collapsed in the classic blue and white theme", () => { - expect(marxico).toMatch( - /\.edgeever-mermaid-code-block:not\(\.is-source-visible\) \.edgeever-mermaid-source\s*{[^}]*display:\s*none;/s, - ); - }); }); diff --git a/apps/web/src/styles/publish-layout.css b/apps/web/src/styles/publish-layout.css index 994e7b3e..4cb6f7e3 100644 --- a/apps/web/src/styles/publish-layout.css +++ b/apps/web/src/styles/publish-layout.css @@ -1,12 +1,40 @@ /* Paper editor themes: writing canvas matches Copy to WeChat. */ +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) { + --editor-theme-bg: var(--publish-bg); + --editor-theme-text: var(--publish-text); + --editor-theme-muted: var(--publish-text-muted); + --editor-theme-heading: var(--publish-text-strong); + --editor-theme-accent: var(--publish-accent); + --editor-theme-soft: var(--publish-accent-soft); + --editor-theme-border: var(--publish-accent-border); + --editor-theme-code-bg: var(--publish-code-bg); + --editor-theme-code-text: var(--publish-code-text); + --editor-theme-table-stripe: color-mix(in srgb, var(--publish-accent-soft) 72%, var(--publish-bg)); + --editor-theme-table-hover: var(--publish-accent-soft); + --brand-green: var(--publish-accent); + --brand-green-hover: var(--publish-link); + --brand-green-muted: var(--publish-accent-soft); + --brand-green-soft: var(--publish-accent-soft); + --brand-green-border: var(--publish-accent-border); + --brand-green-text: var(--publish-link); + --memo-content-divider-color: var(--publish-accent-border); +} + .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror { color: var(--publish-text); + background: var(--publish-bg); + letter-spacing: var(--editor-letter-spacing); + line-height: var(--editor-body-line-height); counter-reset: publish-h2; } -.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror p { +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror p, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror li, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror blockquote { color: var(--publish-text); + letter-spacing: var(--editor-letter-spacing); + line-height: var(--editor-body-line-height); } .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h1, @@ -15,6 +43,7 @@ color: var(--publish-text-strong); border: 0; padding-left: 0; + letter-spacing: var(--editor-heading-letter-spacing); } .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror strong { @@ -39,6 +68,7 @@ } .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror code { + border-color: var(--publish-accent-border); background: var(--publish-code-bg); color: var(--publish-code-text); } @@ -53,22 +83,69 @@ } .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror th { - background: var(--publish-accent-soft); color: var(--publish-text-strong); border-color: var(--publish-accent-border); } .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror td { - border-color: var(--publish-divider); color: var(--publish-text); } +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block, .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="intro"], .edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="key-point"] { border-color: var(--publish-accent-border); background: var(--publish-surface); } +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block:hover .edgeever-theme-block__toolbar, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-selected="true"] .edgeever-theme-block__toolbar { + border-bottom-color: var(--publish-divider); +} + +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .tableWrapper { + border-color: var(--publish-accent-border); + background: var(--publish-bg); +} + +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th { + background: var(--publish-accent-soft); + color: var(--publish-text-strong); + border-color: var(--publish-accent-border); +} + +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror td { + background: var(--publish-bg); + border-color: var(--publish-divider); + color: var(--publish-text); +} + +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror tbody tr:nth-child(even) td { + background: color-mix(in srgb, var(--publish-accent-soft) 72%, var(--publish-bg)); +} + +.edgeever-editor[data-paper-theme][data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror tbody tr:hover td { + background: var(--publish-accent-soft); +} + +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-mermaid-preview { + border-color: var(--publish-accent-border); + background: var(--publish-surface); +} + +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-mermaid-tool-button:hover, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-mermaid-tool-button:focus-visible, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-mermaid-tool-button[aria-pressed="true"] { + background: var(--publish-accent-soft); + color: var(--publish-link); +} + +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror strong, +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror strong, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror strong { + color: var(--publish-text-strong); +} + .edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror hr:not([data-edgeever-merge-divider]), .edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror hr:not([data-edgeever-merge-divider]) { width: 42%; @@ -76,129 +153,384 @@ margin-right: auto; } +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h3::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h3::after, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + color: var(--publish-accent); + font-weight: 700; +} + +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1::before { + display: block; + margin: 0 0 0.45rem; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.16em; + line-height: 1.4; +} + +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h1::before { content: "GUIDE · ARTICLE"; } +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h1::before { content: "ARTICLE"; } +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h1::before, +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h1::before { content: "EDITION · FIELD NOTES"; } +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h1::before { content: "MINIMAL · ARTICLE"; } +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1::before { content: "FIELD NOTES"; } + .edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h1 { text-align: center; font-weight: 800; - border-bottom: 3px solid var(--publish-accent); - padding-bottom: 0.55rem; - margin: 0 0 1.5rem; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1::after { + display: block; + width: 42px; + height: 3px; + margin: 0.55rem 0 0; + background: var(--publish-accent); + content: ""; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h1::before { + display: inline-block; + width: 1.7rem; + height: 2.2rem; + margin-left: 0.45rem; + vertical-align: middle; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-sprig) center / contain no-repeat; + mask: var(--publish-ornament-sprig) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h1::after { + width: 9.5rem; + height: 12px; + margin: 0.55rem auto 0; + background-color: var(--publish-accent); + background-image: none; + -webkit-mask: var(--publish-ornament-brush) center / contain no-repeat; + mask: var(--publish-ornament-brush) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2::after { + display: inline-block; + width: 1.45rem; + height: 1.45rem; + margin-left: 0.5rem; + vertical-align: middle; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-sun) center / contain no-repeat; + mask: var(--publish-ornament-sun) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + position: relative; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + width: 1.7rem; + height: 1.25rem; + margin: 0 0 0.35rem; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-quote) center / contain no-repeat; + mask: var(--publish-ornament-quote) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror blockquote::after, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror blockquote::after { + position: absolute; + top: -0.45rem; + right: 1rem; + width: 3.3rem; + height: 1.15rem; + background-color: var(--publish-accent); + content: ""; + transform: rotate(12deg); + -webkit-mask: var(--publish-ornament-tape) center / contain no-repeat; + mask: var(--publish-ornament-tape) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + position: relative; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h1::after { + display: inline-block; + width: 1.25rem; + height: 1.7rem; + margin-left: 0.45rem; + vertical-align: middle; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-leaf) center / contain no-repeat; + mask: var(--publish-ornament-leaf) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h2 { + border-bottom: 0; + padding-bottom: 0; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h2::after { + display: block; + width: 6rem; + height: 0.7rem; + margin-top: 0.3rem; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-swoosh) left / contain no-repeat; + mask: var(--publish-ornament-swoosh) left / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h1::after, +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h1::after { + content: "VOL. 01"; + width: auto; + height: auto; + background: none; + color: var(--publish-text-muted); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.14em; } .edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2, -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h2, .edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2, -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2 { +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h2 { counter-increment: publish-h2; } -.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2::before { - content: counter(publish-h2, decimal-leading-zero); - display: block; +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + display: inline-block; + margin-right: 0.7rem; + vertical-align: middle; color: var(--publish-accent); - font-size: 1.75rem; + font-family: Georgia, "Times New Roman", serif; + font-size: 2.6rem; font-style: italic; font-weight: 700; - line-height: 1.1; - margin: 0 0 0.35rem; + line-height: 0.9; + content: counter(publish-h2, decimal-leading-zero); } -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2::before, -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2::before, -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2::before { - content: counter(publish-h2, decimal-leading-zero); - display: block; - color: var(--publish-accent); - font-size: 0.8rem; +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + font-size: 2.4rem; +} + +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + display: inline-block; + min-width: 2rem; + margin-right: 0.7rem; + padding: 0.35rem 0.45rem; + vertical-align: middle; + border-radius: 6px; + background: var(--publish-accent); + color: #fff; + font-size: 0.85rem; font-weight: 700; - letter-spacing: 0.12em; - margin: 0 0 0.25rem; + line-height: 1; + text-align: center; + content: counter(publish-h2, decimal-leading-zero); +} + +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + display: block; + margin: 0 0 0.35rem; + color: var(--publish-accent); + font-family: Georgia, "Times New Roman", serif; + font-size: 2.8rem; + font-style: italic; + font-weight: 700; + line-height: 0.9; + content: counter(publish-h2, decimal-leading-zero); +} + +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h2::before, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + display: block; + margin: 0 0 0.3rem; + color: var(--publish-text-muted); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.16em; +} + +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + content: "NO. " counter(publish-h2, decimal-leading-zero); +} + +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h2::before { + content: counter(publish-h2, decimal-leading-zero) " · CHAPTER ONE"; } .edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, .edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty { +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty { counter-increment: none; } -.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::before, -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::before, -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::before, -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::before { +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h1.is-empty::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h1.is-empty::after, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h2.is-empty::after, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h3.is-empty::before, +.edgeever-editor[data-paper-theme]:not([data-editor-theme="default"]) .ProseMirror h3.is-empty::after { content: none; } -.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h3 { +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h3, +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h3 { text-align: center; font-size: 0.95rem; - border-bottom: 2px solid var(--publish-accent-border); - padding-bottom: 0.45rem; } -.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror blockquote, +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror h3::after, +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h3::after { + display: block; + width: 10rem; + height: 0; + margin: 0.55rem auto 0; + border-top: 3px solid var(--publish-accent); + box-shadow: 28px 0 0 0 var(--publish-accent-border); + content: ""; +} + +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h3, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h3, +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h3 { + border-left: 3px solid var(--publish-accent); + padding-left: 0.65rem; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h3 { + color: var(--publish-accent); + text-align: center; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h3::before, +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h3::after { + content: "//"; + margin: 0 0.55rem; + font-weight: 800; +} + +.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h3::before { + content: "— "; + color: var(--publish-text-muted); +} + +.edgeever-editor[data-editor-theme="letter"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + margin: 0 0 1.5rem; + padding: 1.1rem 1rem 1.15rem 1.2rem; + border: 1px solid var(--publish-accent-border); + border-radius: 18px; + background-color: var(--publish-surface); + background-image: linear-gradient(to right, var(--publish-accent-grid) 1px, transparent 1px), + linear-gradient(to bottom, var(--publish-accent-grid) 1px, transparent 1px); + background-size: 30px 30px; + color: var(--publish-text-muted); +} + +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + display: block; + margin: 0 0 0.2rem; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.8rem; + font-weight: 700; + line-height: 1; + content: "“"; +} + +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + display: block; + width: 1.7rem; + height: 1.25rem; + margin: 0 0 0.35rem; + background-color: var(--publish-accent); + content: ""; + -webkit-mask: var(--publish-ornament-quote) center / contain no-repeat; + mask: var(--publish-ornament-quote) center / contain no-repeat; +} + +.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror blockquote, +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + margin: 0 0 1.5rem; + padding: 0.35rem 0 0.35rem 1rem; + border-left: 3px solid var(--publish-accent); + background: transparent; + color: var(--publish-text-muted); +} + +.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + border-left-width: 4px; +} + +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror blockquote, +.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror blockquote, .edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror blockquote { margin: 0 0 1.5rem; padding: 1rem 1.1rem; border: 1px solid var(--publish-accent-border); border-radius: 12px; - background: var(--publish-surface); - color: var(--publish-text-muted); -} - -.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror blockquote { - border-radius: 16px; -} - -.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror h2, -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left: 4px solid var(--publish-accent); - padding-left: 0.75rem; -} - -.edgeever-editor[data-editor-theme="guide"]:not([data-editor-theme="default"]) .ProseMirror blockquote, -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror blockquote, -.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror blockquote { - margin: 0 0 1.5rem; - padding: 0.75rem 1rem; - border-left: 4px solid var(--publish-accent); background: var(--publish-accent-soft); color: var(--publish-text-muted); } -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h1 { - text-align: left; - border-top: 3px solid var(--publish-accent); - padding-top: 0.55rem; -} - -.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h3, -.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h3 { - border-bottom: 2px solid var(--publish-accent); - padding-bottom: 0.35rem; +.edgeever-editor[data-editor-theme="blueprint"]:not([data-editor-theme="default"]) .ProseMirror h2 { + border-left: 3px solid var(--publish-accent); + padding-left: 0.75rem; } .edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h1, -.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1 { +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h1 { text-align: left; font-weight: 700; } -.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h1 { - font-weight: 600; - margin: 0 0 2rem; -} - -.edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror h2, .edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-bottom: 1px solid var(--publish-divider); - padding-bottom: 0.45rem; + display: inline-block; + border-bottom: 2px solid var(--publish-accent); + padding-bottom: 0.4rem; } .edgeever-editor[data-editor-theme="journal"]:not([data-editor-theme="default"]) .ProseMirror blockquote { margin: 0 0 1.5rem; - padding: 0.25rem 0 0.25rem 1rem; - border-left: 2px solid var(--publish-divider); - background: transparent; + padding: 1rem 0.5rem; + border-top: 1px solid var(--publish-accent-border); + border-bottom: 1px solid var(--publish-accent-border); + background: var(--publish-surface); color: var(--publish-text-muted); font-style: italic; } @@ -208,100 +540,73 @@ border-radius: 12px; } -.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h1 { - font-weight: 800; - border-bottom: 6px solid var(--publish-accent); - padding-bottom: 0.55rem; -} - -.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left: 6px solid var(--publish-accent); - padding-left: 0.85rem; -} - -.edgeever-editor[data-editor-theme="stance"]:not([data-editor-theme="default"]) .ProseMirror blockquote { +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror blockquote { margin: 0 0 1.5rem; - padding: 0.85rem 1rem; - border-left: 6px solid var(--publish-accent); + padding: 2.4rem 1rem 1rem; + border: 0; + border-radius: 8px; background: var(--publish-accent-soft); color: var(--publish-text-muted); } -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h1 { - letter-spacing: 0.12em; - font-size: 1.35rem; - border-bottom: 1px dashed var(--publish-accent); - padding-bottom: 0.55rem; -} - -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h2, -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror h3 { - border-bottom: 1px dashed var(--publish-accent-border); - padding-bottom: 0.4rem; -} - -.edgeever-editor[data-editor-theme="stub"]:not([data-editor-theme="default"]) .ProseMirror blockquote { - margin: 0 0 1.5rem; - padding: 0.85rem 1rem; - border: 1px dashed var(--publish-accent); - border-radius: 2px; - background: var(--publish-surface); - color: var(--publish-text-muted); -} - -.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h1 { - font-size: 1.4rem; - border-bottom: 1px solid var(--publish-accent); - padding-bottom: 0.5rem; -} - -.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror h2 { - color: var(--publish-accent); - font-size: 1.15rem; -} - -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h1 { - letter-spacing: 0.06em; - border-bottom: 1px solid var(--publish-divider); - padding-bottom: 0.5rem; -} - -.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left: 2px solid var(--publish-accent); - padding-left: 0.7rem; +.edgeever-editor[data-editor-theme="brief"]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + display: block; + margin: -2.4rem -1rem 0.85rem; + padding: 0.55rem 1rem; + background: var(--publish-accent); + color: #fff; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.16em; + content: "EDITOR'S NOTE"; } .edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror blockquote { margin: 0 0 1.5rem; - padding: 0.75rem 1rem; - border: 1px solid var(--publish-divider); + padding: 0; + border: 0; background: transparent; + color: var(--publish-text-strong); + font-weight: 600; + text-align: center; +} + +.edgeever-editor[data-editor-theme="outline"]:not([data-editor-theme="default"]) .ProseMirror blockquote::before { + display: block; + margin: 0 0 0.65rem; color: var(--publish-text-muted); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.18em; + content: "QUOTE"; } .edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror h2 { - font-weight: 500; - margin-top: 2.4rem; + font-weight: 600; + margin-top: 2.2rem; } .edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror blockquote { margin: 0 0 2rem; - padding: 0.5rem 0; + padding: 0.35rem 1.5rem; border: 0; background: transparent; color: var(--publish-text-muted); font-style: italic; + text-align: center; } -.edgeever-editor[data-editor-theme="grove"]:not([data-editor-theme="default"]) .ProseMirror h1 { - border-bottom: 2px solid var(--publish-accent); - padding-bottom: 0.5rem; +.edgeever-editor[data-editor-theme="zen"]:not([data-editor-theme="default"]) .ProseMirror blockquote::after { + display: block; + margin-top: 0.5rem; + color: var(--publish-accent); + letter-spacing: 0.2em; + content: "—"; } .edgeever-editor[data-publish-surface="phone"][data-paper-theme] .ProseMirror h1 { font-size: 1.5rem; font-weight: 800; - letter-spacing: normal; line-height: 1.4; margin-bottom: 0.75rem; } @@ -322,15 +627,14 @@ padding: 0.65rem 0.75rem; } -.edgeever-editor[data-publish-surface="phone"][data-editor-theme="letter"] .ProseMirror h2::before { - font-size: 1.15rem; - margin-bottom: 0.2rem; +.edgeever-editor[data-publish-surface="phone"][data-editor-theme="letter"] .ProseMirror h2::before, +.edgeever-editor[data-publish-surface="phone"][data-editor-theme="guide"] .ProseMirror h2::before, +.edgeever-editor[data-publish-surface="phone"][data-editor-theme="brief"] .ProseMirror h2::before { + font-size: 2rem; } -.edgeever-editor[data-publish-surface="phone"][data-editor-theme="blueprint"] .ProseMirror h2::before, -.edgeever-editor[data-publish-surface="phone"][data-editor-theme="stub"] .ProseMirror h2::before, .edgeever-editor[data-publish-surface="phone"][data-editor-theme="outline"] .ProseMirror h2::before { - font-size: 0.75rem; + font-size: 2.1rem; } :root.dark .edgeever-editor[data-paper-theme] { @@ -341,6 +645,7 @@ --publish-surface: color-mix(in srgb, var(--publish-accent) 16%, var(--workspace-editor)); --publish-accent-soft: color-mix(in srgb, var(--publish-accent) 14%, var(--workspace-editor)); --publish-accent-border: color-mix(in srgb, var(--publish-accent) 36%, var(--workspace-editor)); + --publish-accent-grid: color-mix(in srgb, var(--publish-accent) 18%, transparent); --publish-divider: color-mix(in srgb, var(--publish-accent) 26%, var(--workspace-editor)); --publish-code-bg: color-mix(in srgb, var(--publish-accent) 10%, var(--workspace-editor)); --publish-code-text: var(--publish-accent); diff --git a/docs/demo-sample-note.md b/docs/demo-sample-note.md index ab9e48d4..3ff969ec 100644 --- a/docs/demo-sample-note.md +++ b/docs/demo-sample-note.md @@ -67,7 +67,7 @@ EdgeEver is engineered to provide a distraction-free, elegant writing experience - **Reading Protection Mode**: When reading or reviewing notes, press `Cmd/Ctrl + E` to toggle read-only protection, locking the editor to prevent accidental edits while browsing; press it again to seamlessly resume editing. ### 🎭 Curated Typography Themes & Publishing Export -- **Preset Editor Themes**: Switch effortlessly between `WeChat Classic Green`, `Modern Mint`, `Minimal Emerald`, `Outline Emerald`, and more. +- **Preset Editor Themes**: Switch effortlessly between `WeChat Green`, `Mint Green`, `Minimal Green`, `Ruled Green`, and paper styles such as Letter, Guide, and Blueprint. - **One-Click Publishing Export**: Built for publishers and bloggers. Click the **WeChat Icon** in the top bar to format your note with inline CSS. Paste directly into WeChat Official Account editor, Substack, Medium, or WordPress while preserving layout and syntax highlighting. ### 🖼️ 8 Exquisite Social Poster Themes diff --git a/docs/demo-sample-note.zh-CN.md b/docs/demo-sample-note.zh-CN.md index 66ca7c10..2a7b2bb9 100644 --- a/docs/demo-sample-note.zh-CN.md +++ b/docs/demo-sample-note.zh-CN.md @@ -67,7 +67,7 @@ EdgeEver 将高效与优雅融入每一处交互细节,助你专注于思考 - **阅读保护模式 (Reading Protection)**:日常翻阅或查阅笔记时,按下 `Cmd/Ctrl + E` 即可一键开启只读保护,锁定当前编辑状态,避免沉浸阅读时误触键盘或意外改动笔记内容;再次按下即可随手切回编辑。 ### 🎭 精选排版主题与一键自媒体发布 -- **内置排版主题**:支持一键切换 `WeChat Classic Green (微信经典绿)`、`Modern Mint (薄荷青)`、`Minimal Emerald (极简祖母绿)`、`Outline Emerald (大纲祖母绿)` 等风格。 +- **内置排版主题**:支持一键切换 `微信绿`、`薄荷绿`、`极简绿`、`描边绿`,以及暖笺、教程、蓝图等纸稿风格。 - **一键排版复制**:专为内容创作者设计。点击顶部工具栏的**微信公众号图标**,系统自动将当前笔记转为内联 CSS 样式的优雅富文本,直接粘贴至微信公众号后台、Substack 或 WordPress,排版与代码高亮完美保真。 ### 🖼️ 8 套精美社交分享海报 (Poster Cards) diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 65965db4..e89f50e2 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -613,16 +613,20 @@ export const enUS = { outline: "Outline", zen: "Whitespace", grove: "Grove", - "minimal-emerald": "Minimal Emerald", - "outline-emerald": "Outline Emerald", - "wechat-green": "WeChat Classic Green", - "modern-mint": "Modern Mint", - marxico: "Classic Blue & White", + "minimal-emerald": "Minimal Green", + "outline-emerald": "Ruled Green", + "wechat-green": "WeChat Green", + "modern-mint": "Mint Green", custom: "Custom", }, customEditorTheme: { title: "Custom editor theme", description: "Adjust editor colors. This setting is saved in the current browser.", + settingsTitle: "Custom theme", + settingsDescription: "Switch editor themes from the toolbar. Manage browser-only custom palettes here.", + defaultName: "Custom theme", + newName: "New theme {{n}}", + create: "Create new theme…", edit: "Edit colors", import: "Import", export: "Export", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 4cb01fef..d92736f4 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -613,16 +613,20 @@ export const zhCN = { outline: "纲要", zen: "留白", grove: "草木", - "minimal-emerald": "minimal品牌绿", - "outline-emerald": "Outline 品牌绿", - "wechat-green": "微信经典绿", - "modern-mint": "极客薄荷绿", - marxico: "经典蓝白", + "minimal-emerald": "极简绿", + "outline-emerald": "描边绿", + "wechat-green": "微信绿", + "modern-mint": "薄荷绿", custom: "自定义", }, customEditorTheme: { title: "自定义编辑器主题", description: "调整正文编辑区的颜色,设置仅保存在当前浏览器中。", + settingsTitle: "自定义主题", + settingsDescription: "全部排版主题在编辑器工具栏切换。这里只管理保存在当前浏览器中的自定义配色。", + defaultName: "自定义主题", + newName: "新主题 {{n}}", + create: "新建主题…", edit: "编辑配色", import: "导入", export: "导出",