feat: use tailwindcss-typography as markdown styling

This commit is contained in:
Jiang Feng
2025-04-27 14:19:45 +08:00
parent 741e2f0e62
commit 906df61d81
9 changed files with 140 additions and 220 deletions

View File

@@ -99,7 +99,7 @@ export function AISelector({ onOpenChange }: AISelectorProps) {
{hasCompletion && (
<div className="flex max-h-[400px]">
<ScrollArea>
<div className="prose prose-sm p-2 px-4">
<div className="prose prose-sm dark:prose-invert p-2 px-4">
<Markdown>{completion}</Markdown>
</div>
</ScrollArea>

View File

@@ -38,12 +38,12 @@ const extensions = [...defaultExtensions, slashCommand];
export interface ReportEditorProps {
content: Content;
onMarkdownChange?: (markdown: string) => void;
}
const ReportEditor = ({ content }: ReportEditorProps) => {
const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => {
const [initialContent, setInitialContent] = useState<Content>(() => content);
const [saveStatus, setSaveStatus] = useState("Saved");
const [charsCount, setCharsCount] = useState();
const [openNode, setOpenNode] = useState(false);
const [openColor, setOpenColor] = useState(false);
@@ -63,17 +63,21 @@ const ReportEditor = ({ content }: ReportEditorProps) => {
const debouncedUpdates = useDebouncedCallback(
async (editor: EditorInstance) => {
const json = editor.getJSON();
setCharsCount(editor.storage.characterCount.words());
window.localStorage.setItem(
"html-content",
highlightCodeblocks(editor.getHTML()),
);
window.localStorage.setItem("novel-content", JSON.stringify(json));
window.localStorage.setItem(
"markdown",
editor.storage.markdown.getMarkdown(),
);
// const json = editor.getJSON();
// // setCharsCount(editor.storage.characterCount.words());
// window.localStorage.setItem(
// "html-content",
// highlightCodeblocks(editor.getHTML()),
// );
// window.localStorage.setItem("novel-content", JSON.stringify(json));
// window.localStorage.setItem(
// "markdown",
// editor.storage.markdown.getMarkdown(),
// );
if (onMarkdownChange) {
const markdown = editor.storage.markdown.getMarkdown();
onMarkdownChange(markdown);
}
setSaveStatus("Saved");
},
500,
@@ -89,26 +93,12 @@ const ReportEditor = ({ content }: ReportEditorProps) => {
return (
<div className="relative w-full">
<div className="absolute top-5 right-5 z-10 mb-5 flex gap-2">
<div className="bg-accent text-muted-foreground rounded-lg px-2 py-1 text-sm">
{saveStatus}
</div>
<div
className={
charsCount
? "bg-accent text-muted-foreground rounded-lg px-2 py-1 text-sm"
: "hidden"
}
>
{charsCount} Words
</div>
</div>
<EditorRoot>
<EditorContent
immediatelyRender={false}
initialContent={initialContent as JSONContent}
extensions={extensions}
className="border-muted bg-background relative h-full w-full overflow-auto sm:mb-[calc(20vh)] sm:border sm:shadow-lg"
className="border-muted relative h-full w-full"
editorProps={{
handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event),

View File

@@ -1,32 +1,18 @@
import {
Brain,
CheckSquare,
Code,
Heading1,
Heading2,
Heading3,
ImageIcon,
List,
ListOrdered,
MessageSquarePlus,
Text,
TextQuote,
Twitter,
Youtube,
} from "lucide-react";
import { Command, createSuggestionItems, renderItems } from "novel";
import { uploadFn } from "./image-upload";
// import { uploadFn } from "./image-upload";
export const suggestionItems = createSuggestionItems([
{
title: "Send Feedback",
description: "Let us know how we can improve.",
icon: <MessageSquarePlus size={18} />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
window.open("/feedback", "_blank");
},
},
{
title: "Text",
description: "Just start typing with plain text.",
@@ -132,83 +118,83 @@ export const suggestionItems = createSuggestionItems([
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
// upload image
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
if (!file) return;
const pos = editor.view.state.selection.from;
uploadFn(file, editor.view, pos);
}
};
input.click();
},
},
{
title: "Youtube",
description: "Embed a Youtube video.",
searchTerms: ["video", "youtube", "embed"],
icon: <Youtube size={18} />,
command: ({ editor, range }) => {
const videoLink = prompt("Please enter Youtube Video Link");
//From https://regexr.com/3dj5t
const ytRegex = new RegExp(
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/,
);
// {
// title: "Image",
// description: "Upload an image from your computer.",
// searchTerms: ["photo", "picture", "media"],
// icon: <ImageIcon size={18} />,
// command: ({ editor, range }) => {
// editor.chain().focus().deleteRange(range).run();
// // upload image
// const input = document.createElement("input");
// input.type = "file";
// input.accept = "image/*";
// input.onchange = async () => {
// if (input.files?.length) {
// const file = input.files[0];
// if (!file) return;
// const pos = editor.view.state.selection.from;
// uploadFn(file, editor.view, pos);
// }
// };
// input.click();
// },
// },
// {
// title: "Youtube",
// description: "Embed a Youtube video.",
// searchTerms: ["video", "youtube", "embed"],
// icon: <Youtube size={18} />,
// command: ({ editor, range }) => {
// const videoLink = prompt("Please enter Youtube Video Link");
// //From https://regexr.com/3dj5t
// const ytRegex = new RegExp(
// /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/,
// );
if (videoLink && ytRegex.test(videoLink)) {
editor
.chain()
.focus()
.deleteRange(range)
.setYoutubeVideo({
src: videoLink,
})
.run();
} else {
if (videoLink !== null) {
alert("Please enter a correct Youtube Video Link");
}
}
},
},
{
title: "Twitter",
description: "Embed a Tweet.",
searchTerms: ["twitter", "embed"],
icon: <Twitter size={18} />,
command: ({ editor, range }) => {
const tweetLink = prompt("Please enter Twitter Link");
const tweetRegex = new RegExp(
/^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/,
);
// if (videoLink && ytRegex.test(videoLink)) {
// editor
// .chain()
// .focus()
// .deleteRange(range)
// .setYoutubeVideo({
// src: videoLink,
// })
// .run();
// } else {
// if (videoLink !== null) {
// alert("Please enter a correct Youtube Video Link");
// }
// }
// },
// },
// {
// title: "Twitter",
// description: "Embed a Tweet.",
// searchTerms: ["twitter", "embed"],
// icon: <Twitter size={18} />,
// command: ({ editor, range }) => {
// const tweetLink = prompt("Please enter Twitter Link");
// const tweetRegex = new RegExp(
// /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/,
// );
if (tweetLink && tweetRegex.test(tweetLink)) {
editor
.chain()
.focus()
.deleteRange(range)
.setTweet({
src: tweetLink,
})
.run();
} else {
if (tweetLink !== null) {
alert("Please enter a correct Twitter Link");
}
}
},
},
// if (tweetLink && tweetRegex.test(tweetLink)) {
// editor
// .chain()
// .focus()
// .deleteRange(range)
// .setTweet({
// src: tweetLink,
// })
// .run();
// } else {
// if (tweetLink !== null) {
// alert("Please enter a correct Twitter Link");
// }
// }
// },
// },
]);
export const slashCommand = Command.configure({