Add themed editor blocks and split theme styles

This commit is contained in:
tianma-if committed 2026-07-23 17:54:39 +08:00
1 parent 2e5bf5063e
commit a5222fbddd
24 files changed
+1145 -154

No files matched your search

+1
View File
@@ -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
+1
View File
@@ -240,6 +240,7 @@ Cloudflare Worker 侧执行图片处理会消耗计算/图片处理额度,因
## 致谢
- 编辑器主题的视觉设计参考自 [gzh-design-skill](https://github.com/isjiamu/gzh-design-skill),相关主题由甲木 × 摸鱼小李原创。感谢作者的开源工作。
- `嫩青`编辑器主题的视觉样式参考自 [MDNice 编辑器](https://editor.mdnice.com/)。感谢原作者提供的设计参考。
## 免责声明
+35
View File
@@ -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<MobileImeDebugEntry[]>([]);
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 = ({
<Button className="hidden h-8 w-8 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-950 focus-visible:ring-2 focus-visible:ring-slate-300 sm:inline-flex" size="icon" variant="ghost" title={t("editor.searchCurrentMemo")} aria-label={t("editor.searchCurrentMemo")} onClick={() => openNoteSearch()}>
<Search className="h-5 w-5" strokeWidth={2.25} />
</Button>
<Button
className={cn("hidden h-8 items-center gap-1.5 px-2 text-xs font-medium text-emerald-700 transition-colors hover:bg-emerald-50 hover:text-emerald-900 focus-visible:ring-2 focus-visible:ring-emerald-300 sm:inline-flex", wechatCopyState === "copied" && "bg-emerald-50")}
variant="ghost"
title={t("editor.copyToWeChat")}
aria-label={t("editor.copyToWeChat")}
onClick={() => void handleCopyToWeChat()}
disabled={!editor || effectiveReadOnly || useMobilePlainTextEditor}
>
<Copy className="h-4 w-4" />
{t(wechatCopyState === "copied" ? "editor.copiedToWeChat" : wechatCopyState === "error" ? "editor.copyToWeChatFailed" : "editor.copyToWeChat")}
</Button>
<Button className="hidden h-8 w-8 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-950 focus-visible:ring-2 focus-visible:ring-slate-300 sm:inline-flex" size="icon" variant="ghost" title={t("editor.versionHistory")} aria-label={t("editor.versionHistory")} onClick={() => setHistoryOpen(true)}>
<History className="h-5 w-5" strokeWidth={2.25} />
</Button>
+29
View File
@@ -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 = ({
>
<Minus className="h-4 w-4" />
</EditorToolbarButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-transparent bg-transparent text-slate-700 transition hover:border-slate-200 hover:bg-slate-50 disabled:pointer-events-none disabled:opacity-40"
aria-label={t("editorToolbar.themeBlock")}
title={t("editorToolbar.themeBlock")}
disabled={disabled}
onMouseDown={(event) => event.preventDefault()}
>
<Blocks className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{(["intro", "key-point", "callout", "chapter"] as ThemeBlockKind[]).map((kind) => (
<DropdownMenuItem key={kind} onSelect={() => run((current) => insertThemeBlock(current, kind))}>
{t(`editorToolbar.themeBlocks.${kind}`)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<EditorTableMenu editor={editor} readOnly={readOnly} />
</>
)}
@@ -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,
+96
View File
@@ -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<ThemeBlockKind, typeof MessageCircle>;
const ThemeBlockView = ({ node, deleteNode, selected }: NodeViewProps) => {
const { t } = useTranslation();
const kind = (node.attrs.kind as ThemeBlockKind) || "intro";
const Icon = kindIcon[kind] ?? MessageCircle;
return (
<NodeViewWrapper
className="edgeever-theme-block"
data-theme-block-kind={kind}
data-theme-block-selected={selected ? "true" : undefined}
>
<div className="edgeever-theme-block__toolbar" contentEditable={false}>
<span className="edgeever-theme-block__label">
<Icon aria-hidden="true" className="h-3.5 w-3.5" />
{t(`editorToolbar.themeBlocks.${kind}`)}
</span>
<button
type="button"
className="edgeever-theme-block__delete"
aria-label={t("editorToolbar.deleteThemeBlock")}
title={t("editorToolbar.deleteThemeBlock")}
onMouseDown={(event) => event.preventDefault()}
onClick={deleteNode}
>
<Trash2 aria-hidden="true" className="h-3.5 w-3.5" />
</button>
</div>
<NodeViewContent className="edgeever-theme-block__content" />
</NodeViewWrapper>
);
};
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();
};
@@ -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];
+116
View File
@@ -0,0 +1,116 @@
import type { Editor } from "@tiptap/react";
import { marked } from "marked";
const WECHAT_STYLES: Record<string, string> = {
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<string, string> = {
intro: "引言",
"key-point": "重点观点",
callout: "提示",
chapter: "章节",
};
const THEME_BLOCK_STYLES: Record<string, { block: string; label: string }> = {
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<HTMLElement>("*").forEach((element) => {
const style = WECHAT_STYLES[element.tagName.toLowerCase()];
if (style) element.style.cssText = `${style}${element.style.cssText}`;
});
root.querySelectorAll<HTMLElement>("pre code").forEach((element) => {
element.style.cssText = "padding: 0; background: transparent; color: inherit; font-family: Menlo, Consolas, monospace; font-size: 13px; white-space: pre-wrap;";
});
root.querySelectorAll<HTMLElement>("[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<HTMLElement>("[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 ?? "");
};
+340
View File
@@ -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);
}
@@ -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;
}
@@ -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";
@@ -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);
}
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
+1 -152
View File
@@ -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;
+1
View File
@@ -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",
+1
View File
@@ -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",
+30
View File
@@ -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");
});
});
+31 -2
View File
@@ -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<typeof markdownManager.serialize>[0]);
return markdownManager.serialize(stripEditorOnlyNodes(doc) as Parameters<typeof markdownManager.serialize>[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<string, unknown>; 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<string, unknown> | undefined, key: string) => {
+12
View File
@@ -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",
+12
View File
@@ -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: "在下方添加行",