diff --git a/README.md b/README.md index 6558e672..59cad7b7 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ If you want to migrate notes from other platforms to EdgeEver, please refer to t ## Acknowledgements - The visual design of the editor themes is inspired by [gzh-design-skill](https://github.com/isjiamu/gzh-design-skill), whose themes were originally created by Jiamu (甲木) × Moyu Xiaoli (摸鱼小李). Thanks for their open-source work. +- The `嫩青` editor theme is adapted from the visual style of the [MDNice editor](https://editor.mdnice.com/). Thanks to the original authors for the reference. ## Disclaimer diff --git a/README.zh-CN.md b/README.zh-CN.md index d121b609..231ea724 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -240,6 +240,7 @@ Cloudflare Worker 侧执行图片处理会消耗计算/图片处理额度,因 ## 致谢 - 编辑器主题的视觉设计参考自 [gzh-design-skill](https://github.com/isjiamu/gzh-design-skill),相关主题由甲木 × 摸鱼小李原创。感谢作者的开源工作。 +- `嫩青`编辑器主题的视觉样式参考自 [MDNice 编辑器](https://editor.mdnice.com/)。感谢原作者提供的设计参考。 ## 免责声明 diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index 3a5f3ee6..0672ca1f 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -27,6 +27,7 @@ import { Search, Type, X, + Copy, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { GitHubRepositoryLink } from "@/components/GitHubRepositoryLink"; @@ -85,6 +86,8 @@ import { getEditableMemoTitle, getNotebookMoveOptions, } from "@/lib/app-helpers"; +import { copyEditorToWeChat, copyMarkdownToWeChat } from "@/lib/wechat-copy"; +import { ThemeBlock } from "./ThemeBlock"; const SUPPORTED_PASTE_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"]); const MOBILE_EDITOR_QUERY = "(max-width: 639px)"; @@ -1145,6 +1148,7 @@ const RichEditorPane = ({ const [mobileImeDebugOpen, setMobileImeDebugOpen] = useState(false); const [mobileImeDebugActiveElement, setMobileImeDebugActiveElement] = useState(getActiveElementLabel); const [mobileImeDebugEvents, setMobileImeDebugEvents] = useState([]); + const [wechatCopyState, setWechatCopyState] = useState<"idle" | "copied" | "error">("idle"); const notebookOptions = useMemo(() => getNotebookMoveOptions(notebooks), [notebooks]); const readOnly = isTrashView || Boolean(memo?.isDeleted); const mobileDefaultEditRequested = Boolean(memo?.id && memo.id === mobileDefaultEditMemoId && !readOnly); @@ -1306,6 +1310,7 @@ const RichEditorPane = ({ extensions: [ StarterKit.configure({ codeBlock: false }), EdgeEverCodeBlock.configure({ lowlight: codeBlockLowlight, defaultLanguage: "plaintext" }), + ThemeBlock, ResizableImage.configure({ allowBase64: false, inline: false, @@ -1919,6 +1924,25 @@ const RichEditorPane = ({ markDirty(); }, [markDirty]); + const handleCopyToWeChat = useCallback(async () => { + if (!isEditorReady(editor)) { + return; + } + + try { + if (useMarkdownSourceEditor) { + await copyMarkdownToWeChat(markdownSource); + } else { + await copyEditorToWeChat(editor); + } + setWechatCopyState("copied"); + window.setTimeout(() => setWechatCopyState("idle"), 1800); + } catch { + setWechatCopyState("error"); + window.setTimeout(() => setWechatCopyState("idle"), 2200); + } + }, [editor, markdownSource, useMarkdownSourceEditor]); + useEffect(() => { if (!useMobilePlainTextEditor) { return; @@ -2560,6 +2584,17 @@ const RichEditorPane = ({ + diff --git a/apps/web/src/components/EditorToolbar.tsx b/apps/web/src/components/EditorToolbar.tsx index 7dec5c61..fc9e2543 100644 --- a/apps/web/src/components/EditorToolbar.tsx +++ b/apps/web/src/components/EditorToolbar.tsx @@ -15,6 +15,7 @@ import { Workflow, Minus, Paperclip, + Blocks, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; @@ -22,6 +23,13 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { getActiveBlockValue } from "@/lib/app-helpers"; import { CODE_BLOCK_LANGUAGES, getCodeBlockLanguageValue } from "@/lib/code-block"; import { EditorTableMenu } from "@/components/EditorTableMenu"; +import { insertThemeBlock, type ThemeBlockKind } from "./ThemeBlock"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; const EditorToolbarButton = ({ active = false, @@ -393,6 +401,27 @@ export const EditorToolbar = ({ > + + + + + + {(["intro", "key-point", "callout", "chapter"] as ThemeBlockKind[]).map((kind) => ( + run((current) => insertThemeBlock(current, kind))}> + {t(`editorToolbar.themeBlocks.${kind}`)} + + ))} + + )} diff --git a/apps/web/src/components/MobileStandaloneTiptapEditor.tsx b/apps/web/src/components/MobileStandaloneTiptapEditor.tsx index 59081adc..ab295781 100644 --- a/apps/web/src/components/MobileStandaloneTiptapEditor.tsx +++ b/apps/web/src/components/MobileStandaloneTiptapEditor.tsx @@ -40,6 +40,7 @@ import { } from "@/lib/mobile-editor-standalone"; import { getMemoUpdateQueueId, isMemoUpdateAlreadyApplied, queueMemoUpdate, shouldQueueMemoSaveError } from "@/lib/sync-queue"; import { EdgeEverCodeBlock, codeBlockLowlight } from "@/lib/code-block"; +import { ThemeBlock } from "./ThemeBlock"; type ListNotebooksResponse = { notebooks: Notebook[]; @@ -172,6 +173,7 @@ export const MobileStandaloneTiptapEditor = ({ extensions: [ StarterKit.configure({ codeBlock: false }), EdgeEverCodeBlock.configure({ lowlight: codeBlockLowlight, defaultLanguage: "plaintext" }), + ThemeBlock, Image.configure({ allowBase64: false, inline: false, diff --git a/apps/web/src/components/ThemeBlock.tsx b/apps/web/src/components/ThemeBlock.tsx new file mode 100644 index 00000000..5e84a2bc --- /dev/null +++ b/apps/web/src/components/ThemeBlock.tsx @@ -0,0 +1,96 @@ +import { Node, mergeAttributes } from "@tiptap/core"; +import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, type NodeViewProps } from "@tiptap/react"; +import { Lightbulb, MessageCircle, PanelTop, Trash2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export const THEME_BLOCK_KINDS = ["intro", "key-point", "callout", "chapter"] as const; +export type ThemeBlockKind = (typeof THEME_BLOCK_KINDS)[number]; + +const kindIcon = { + intro: MessageCircle, + "key-point": Lightbulb, + callout: PanelTop, + chapter: PanelTop, +} satisfies Record; + +const ThemeBlockView = ({ node, deleteNode, selected }: NodeViewProps) => { + const { t } = useTranslation(); + const kind = (node.attrs.kind as ThemeBlockKind) || "intro"; + const Icon = kindIcon[kind] ?? MessageCircle; + + return ( + +
+ + + +
+ +
+ ); +}; + +export const ThemeBlock = Node.create({ + name: "edgeeverThemeBlock", + group: "block", + content: "block+", + defining: true, + isolating: true, + + addAttributes() { + return { + kind: { + default: "intro", + parseHTML: (element: HTMLElement) => element.getAttribute("data-theme-block-kind") || "intro", + renderHTML: (attributes: { kind?: string }) => ({ + "data-theme-block-kind": attributes.kind || "intro", + }), + }, + }; + }, + + parseHTML() { + return [{ tag: "section[data-theme-block-kind]" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["section", mergeAttributes(HTMLAttributes, { "data-edgeever-theme-block": "true" }), 0]; + }, + + addNodeView() { + return ReactNodeViewRenderer(ThemeBlockView); + }, +}); + +export const insertThemeBlock = (editor: { chain: () => any; state: any }, kind: ThemeBlockKind) => { + const { from, to } = editor.state.selection; + const selectedText = editor.state.doc.textBetween(from, to, "\n", "\n").trim(); + + editor + .chain() + .focus() + .insertContentAt( + { from, to }, + { + type: "edgeeverThemeBlock", + attrs: { kind }, + content: [{ type: "paragraph", content: selectedText ? [{ type: "text", text: selectedText }] : undefined }], + } + ) + .run(); +}; diff --git a/apps/web/src/components/ThemeProvider.tsx b/apps/web/src/components/ThemeProvider.tsx index 871cb6fd..f0c13206 100644 --- a/apps/web/src/components/ThemeProvider.tsx +++ b/apps/web/src/components/ThemeProvider.tsx @@ -29,6 +29,7 @@ export const EDITOR_THEME_NAMES = [ "zen-whitespace", "moyu-ticket", "olive-journal", + "mdnice-nenqing", ] as const; export type EditorThemeName = (typeof EDITOR_THEME_NAMES)[number]; diff --git a/apps/web/src/lib/wechat-copy.ts b/apps/web/src/lib/wechat-copy.ts new file mode 100644 index 00000000..ca9ab8de --- /dev/null +++ b/apps/web/src/lib/wechat-copy.ts @@ -0,0 +1,116 @@ +import type { Editor } from "@tiptap/react"; +import { marked } from "marked"; + +const WECHAT_STYLES: Record = { + p: "margin: 0 0 1em; line-height: 1.75; font-size: 16px; color: #333;", + h1: "margin: 1.2em 0 0.6em; font-size: 24px; line-height: 1.35; font-weight: 700; color: #1f2937;", + h2: "margin: 1.1em 0 0.55em; font-size: 21px; line-height: 1.4; font-weight: 700; color: #1f2937;", + h3: "margin: 1em 0 0.5em; font-size: 18px; line-height: 1.45; font-weight: 700; color: #1f2937;", + blockquote: "margin: 1em 0; padding: 0.6em 1em; border-left: 4px solid #10b981; background: #f0fdf4; color: #4b5563; line-height: 1.75;", + ul: "margin: 0 0 1em; padding-left: 1.6em; line-height: 1.75;", + ol: "margin: 0 0 1em; padding-left: 1.6em; line-height: 1.75;", + li: "margin: 0.25em 0; line-height: 1.75;", + a: "color: #059669; text-decoration: underline;", + strong: "font-weight: 700;", + em: "font-style: italic;", + del: "text-decoration: line-through;", + code: "padding: 0.15em 0.35em; border-radius: 3px; background: #f3f4f6; color: #be123c; font-family: Menlo, Consolas, monospace; font-size: 0.9em;", + pre: "margin: 1em 0; padding: 12px 14px; overflow: hidden; border-radius: 6px; background: #f6f8fa; color: #24292f; line-height: 1.6; text-align: left;", + hr: "margin: 1.5em 0; border: 0; border-top: 1px solid #e5e7eb;", + table: "width: 100%; margin: 1em 0; border-collapse: collapse; font-size: 14px; line-height: 1.6;", + th: "padding: 8px; border: 1px solid #d1d5db; background: #f3f4f6; font-weight: 700; text-align: left;", + td: "padding: 8px; border: 1px solid #d1d5db; text-align: left;", + img: "display: block; max-width: 100%; height: auto; margin: 1em auto;", +}; + +const THEME_BLOCK_LABELS: Record = { + intro: "引言", + "key-point": "重点观点", + callout: "提示", + chapter: "章节", +}; + +const THEME_BLOCK_STYLES: Record = { + intro: { + block: "margin: 20px 0; padding: 0 0 4px; border-left: 5px solid #059669; background: #f0fdf4; color: #374151;", + label: "padding: 10px 14px 0; color: #059669; font-size: 12px; font-weight: 700; letter-spacing: 1px;", + }, + "key-point": { + block: "margin: 20px 0; padding: 0 0 4px; border: 1px solid #a7f3d0; border-radius: 8px; background: #f0fdf4; color: #374151;", + label: "padding: 10px 14px 0; color: #047857; font-size: 12px; font-weight: 700; letter-spacing: 1px;", + }, + callout: { + block: "margin: 20px 0; padding: 0 0 4px; border: 1px dashed #6ee7b7; background: #ecfdf5; color: #374151;", + label: "padding: 10px 14px 0; color: #059669; font-size: 12px; font-weight: 700; letter-spacing: 1px;", + }, + chapter: { + block: "margin: 28px 0 16px; padding: 0 0 4px; border-top: 3px solid #059669; color: #111827;", + label: "padding: 10px 0 0; color: #059669; font-size: 12px; font-weight: 700; letter-spacing: 2px;", + }, +}; + +const applyInlineStyles = (root: HTMLElement, editorTheme?: string) => { + root.style.cssText = "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 16px; line-height: 1.75; color: #333; word-break: break-word;"; + root.querySelectorAll("*").forEach((element) => { + const style = WECHAT_STYLES[element.tagName.toLowerCase()]; + if (style) element.style.cssText = `${style}${element.style.cssText}`; + }); + root.querySelectorAll("pre code").forEach((element) => { + element.style.cssText = "padding: 0; background: transparent; color: inherit; font-family: Menlo, Consolas, monospace; font-size: 13px; white-space: pre-wrap;"; + }); + + root.querySelectorAll("[data-edgeever-theme-block]").forEach((block) => { + const kind = block.getAttribute("data-theme-block-kind") || "intro"; + const themeStyles = THEME_BLOCK_STYLES[kind] || THEME_BLOCK_STYLES.intro; + const accent = editorTheme === "red-white" ? "#dc2626" : editorTheme === "olive-journal" ? "#ed7b2f" : editorTheme === "graphite-minimal" ? "#52525b" : "#059669"; + block.style.cssText = `${themeStyles.block} border-left-color: ${accent}; ${block.style.cssText}`; + + const label = document.createElement("p"); + label.textContent = THEME_BLOCK_LABELS[kind] || "主题组件"; + label.style.cssText = `${themeStyles.label} color: ${accent}; margin: 0;`; + block.insertBefore(label, block.firstChild); + }); +}; + +export const buildWeChatClipboardHtml = (editor: Editor) => { + const container = document.createElement("div"); + container.innerHTML = editor.getHTML(); + const editorTheme = editor.view.dom.closest("[data-editor-theme]")?.dataset.editorTheme; + applyInlineStyles(container, editorTheme); + return container.outerHTML; +}; + +const copyHtmlToClipboard = async (html: string, plainText: string) => { + if (navigator.clipboard && "ClipboardItem" in window) { + await navigator.clipboard.write([new ClipboardItem({ + "text/html": new Blob([html], { type: "text/html" }), + "text/plain": new Blob([plainText], { type: "text/plain" }), + })]); + return; + } + + const selection = window.getSelection(); + const range = document.createRange(); + const container = document.createElement("div"); + container.setAttribute("contenteditable", "true"); + container.style.cssText = "position: fixed; left: -99999px; top: 0;"; + container.innerHTML = html; + document.body.appendChild(container); + range.selectNodeContents(container); + selection?.removeAllRanges(); + selection?.addRange(range); + const copied = document.execCommand("copy"); + selection?.removeAllRanges(); + container.remove(); + if (!copied) throw new Error("Clipboard copy was not available"); +}; + +export const copyEditorToWeChat = async (editor: Editor) => + copyHtmlToClipboard(buildWeChatClipboardHtml(editor), editor.getText({ blockSeparator: "\n" })); + +export const copyMarkdownToWeChat = async (markdown: string) => { + const container = document.createElement("div"); + container.innerHTML = marked.parse(markdown, { async: false, gfm: true, breaks: false }); + applyInlineStyles(container); + await copyHtmlToClipboard(container.outerHTML, container.textContent ?? ""); +}; diff --git a/apps/web/src/styles/editor-themes/base.css b/apps/web/src/styles/editor-themes/base.css new file mode 100644 index 00000000..32fa33dc --- /dev/null +++ b/apps/web/src/styles/editor-themes/base.css @@ -0,0 +1,340 @@ +/* Shared editor theme structure and themed block components */ + +/* Editor reading themes inspired by the curated gzh-design-skill presets. */ +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #ffffff; + --editor-theme-text: #1f2937; + --editor-theme-muted: #64748b; + --editor-theme-accent: #059669; + --editor-theme-soft: #ecfdf5; + --editor-theme-border: #a7f3d0; + background: var(--editor-theme-bg); +} +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror { + + background: var(--editor-theme-bg); + color: var(--editor-theme-text); + font-family: var(--editor-theme-font, inherit); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h3 { + + color: var(--editor-theme-heading, var(--editor-theme-text)); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h1 { + + border-bottom: 2px solid var(--editor-theme-accent); + padding-bottom: 0.55rem; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border-left: 4px solid var(--editor-theme-accent); + padding-left: 0.7rem; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border-left: 4px solid var(--editor-theme-accent); + background: var(--editor-theme-soft); + color: var(--editor-theme-muted); + padding: 0.8rem 1rem; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror hr { + + border-color: var(--editor-theme-border); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror code, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror pre, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-code-source { + + border-color: var(--editor-theme-border); + background: var(--editor-theme-soft); + color: var(--editor-theme-text); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th { + + background: var(--editor-theme-soft); + color: var(--editor-theme-heading, var(--editor-theme-text)); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror td, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th { + + border-color: var(--editor-theme-border); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror { + + letter-spacing: 0.01em; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror p { + + line-height: 1.8; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: var(--editor-theme-accent); + font-weight: 700; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror em { + + color: var(--editor-theme-muted); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror a:not(.edgeever-attachment-link) { + + color: var(--editor-theme-accent); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--editor-theme-accent) 40%, transparent); + text-underline-offset: 0.15em; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror ul, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror ol { + + line-height: 1.75; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror li::marker { + + color: var(--editor-theme-accent); + font-weight: 700; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror hr { + + height: 2px; + border: 0; + background: var(--editor-theme-border); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror img { + + border: 1px solid var(--editor-theme-border); + border-radius: var(--editor-theme-radius, 6px); + box-shadow: var(--editor-theme-shadow, none); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .tableWrapper { + + border-color: var(--editor-theme-border); + border-radius: var(--editor-theme-radius, 6px); + box-shadow: var(--editor-theme-shadow, none); +} + +.edgeever-editor .ProseMirror .edgeever-theme-block { + + position: relative; + margin: 1.25rem 0; + border: 1px solid #dbeee5; + border-radius: 4px; + background: #fbfefc; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__toolbar { + + display: flex; + align-items: center; + justify-content: space-between; + min-height: 0; + max-height: 0; + overflow: hidden; + padding: 0; + border-bottom: 0; + color: #64748b; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + opacity: 0; + transition: max-height 140ms ease, padding 140ms ease, opacity 140ms ease; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__label { + + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__delete { + + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: #64748b; + cursor: pointer; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__content { + + padding: 0.35rem 0.9rem 0.45rem; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block:hover .edgeever-theme-block__toolbar, +.edgeever-editor .ProseMirror .edgeever-theme-block[data-theme-block-selected="true"] .edgeever-theme-block__toolbar { + + min-height: 2rem; + max-height: 2.5rem; + padding: 0.35rem 0.75rem; + border-bottom: 1px solid #e2eee8; + opacity: 1; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__content > :first-child { + + margin-top: 0; +} + +.edgeever-editor .ProseMirror .edgeever-theme-block__content > :last-child { + + margin-bottom: 0; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: var(--editor-theme-soft); + --theme-block-border: var(--editor-theme-border); + --theme-block-shadow: var(--editor-theme-shadow, none); + position: relative; + margin: 1.25rem 0; + border: 1px solid color-mix(in srgb, var(--theme-block-border) 58%, transparent); + border-radius: var(--editor-theme-radius, 4px); + background: color-mix(in srgb, var(--theme-block-bg) 42%, var(--editor-theme-bg)); + box-shadow: none; + color: var(--editor-theme-text); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__toolbar { + + display: flex; + align-items: center; + justify-content: space-between; + min-height: 0; + max-height: 0; + overflow: hidden; + padding: 0; + border-bottom: 0; + color: var(--editor-theme-accent); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + opacity: 0; + transition: max-height 140ms ease, padding 140ms ease, opacity 140ms ease; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__label { + + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__delete { + + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: var(--editor-theme-muted); + cursor: pointer; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__delete:hover { + + color: var(--editor-theme-accent); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__content { + + padding: 0.35rem 0.9rem 0.45rem; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block:hover .edgeever-theme-block__toolbar, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-selected="true"] .edgeever-theme-block__toolbar { + + min-height: 2rem; + max-height: 2.5rem; + padding: 0.35rem 0.75rem; + border-bottom: 1px solid color-mix(in srgb, var(--theme-block-border) 45%, transparent); + opacity: 1; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__content > :first-child { + + margin-top: 0; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__content > :last-child { + + margin-bottom: 0; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-selected="true"] { + + outline: 2px solid color-mix(in srgb, var(--editor-theme-accent) 42%, transparent); + outline-offset: 2px; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="intro"] { + + border-top: 0; + border-right: 0; + border-bottom: 0; + border-left: 3px solid var(--editor-theme-accent); + border-radius: 0; + background: transparent; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="key-point"] { + + border-right: 0; + border-bottom: 0; + border-left: 3px solid var(--editor-theme-accent); + border-radius: 0; + background: color-mix(in srgb, var(--theme-block-bg) 35%, var(--editor-theme-bg)); +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="callout"] { + + border-style: solid; + border-left-width: 3px; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="chapter"] { + + border-top: 3px solid var(--editor-theme-accent); + border-right: 0; + border-bottom: 0; + border-left: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block[data-theme-block-kind="chapter"] .edgeever-theme-block__toolbar { + + border-bottom-color: var(--editor-theme-border); + padding-left: 0; +} + +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th, +.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror td { + + font-family: var(--editor-theme-font, inherit); +} diff --git a/apps/web/src/styles/editor-themes/graphite-minimal.css b/apps/web/src/styles/editor-themes/graphite-minimal.css new file mode 100644 index 00000000..9003298f --- /dev/null +++ b/apps/web/src/styles/editor-themes/graphite-minimal.css @@ -0,0 +1,45 @@ +/* graphite-minimal editor theme */ + +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: #f4f4f5; + --theme-block-border: #a1a1aa; +} +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #fafafa; + --editor-theme-text: #3f3f46; + --editor-theme-accent: #52525b; + --editor-theme-soft: #f4f4f5; + --editor-theme-border: #d4d4d8; + --editor-theme-font: Inter, ui-sans-serif, system-ui, sans-serif; + --editor-theme-radius: 0; +} + +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border-left: 0; + border-bottom: 1px solid #a1a1aa; + padding-left: 0; + border-bottom-color: #52525b; +} + +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + font-size: 2.1rem; + letter-spacing: -0.03em; +} + +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: #27272a; + border-bottom: 2px solid #52525b; +} + +.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border-left: 1px solid #71717a; + background: #f4f4f5; + color: #52525b; +} diff --git a/apps/web/src/styles/editor-themes/index.css b/apps/web/src/styles/editor-themes/index.css new file mode 100644 index 00000000..20ce0793 --- /dev/null +++ b/apps/web/src/styles/editor-themes/index.css @@ -0,0 +1,8 @@ +@import "./base.css"; +@import "./moyu-green.css"; +@import "./red-white.css"; +@import "./graphite-minimal.css"; +@import "./zen-whitespace.css"; +@import "./moyu-ticket.css"; +@import "./olive-journal.css"; +@import "./mdnice-nenqing.css"; diff --git a/apps/web/src/styles/editor-themes/mdnice-nenqing.css b/apps/web/src/styles/editor-themes/mdnice-nenqing.css new file mode 100644 index 00000000..38c215b5 --- /dev/null +++ b/apps/web/src/styles/editor-themes/mdnice-nenqing.css @@ -0,0 +1,126 @@ +/* mdnice-nenqing editor theme */ + +/* Adapted from the visual rules of MDNice's "嫩青" editor theme. */ +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #ffffff; + --editor-theme-text: #595959; + --editor-theme-muted: #595959; + --editor-theme-heading: rgb(0 0 0 / 85%); + --editor-theme-accent: #47c1a8; + --editor-theme-soft: rgb(0 0 0 / 5%); + --editor-theme-border: #47c1a8; + --editor-theme-font: Optima, "Microsoft YaHei", "PingFang SC", serif; +} +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror { + + font-size: 1rem; + line-height: 1.8; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror p { + + margin: 0; + padding: 0.5rem 0; + line-height: 1.8; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h2, +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h3 { + + color: var(--editor-theme-heading); + font-family: var(--editor-theme-font); + font-weight: 700; + margin: 1.875rem 0 0.9375rem; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + font-size: 1.75rem; + line-height: 1.5; + border-bottom: 2px solid #595959; + padding-bottom: 0; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + font-size: 1.5rem; + line-height: 1.5; + border-left: 0; + border-bottom: 2px solid #595959; + padding-left: 0; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror h3 { + + font-size: 1.17rem; + line-height: 1.3; + border: 0; + padding-left: 0; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror strong, +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror a { + + color: var(--editor-theme-accent); +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + margin: 1.25rem 0; + border-left: 0; + border-radius: 0; + background: var(--editor-theme-soft); + color: var(--editor-theme-text); + padding: 0.625rem 0.625rem 0.625rem 1.25rem; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror ul, +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror ol { + + margin: 0.5rem 0; + padding-left: 1.5625rem; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror li { + + margin: 0; + line-height: 1.5; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror li::marker { + + color: var(--editor-theme-accent); + font-weight: 700; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror code { + + border: 0; + border-radius: 2px; + background: rgb(27 31 35 / 5%); + color: var(--editor-theme-accent); + font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; + font-size: 0.875rem; + padding: 0.125rem 0.25rem; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror pre { + + border: 0; + border-radius: 0.3125rem; + background: #f6f6f6; + color: #595959; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror hr { + + border-color: #808080; +} + +.edgeever-editor[data-editor-theme="mdnice-nenqing"]:not([data-editor-theme="default"]) .ProseMirror th { + + background: rgb(71 193 168 / 12%); + color: var(--editor-theme-heading); +} diff --git a/apps/web/src/styles/editor-themes/moyu-green.css b/apps/web/src/styles/editor-themes/moyu-green.css new file mode 100644 index 00000000..8aaff0f9 --- /dev/null +++ b/apps/web/src/styles/editor-themes/moyu-green.css @@ -0,0 +1,53 @@ +/* moyu-green editor theme */ + +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: #f0fdf4; + --theme-block-border: #bbf7d0; +} +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #fbfefc; + --editor-theme-text: #1f2937; + --editor-theme-accent: #059669; + --editor-theme-soft: #ecfdf5; + --editor-theme-border: #a7f3d0; + --editor-theme-radius: 10px; + --editor-theme-shadow: 0 2px 10px rgb(5 150 105 / 0.08); +} + +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + color: #111827; + border-bottom-color: #059669; + text-decoration: underline; + text-decoration-color: #fde68a; + text-decoration-thickness: 0.22em; + text-underline-offset: -0.08em; +} + +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border-left-width: 5px; + border-left-color: #059669; + background: linear-gradient(90deg, #ecfdf5, transparent); + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: #059669; + text-decoration: underline; + text-decoration-color: #a7f3d0; + text-decoration-thickness: 2px; + text-underline-offset: 0.16em; +} + +.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border: 1px dashed #6ee7b7; + border-left: 5px solid #059669; + border-radius: 8px; + box-shadow: 0 3px 10px rgb(5 150 105 / 0.07); +} diff --git a/apps/web/src/styles/editor-themes/moyu-ticket.css b/apps/web/src/styles/editor-themes/moyu-ticket.css new file mode 100644 index 00000000..193ba433 --- /dev/null +++ b/apps/web/src/styles/editor-themes/moyu-ticket.css @@ -0,0 +1,42 @@ +/* moyu-ticket editor theme */ + +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: #f0fdf4; + --theme-block-border: #6ee7b7; + --theme-block-shadow: 4px 4px 0 #a7f3d0; + border-style: dashed; +} +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #f7fffb; + --editor-theme-text: #1f2937; + --editor-theme-accent: #047857; + --editor-theme-soft: #d1fae5; + --editor-theme-border: #6ee7b7; + --editor-theme-radius: 2px; + --editor-theme-shadow: 4px 4px 0 #a7f3d0; +} + +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror blockquote, +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror pre { + + border: 2px solid var(--editor-theme-accent); + box-shadow: 4px 4px 0 #a7f3d0; +} + +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border: 1px dashed #6ee7b7; + background: #f0fdf4; + padding: 0.65rem 0.8rem; + box-shadow: 4px 4px 0 #a7f3d0; +} + +.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: #065f46; + background: #d1fae5; + padding: 0 0.25em; +} diff --git a/apps/web/src/styles/editor-themes/olive-journal.css b/apps/web/src/styles/editor-themes/olive-journal.css new file mode 100644 index 00000000..b79dcf9f --- /dev/null +++ b/apps/web/src/styles/editor-themes/olive-journal.css @@ -0,0 +1,65 @@ +/* olive-journal editor theme */ + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: #eeefe9; + --theme-block-border: #bfc1b7; +} +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__toolbar { + + background: #1e1f23; + color: #fff; +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #fffdf8; + --editor-theme-text: #292a2d; + --editor-theme-accent: #ed7b2f; + --editor-theme-soft: #fff3e8; + --editor-theme-border: #f3c7a6; + --editor-theme-font: Georgia, "Songti SC", STSong, serif; + --editor-theme-radius: 4px; + --editor-theme-shadow: 0 2px 8px rgb(30 31 35 / 0.06); +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + background: #292a2d; + color: #fffdf8; + border-bottom: 0; + padding: 0.7rem 0.9rem; +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border-left-color: var(--editor-theme-accent); +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h1, +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + color: #23251d; + border-bottom-color: #bfc1b7; +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h3 { + + color: #23251d; + box-shadow: inset 0 -0.5em 0 rgb(245 78 0 / 18%); + width: fit-content; +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: #23251d; + border-bottom: 2px solid #ed7b2f; +} + +.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border: 1px solid #bfc1b7; + border-left: 4px solid #ed7b2f; + background: #eeefe9; + color: #4d4f46; +} diff --git a/apps/web/src/styles/editor-themes/red-white.css b/apps/web/src/styles/editor-themes/red-white.css new file mode 100644 index 00000000..5a15926c --- /dev/null +++ b/apps/web/src/styles/editor-themes/red-white.css @@ -0,0 +1,52 @@ +/* red-white editor theme */ + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: #fef2f2; + --theme-block-border: #fca5a5; +} +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block__toolbar { + + background: #dc2626; + color: #fff; +} + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #ffffff; + --editor-theme-text: #262626; + --editor-theme-accent: #dc2626; + --editor-theme-soft: #fff1f2; + --editor-theme-border: #fecaca; + --editor-theme-radius: 6px; + --editor-theme-shadow: 0 4px 18px rgb(220 38 38 / 0.1); +} + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + border-bottom-width: 3px; + border-bottom-color: #dc2626; +} + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror h2 { + + border-left: 5px solid #dc2626; + border-bottom: 2px solid #fca5a5; + background: #fef2f2; + padding-top: 0.35rem; + padding-bottom: 0.35rem; +} + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror strong { + + color: #1c1917; + background: #fee2e2; + border-bottom: 2px solid #fecaca; + padding: 0 0.2em; +} + +.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border-left-color: #dc2626; + background: #fef2f2; +} diff --git a/apps/web/src/styles/editor-themes/zen-whitespace.css b/apps/web/src/styles/editor-themes/zen-whitespace.css new file mode 100644 index 00000000..735361cb --- /dev/null +++ b/apps/web/src/styles/editor-themes/zen-whitespace.css @@ -0,0 +1,45 @@ +/* zen-whitespace editor theme */ + +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror .edgeever-theme-block { + + --theme-block-bg: transparent; + --theme-block-border: #b5c8bc; +} +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) { + + --editor-theme-bg: #fcfdfc; + --editor-theme-text: #35443b; + --editor-theme-accent: #4a5d52; + --editor-theme-soft: #f1f6f2; + --editor-theme-border: #b5c8bc; + --editor-theme-font: Georgia, "Songti SC", STSong, serif; + --editor-theme-radius: 0; +} + +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror { + + padding-left: clamp(2rem, 9vw, 7rem); + padding-right: clamp(2rem, 9vw, 7rem); + line-height: 1.85; +} + +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror h1 { + + border-bottom: 1px solid var(--editor-theme-border); + font-weight: 600; + letter-spacing: 0.04em; +} + +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror p { + + margin-bottom: 0.75rem; + line-height: 2; +} + +.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror blockquote { + + border-left: 1px solid #b5c8bc; + background: transparent; + color: #4a5d52; + padding-left: 1.2rem; +} diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 85b1de8b..d2e2e224 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -1,4 +1,5 @@ @import "highlight.js/styles/github.css"; +@import "./editor-themes/index.css"; @tailwind base; @tailwind components; @@ -952,158 +953,6 @@ textarea { border-color: #334155; } -/* Editor reading themes inspired by the curated gzh-design-skill presets. */ -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) { - --editor-theme-bg: #ffffff; - --editor-theme-text: #1f2937; - --editor-theme-muted: #64748b; - --editor-theme-accent: #059669; - --editor-theme-soft: #ecfdf5; - --editor-theme-border: #a7f3d0; - background: var(--editor-theme-bg); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror { - background: var(--editor-theme-bg); - color: var(--editor-theme-text); - font-family: var(--editor-theme-font, inherit); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h1, -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h2, -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h3 { - color: var(--editor-theme-heading, var(--editor-theme-text)); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h1 { - border-bottom: 2px solid var(--editor-theme-accent); - padding-bottom: 0.55rem; -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left: 4px solid var(--editor-theme-accent); - padding-left: 0.7rem; -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror blockquote { - border-left: 4px solid var(--editor-theme-accent); - background: var(--editor-theme-soft); - color: var(--editor-theme-muted); - padding: 0.8rem 1rem; -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror hr { - border-color: var(--editor-theme-border); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror code, -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror pre, -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror .edgeever-code-source { - border-color: var(--editor-theme-border); - background: var(--editor-theme-soft); - color: var(--editor-theme-text); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th { - background: var(--editor-theme-soft); - color: var(--editor-theme-heading, var(--editor-theme-text)); -} - -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror td, -.edgeever-editor[data-editor-theme]:not([data-editor-theme="default"]) .ProseMirror th { - border-color: var(--editor-theme-border); -} - -.edgeever-editor[data-editor-theme="moyu-green"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #fbfefc; - --editor-theme-text: #1f2937; - --editor-theme-accent: #059669; - --editor-theme-soft: #ecfdf5; - --editor-theme-border: #a7f3d0; -} - -.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #ffffff; - --editor-theme-text: #262626; - --editor-theme-accent: #dc2626; - --editor-theme-soft: #fff1f2; - --editor-theme-border: #fecaca; -} - -.edgeever-editor[data-editor-theme="red-white"]:not([data-editor-theme="default"]) .ProseMirror h1 { - border-bottom-width: 3px; -} - -.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #fafafa; - --editor-theme-text: #3f3f46; - --editor-theme-accent: #52525b; - --editor-theme-soft: #f4f4f5; - --editor-theme-border: #d4d4d8; - --editor-theme-font: Inter, ui-sans-serif, system-ui, sans-serif; -} - -.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror h1, -.edgeever-editor[data-editor-theme="graphite-minimal"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left: 0; - border-bottom: 1px solid #a1a1aa; - padding-left: 0; -} - -.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #fcfdfc; - --editor-theme-text: #35443b; - --editor-theme-accent: #4a5d52; - --editor-theme-soft: #f1f6f2; - --editor-theme-border: #b5c8bc; - --editor-theme-font: Georgia, "Songti SC", STSong, serif; -} - -.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror { - padding-left: clamp(2rem, 9vw, 7rem); - padding-right: clamp(2rem, 9vw, 7rem); - line-height: 1.85; -} - -.edgeever-editor[data-editor-theme="zen-whitespace"]:not([data-editor-theme="default"]) .ProseMirror h1 { - border-bottom: 1px solid var(--editor-theme-border); - font-weight: 600; -} - -.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #f7fffb; - --editor-theme-text: #1f2937; - --editor-theme-accent: #047857; - --editor-theme-soft: #d1fae5; - --editor-theme-border: #6ee7b7; -} - -.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror blockquote, -.edgeever-editor[data-editor-theme="moyu-ticket"]:not([data-editor-theme="default"]) .ProseMirror pre { - border: 2px solid var(--editor-theme-accent); - box-shadow: 4px 4px 0 #a7f3d0; -} - -.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) { - --editor-theme-bg: #fffdf8; - --editor-theme-text: #292a2d; - --editor-theme-accent: #ed7b2f; - --editor-theme-soft: #fff3e8; - --editor-theme-border: #f3c7a6; - --editor-theme-font: Georgia, "Songti SC", STSong, serif; -} - -.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h1 { - background: #292a2d; - color: #fffdf8; - border-bottom: 0; - padding: 0.7rem 0.9rem; -} - -.edgeever-editor[data-editor-theme="olive-journal"]:not([data-editor-theme="default"]) .ProseMirror h2 { - border-left-color: var(--editor-theme-accent); -} - :root.dark .markdown-content pre { background-color: #13261a; border-color: #166534; diff --git a/bun.lock b/bun.lock index be8652c1..a9b3f4bf 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@radix-ui/react-toggle-group": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.10", "@tanstack/react-query": "latest", + "@tiptap/core": "3.27.1", "@tiptap/extension-image": "^3.27.1", "@tiptap/extension-placeholder": "latest", "@tiptap/extension-table": "3.27.1", diff --git a/package.json b/package.json index 145321d9..0f18bdba 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@radix-ui/react-toggle-group": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.10", "@tanstack/react-query": "latest", + "@tiptap/core": "3.27.1", "@tiptap/extension-image": "^3.27.1", "@tiptap/extension-placeholder": "latest", "@tiptap/extension-table": "3.27.1", diff --git a/packages/shared/src/content.test.mjs b/packages/shared/src/content.test.mjs index a972c04d..d07d0fbd 100644 --- a/packages/shared/src/content.test.mjs +++ b/packages/shared/src/content.test.mjs @@ -72,3 +72,33 @@ describe("Mermaid Markdown conversion", () => { expect(docToMarkdown(doc)).toBe(markdown); }); }); + +describe("Theme block compatibility", () => { + test("keeps themed blocks in the richer JSON document when Markdown is also present", () => { + const doc = { + type: "doc", + content: [{ + type: "edgeeverThemeBlock", + attrs: { kind: "key-point" }, + content: [{ type: "paragraph", content: [{ type: "text", text: "Important" }] }], + }], + }; + + expect(resolveMemoContentDoc(doc, "Important")).toBe(doc); + }); + + test("exports themed blocks as readable quoted Markdown instead of dropping their text", () => { + const doc = { + type: "doc", + content: [{ + type: "edgeeverThemeBlock", + attrs: { kind: "intro" }, + content: [{ type: "paragraph", content: [{ type: "text", text: "Read this first" }] }], + }], + }; + + const markdown = docToMarkdown(doc); + expect(markdown).toContain("\\[intro\\]"); + expect(markdown).toContain("Read this first"); + }); +}); diff --git a/packages/shared/src/content.ts b/packages/shared/src/content.ts index 139d1ede..9d4ee765 100644 --- a/packages/shared/src/content.ts +++ b/packages/shared/src/content.ts @@ -72,7 +72,7 @@ export const resolveMemoContentDoc = ( const currentDoc = contentJson && Array.isArray(contentJson.content) ? upgradeLegacyAttachmentLinks(contentJson) : emptyDoc(); - if (!contentMarkdown?.trim() || docContainsNodeType(currentDoc, "table")) { + if (!contentMarkdown?.trim() || docContainsNodeType(currentDoc, "table") || docContainsNodeType(currentDoc, "edgeeverThemeBlock")) { return currentDoc; } @@ -215,7 +215,36 @@ export const docToMarkdown = (doc: unknown): string => { return ""; } - return markdownManager.serialize(doc as Parameters[0]); + return markdownManager.serialize(stripEditorOnlyNodes(doc) as Parameters[0]); +}; + +/** + * Theme blocks are richer editor-only nodes. Markdown has no portable equivalent, + * so exports keep their text as a quoted section instead of silently dropping it. + */ +const stripEditorOnlyNodes = (doc: unknown): unknown => { + if (!doc || typeof doc !== "object") { + return doc; + } + + const node = doc as { type?: unknown; attrs?: Record; content?: unknown }; + if (node.type === "edgeeverThemeBlock") { + const label = getStringAttr(node.attrs, "kind"); + const content = Array.isArray(node.content) ? node.content : []; + return { + type: "blockquote", + content: [ + { type: "paragraph", content: [{ type: "text", text: label ? `[${label}]` : "[主题化组件]" }] }, + ...content.map(stripEditorOnlyNodes), + ], + }; + } + + if (!Array.isArray(node.content)) { + return doc; + } + + return { ...node, content: node.content.map(stripEditorOnlyNodes) }; }; const getStringAttr = (attrs: Record | undefined, key: string) => { diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index dd926f83..e5b3f92d 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -157,6 +157,7 @@ export const enUS = { "zen-whitespace": "Zen Whitespace", "moyu-ticket": "Moyu Ticket", "olive-journal": "Olive Journal", + "mdnice-nenqing": "Tender Green (MDNice)", }, mermaidThemes: { "zinc-light": "Zinc Light", @@ -531,6 +532,9 @@ export const enUS = { typeInput: "Type", clipboardReadFailed: "Could not read the clipboard. Check that your browser allows clipboard access.", characterCount: "{{count}} chars", + copyToWeChat: "Copy to WeChat", + copiedToWeChat: "Copied", + copyToWeChatFailed: "Copy failed", }, editorToolbar: { toolbar: "Editor toolbar", @@ -557,6 +561,14 @@ export const enUS = { mermaidEmpty: "Enter Mermaid source below to generate a diagram.", mermaidSource: "Mermaid source", horizontalRule: "Divider", + themeBlock: "Themed block", + deleteThemeBlock: "Delete themed block", + themeBlocks: { + intro: "Intro card", + "key-point": "Key point card", + callout: "Callout block", + chapter: "Chapter heading", + }, table: "Table", insertTable: "Insert 3 × 3 table", addTableRow: "Add row below", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index ea6508b2..79bb314f 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -157,6 +157,7 @@ export const zhCN = { "zen-whitespace": "留白禅意风", "moyu-ticket": "摸鱼票据风", "olive-journal": "橄榄手记", + "mdnice-nenqing": "嫩青(MDNice)", }, mermaidThemes: { "zinc-light": "Zinc 浅色", @@ -531,6 +532,9 @@ export const zhCN = { typeInput: "输入", clipboardReadFailed: "读取剪贴板失败。请确认浏览器允许剪贴板权限。", characterCount: "{{count}} 字", + copyToWeChat: "复制到公众号", + copiedToWeChat: "已复制", + copyToWeChatFailed: "复制失败", }, editorToolbar: { toolbar: "编辑器工具栏", @@ -557,6 +561,14 @@ export const zhCN = { mermaidEmpty: "在下方输入 Mermaid 源码以生成图表。", mermaidSource: "Mermaid 源码", horizontalRule: "分割线", + themeBlock: "主题化组件", + deleteThemeBlock: "删除主题化组件", + themeBlocks: { + intro: "引言卡片", + "key-point": "重点观点卡", + callout: "提示块", + chapter: "章节标题块", + }, table: "表格", insertTable: "插入 3 × 3 表格", addTableRow: "在下方添加行",