Skip to content

Commit

Permalink
feat: 에디터 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
XionWCFM committed Dec 10, 2024
1 parent c88396b commit 2c701c6
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 44 deletions.
32 changes: 19 additions & 13 deletions apps/blog/app/(admin)/write/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
"use client";

import { MdxViewer } from "@repo/mdx";
import { MdxEditor } from "@repo/mdx/editor";
import { Debounce } from "@xionwcfm/react";
import { Flex, Stack } from "@xionwcfm/xds";
import dynamic from "next/dynamic";
import { useState } from "react";

const MdxViewer = dynamic(() => import("@repo/mdx").then((mod) => mod.MdxViewer), { ssr: false });

export default function Page() {
return (
<div>
<MdxViewer
source={`
## Hello World
const [markdown, setMarkdown] = useState("");

\`\`\`js {highlight=1}
const foo = "bar";
console.log(foo);
\`\`\`
`}
/>
</div>
return (
<Flex className="p-32" w={"screen"} h={"screen"} gap={"16"}>
<Stack className="w-full">
<MdxEditor content={markdown} onChange={(value) => setMarkdown(value)} />
</Stack>
<Stack className="w-full border rounded-sm px-16">
<Debounce value={markdown} delay={2500}>
{({ debounced }) => <MdxViewer source={debounced} />}
</Debounce>
</Stack>
</Flex>
);
}
6 changes: 6 additions & 0 deletions packages/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"version": "0.0.0",
"private": true,
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./editor": "./src/MdxEditor.tsx"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "22.10.1",
Expand All @@ -17,9 +21,11 @@
"@mdx-js/loader": "^3.1.0",
"@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@monaco-editor/react": "^4.6.0",
"@next/mdx": "^15.0.3",
"@types/mdx": "^2.0.13",
"markdown-wasm": "^1.2.0",
"monaco-editor": "^0.52.2",
"next-mdx-remote": "^5.0.0",
"react": "catalog:react18",
"react-dom": "catalog:react18",
Expand Down
2 changes: 1 addition & 1 deletion packages/mdx/src/CallOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type CallOutProps = {
export const CallOut = ({ variant, children }: CallOutProps) => {
return (
<div
className={`px-32 py-24 flex bg-success-600 rounded-md flex-col gap-y-16 list-disc font-regular text-neutral-600`}
className={`px-32 py-24 flex bg-primary-alpha-50 text-primary-600 rounded-md flex-col gap-y-16 list-disc font-regular`}
>
{variant} {children}
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/mdx/src/MdxComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ export const MdxComponents: tmdxComponents = {
<Paragraph mt="20" mb="12" as={"h6"} size={"6"} weight={"bold"} color={"neutral-700"} {...props} />
),
p: ({ className, color, ...props }: ParagraphProps) => (
<Paragraph size={"5"} mt="24" className=" leading-[240%]" weight={"light"} color={"neutral-600"} {...props} />
<Paragraph size={"5"} mt="8" weight={"light"} leading={"tight"} color={"neutral-600"} {...props} />
),
pre: ({ className, ...props }: PreProps) => (
<pre className={" my-24 overflow-x-auto rounded-sm bg-primary-50 px-24 py-12 text-neutral-700"} {...props} />
<pre className={" my-16 overflow-x-auto rounded-sm bg-primary-50 px-24 py-12 text-neutral-700"} {...props} />
),
hr: (_props) => <hr className=" border-t border-neutral-300 my-24" />,
code: ({ className, ...props }: ElementProps) => (
Expand All @@ -83,6 +83,7 @@ export const MdxComponents: tmdxComponents = {
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className={"m-0 border-t p-0 even:bg-muted"} {...props} />
),
br: () => null,
th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<th
className={"border px-8 py-4 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"}
Expand Down
107 changes: 107 additions & 0 deletions packages/mdx/src/MdxEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import MonacoEditor, { useMonaco } from "@monaco-editor/react";
import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api";
import type React from "react";
import { useEffect } from "react";

interface Props {
content: string;
onChange: (value: string) => void;
}

export const MdxEditor: React.FC<Props> = ({ content, onChange }) => {
const monaco = useMonaco();

useEffect(() => {
if (monaco) {
const customComponents = [
{
label: "CallOut",
detail: "CallOut component for important messages",
insertText: '<CallOut type="${1|info,warning,error|}">\n $0\n</CallOut>',
documentation: "Available types: info, warning, error",
},
{
label: "Paragraph",
detail: "Paragraph component with various text styling options",
insertText: [
"<Paragraph",
' overflow="${1|default,ellipsis|}"',
' leading="${2|default,denser,normal,tight,loose,looser|}"',
' weight="${3|default,bold,semi-bold,medium,regular,light,thin|}"',
' color="${4|default,white,neutral-50,neutral-100,neutral-200,neutral-300,neutral-400,neutral-500,neutral-600,neutral-700,neutral-800,neutral-900,neutral-950,gray-50,gray-100,gray-200,gray-300,gray-400,gray-500,gray-600,gray-700,gray-800,gray-900,gray-950,warning-50,warning-100,warning-200,warning-300,warning-400,warning-500,warning-600,warning-700,warning-800,warning-900,warning-950,danger-50,danger-100,danger-200,danger-300,danger-400,danger-500,danger-600,danger-700,danger-800,danger-900,danger-950,success-50,success-100,success-200,success-300,success-400,success-500,success-600,success-700,success-800,success-900,success-950,primary-50,primary-100,primary-200,primary-300,primary-400,primary-500,primary-600,primary-700,primary-800,primary-900,primary-950|}"',
' size="${5|xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,8xl,9xl|}"',
' responsive="${6|true,false|}"',
">",
" $0",
"</Paragraph>",
].join("\n"),
documentation: `Available props:
- overflow: default, ellipsis
- leading: default, denser, normal, tight, loose, looser
- weight: default, bold, semi-bold, medium, regular, light, thin
- color: default, white, neutral-[50-950], gray-[50-950], warning-[50-950], danger-[50-950], success-[50-950], primary-[50-950]
- size: xs, sm, base, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl, 8xl, 9xl
- responsive: boolean
- className: string
- children: ReactNode`,
},
];

monaco.languages.registerCompletionItemProvider("markdown", {
provideCompletionItems: (model: Monaco.editor.ITextModel, position: Monaco.Position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};

return {
suggestions: customComponents.map((component) => ({
label: component.label,
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: component.insertText,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
detail: component.detail,
documentation: component.documentation,
range: range,
})),
};
},
});
}
}, [monaco]);

return (
<MonacoEditor
height="100%"
defaultLanguage="markdown"
value={content}
onChange={(value) => onChange(value ?? "")}
theme={"vs-light"}
options={{
minimap: { enabled: false },
wordWrap: "on",
lineNumbers: "on",
folding: true,
fontSize: 14,
suggestOnTriggerCharacters: true,
quickSuggestions: true,
acceptSuggestionOnEnter: "off",
renderLineHighlight: "none",
occurrencesHighlight: "off",
lineHeight: 24,
lineDecorationsWidth: 0,
renderWhitespace: "none",
autoClosingBrackets: "never",
autoClosingQuotes: "never",
autoSurround: "never",
unicodeHighlight: {
ambiguousCharacters: false,
invisibleCharacters: false,
},
}}
/>
);
};
50 changes: 22 additions & 28 deletions packages/mdx/src/MdxViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { evaluate } from "@mdx-js/mdx";
import { MDXProvider } from "@mdx-js/react";
import type React from "react";
"use client";
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import { useEffect, useState } from "react";
import * as runtime from "react/jsx-runtime";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeSlug from "rehype-slug";
import remarkBreaks from "remark-breaks";
import { MdxComponents } from "./MdxComponents";

export const MdxViewer = ({ source }: { source: string }) => {
const [Content, setContent] = useState<React.FC | null>(null);
interface Props {
source: string;
}

export const MdxViewer = (props: Props) => {
const { source } = props;
const [content, setContent] = useState<MDXRemoteSerializeResult | null>(null);

useEffect(() => {
const compileMdx = async () => {
//@ts-expect-error
const { default: mdxContent } = await evaluate(source, {
...runtime,
// biome-ignore lint/style/useNamingConvention: <explanation>
useMDXComponents: () => MdxComponents,
serialize(source, {
mdxOptions: {
remarkPlugins: [remarkBreaks],
rehypePlugins: [
rehypeSlug,
Expand All @@ -35,33 +35,27 @@ export const MdxViewer = ({ source }: { source: string }) => {
rehypePrettyCode,
{
theme: "slack-dark",
//@ts-expect-error
onVisitLine(node) {
onVisitLine(node: any) {
if (node.children.length === 0) {
node.children = [{ type: "text", value: " " }];
}
},
//@ts-expect-error
onVisitHighlightedLine(node) {
onVisitHighlightedLine(node: any) {
node.properties.className.push("line--highlighted");
},
//@ts-expect-error
onVisitHighlightedWord(node) {
onVisitHighlightedWord(node: any) {
node.properties.className = ["word--highlighted"];
},
},
],
],
});
setContent(() => mdxContent);
};

compileMdx();
},
}).then((res) => setContent(res));
}, [source]);

return Content ? (
<MDXProvider components={MdxComponents}>
<Content />
</MDXProvider>
) : null;
if (!content) {
return null;
}

return <MDXRemote {...content} components={MdxComponents} />;
};
40 changes: 40 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2c701c6

Please sign in to comment.