From 2c701c6fe6edb5a90fd0b98c43e3ac6fb0172f4d Mon Sep 17 00:00:00 2001 From: devgiljong Date: Wed, 11 Dec 2024 00:50:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=90=EB=94=94=ED=84=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/blog/app/(admin)/write/page.tsx | 32 ++++---- packages/mdx/package.json | 6 ++ packages/mdx/src/CallOut.tsx | 2 +- packages/mdx/src/MdxComponents.tsx | 5 +- packages/mdx/src/MdxEditor.tsx | 107 +++++++++++++++++++++++++++ packages/mdx/src/MdxViewer.tsx | 50 ++++++------- pnpm-lock.yaml | 40 ++++++++++ 7 files changed, 198 insertions(+), 44 deletions(-) create mode 100644 packages/mdx/src/MdxEditor.tsx diff --git a/apps/blog/app/(admin)/write/page.tsx b/apps/blog/app/(admin)/write/page.tsx index cb536dc..6462e1f 100644 --- a/apps/blog/app/(admin)/write/page.tsx +++ b/apps/blog/app/(admin)/write/page.tsx @@ -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 ( -
- -
+ return ( + + + setMarkdown(value)} /> + + + + {({ debounced }) => } + + + ); } diff --git a/packages/mdx/package.json b/packages/mdx/package.json index 55c78a0..f807e91 100644 --- a/packages/mdx/package.json +++ b/packages/mdx/package.json @@ -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", @@ -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", diff --git a/packages/mdx/src/CallOut.tsx b/packages/mdx/src/CallOut.tsx index ae55f70..239fa14 100644 --- a/packages/mdx/src/CallOut.tsx +++ b/packages/mdx/src/CallOut.tsx @@ -6,7 +6,7 @@ type CallOutProps = { export const CallOut = ({ variant, children }: CallOutProps) => { return (
{variant} {children}
diff --git a/packages/mdx/src/MdxComponents.tsx b/packages/mdx/src/MdxComponents.tsx index 269095f..7df8b45 100644 --- a/packages/mdx/src/MdxComponents.tsx +++ b/packages/mdx/src/MdxComponents.tsx @@ -56,10 +56,10 @@ export const MdxComponents: tmdxComponents = { ), p: ({ className, color, ...props }: ParagraphProps) => ( - + ), pre: ({ className, ...props }: PreProps) => ( -
+    
   ),
   hr: (_props) => 
, code: ({ className, ...props }: ElementProps) => ( @@ -83,6 +83,7 @@ export const MdxComponents: tmdxComponents = { tr: ({ className, ...props }: React.HTMLAttributes) => ( ), + br: () => null, th: ({ className, ...props }: React.HTMLAttributes) => ( void; +} + +export const MdxEditor: React.FC = ({ content, onChange }) => { + const monaco = useMonaco(); + + useEffect(() => { + if (monaco) { + const customComponents = [ + { + label: "CallOut", + detail: "CallOut component for important messages", + insertText: '\n $0\n', + documentation: "Available types: info, warning, error", + }, + { + label: "Paragraph", + detail: "Paragraph component with various text styling options", + insertText: [ + "", + " $0", + "", + ].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 ( + 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, + }, + }} + /> + ); +}; diff --git a/packages/mdx/src/MdxViewer.tsx b/packages/mdx/src/MdxViewer.tsx index 8c7644c..5a0dabd 100644 --- a/packages/mdx/src/MdxViewer.tsx +++ b/packages/mdx/src/MdxViewer.tsx @@ -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(null); +interface Props { + source: string; +} + +export const MdxViewer = (props: Props) => { + const { source } = props; + const [content, setContent] = useState(null); useEffect(() => { - const compileMdx = async () => { - //@ts-expect-error - const { default: mdxContent } = await evaluate(source, { - ...runtime, - // biome-ignore lint/style/useNamingConvention: - useMDXComponents: () => MdxComponents, + serialize(source, { + mdxOptions: { remarkPlugins: [remarkBreaks], rehypePlugins: [ rehypeSlug, @@ -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 ? ( - - - - ) : null; + if (!content) { + return null; + } + + return ; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91273fd..375a7ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -510,6 +510,9 @@ importers: '@mdx-js/react': specifier: ^3.1.0 version: 3.1.0(@types/react@18.3.12)(react@19.0.0-rc.1) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.6.0(monaco-editor@0.52.2)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) '@next/mdx': specifier: ^15.0.3 version: 15.0.3(@mdx-js/loader@3.1.0(acorn@8.14.0)(webpack@5.94.0))(@mdx-js/react@3.1.0(@types/react@18.3.12)(react@19.0.0-rc.1)) @@ -522,6 +525,9 @@ importers: markdown-wasm: specifier: ^1.2.0 version: 1.2.0 + monaco-editor: + specifier: ^0.52.2 + version: 0.52.2 next: specifier: '>=14' version: 15.0.3(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) @@ -1639,6 +1645,18 @@ packages: resolution: {integrity: sha512-jYQMz0GK5FzfwsQZDxs58V2GeUPqma9af7vkLVrdKHzXTpV1cVXxIjSL8+rvDM8iuzVA2BEtunZ0k3bIYAmvIA==} hasBin: true + '@monaco-editor/loader@1.4.0': + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + + '@monaco-editor/react@4.6.0': + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@napi-rs/nice-android-arm-eabi@1.0.1': resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==} engines: {node: '>= 10'} @@ -5102,6 +5120,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -5974,6 +5995,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -7754,6 +7778,18 @@ snapshots: - supports-color - utf-8-validate + '@monaco-editor/loader@1.4.0(monaco-editor@0.52.2)': + dependencies: + monaco-editor: 0.52.2 + state-local: 1.0.7 + + '@monaco-editor/react@4.6.0(monaco-editor@0.52.2)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)': + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.52.2) + monaco-editor: 0.52.2 + react: 19.0.0-rc.1 + react-dom: 19.0.0-rc.1(react@19.0.0-rc.1) + '@napi-rs/nice-android-arm-eabi@1.0.1': optional: true @@ -11953,6 +11989,8 @@ snapshots: mitt@3.0.1: {} + monaco-editor@0.52.2: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -13060,6 +13098,8 @@ snapshots: stackback@0.0.2: {} + state-local@1.0.7: {} + std-env@3.8.0: {} storybook@8.4.7: