Add GitHub-style Markdown editor to PublishModal

This commit is contained in:
CPTProgrammer 2026-07-17 16:09:03 +08:00
parent 659789b0dc
commit 758d5c0207
No known key found for this signature in database
2 changed files with 159 additions and 15 deletions

View File

@ -0,0 +1,157 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import { theme } from "antd";
import { commands, type ICommand } from "@uiw/react-md-editor";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";
// @uiw/react-md-editor accesses browser APIs on import — skip SSR
const MDEditor = dynamic(() => import("@uiw/react-md-editor"), {
ssr: false,
});
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type EditorMode = "edit" | "preview";
interface ChangelogEditorProps {
value: string;
onChange: (value: string) => void;
}
// ---------------------------------------------------------------------------
// GitHub-style Code / Preview tabs
// ---------------------------------------------------------------------------
function ModeTab({
label,
active,
onClick,
}: {
label: string;
active: boolean;
onClick: () => void;
}) {
const { token } = theme.useToken();
return (
<span
style={{
display: "inline-block",
padding: `2px ${token.paddingSM}px`,
fontSize: token.fontSizeSM,
fontWeight: active ? 600 : 400,
color: active ? token.colorPrimary : token.colorTextSecondary,
borderBottom: active
? `2px solid ${token.colorPrimary}`
: "2px solid transparent",
cursor: "pointer",
userSelect: "none",
}}
onClick={onClick}
>
{label}
</span>
);
}
function buildTabCommand(
key: string,
label: string,
active: boolean,
onSwitch: () => void,
): ICommand {
return {
name: key,
keyCommand: key,
icon: <ModeTab label={label} active={active} onClick={onSwitch} />,
buttonProps: null,
execute: (state, api, dispatch) => {
dispatch?.({ preview: key === "preview" ? "preview" : "edit" });
},
};
}
// Formatting commands, like GitHub's toolbar — hidden in Preview mode
const FORMAT_COMMANDS: ICommand[] = [
commands.bold,
commands.italic,
commands.strikethrough,
commands.divider,
commands.link,
commands.code,
commands.codeBlock,
commands.image,
commands.divider,
commands.unorderedListCommand,
commands.orderedListCommand,
commands.checkedListCommand,
commands.quote,
commands.table,
];
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function ChangelogEditor({ value, onChange }: ChangelogEditorProps) {
const { token } = theme.useToken();
const [mode, setMode] = useState<EditorMode>("edit");
const toolbarCommands: ICommand[] =
mode === "edit"
? [
buildTabCommand("code", "Code", true, () => setMode("edit")),
buildTabCommand("preview", "Preview", false, () => setMode("preview")),
commands.divider,
...FORMAT_COMMANDS,
]
: [
buildTabCommand("code", "Code", false, () => setMode("edit")),
buildTabCommand("preview", "Preview", true, () => setMode("preview")),
];
return (
<div
data-color-mode="light"
style={{
// Map the editor's GitHub-flavored CSS variables onto Ant Design tokens
// so the preview/toolbar visually merge into the surrounding UI.
["--md-editor-background-color" as string]: token.colorBgContainer,
["--md-editor-box-shadow-color" as string]: token.colorBorder,
["--md-editor-font-family" as string]: token.fontFamily,
["--color-fg-default" as string]: token.colorText,
["--color-fg-muted" as string]: token.colorTextSecondary,
["--color-canvas-default" as string]: token.colorBgContainer,
["--color-canvas-subtle" as string]: token.colorFillQuaternary,
["--color-border-default" as string]: token.colorBorder,
["--color-border-muted" as string]: token.colorBorderSecondary,
["--color-neutral-muted" as string]: token.colorFillSecondary,
["--color-accent-fg" as string]: token.colorPrimary,
["--color-accent-emphasis" as string]: token.colorPrimaryActive,
}}
>
<MDEditor
value={value}
onChange={(val) => onChange(val ?? "")}
preview={mode}
height={520}
minHeight={400}
maxHeight={900}
visibleDragbar={false}
textareaProps={{
placeholder: "输入 Markdown 格式的更新日志…",
}}
commands={toolbarCommands}
extraCommands={[]}
style={{
borderRadius: token.borderRadius,
fontFamily: token.fontFamily,
}}
/>
</div>
);
}

View File

@ -25,6 +25,7 @@ import { Template } from "@/lib/utils/template";
import type { McVersionEntry } from "@/lib/utils/mcVersion";
import { computeVersionRanges } from "@/lib/utils/mcVersion";
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
import { ChangelogEditor } from "./ChangelogEditor";
const { Text } = Typography;
@ -384,21 +385,7 @@ export function PublishModal({
<Text strong style={{ display: "block", marginBottom: token.marginSM }}>
Changelog (Markdown)
</Text>
{/* TODO: Replace with @uiw/react-md-editor */}
<textarea
style={{
width: "100%",
minHeight: 160,
border: `1px solid ${token.colorBorder}`,
borderRadius: token.borderRadius,
padding: token.paddingSM,
fontFamily: token.fontFamilyCode ?? "monospace",
fontSize: token.fontSizeSM,
}}
placeholder="输入 Markdown 格式的更新日志…"
value={changelog}
onChange={(e) => setChangelog(e.target.value)}
/>
<ChangelogEditor value={changelog} onChange={setChangelog} />
</div>
{/* Version type */}