This commit is contained in:
2026-03-04 17:35:14 +01:00
parent f73b56cdc4
commit db26e26495
113 changed files with 12433 additions and 169 deletions
+106
View File
@@ -0,0 +1,106 @@
import React, { ComponentProps, useRef } from "react";
import { twMerge } from "tailwind-merge";
import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent";
import MarkdownEditorComponent from "./MarkdownEditorComponent";
import MarkdownEditorSelectorButtons from "./MarkdownEditorSelectorButtons";
import Row from "../../layout/Row";
import Stack from "../../layout/Stack";
import AceEditor from "../../editors/AceEditor";
import useStatus from "../../hooks/useStatus";
type Props = {
value?: string;
setValue?: React.Dispatch<React.SetStateAction<string>>;
defaultSideBySide?: boolean;
changeHandler?: (content: string) => void;
editorProps?: ComponentProps<typeof AceEditor>;
maxHeight?: string;
noToggleButtons?: boolean;
};
export default function MarkdownEditor({
value: existingValue,
setValue: setExistingValue,
defaultSideBySide,
changeHandler,
editorProps,
maxHeight: existingMaxHeight,
noToggleButtons,
}: Props) {
const [value, setValue] = React.useState<any>(existingValue || ``);
const [sideBySide, setSideBySide] = React.useState(
defaultSideBySide || false,
);
const [preview, setPreview] = React.useState(false);
const { refresh, setRefresh } = useStatus();
const updatingFromExtValueRef = useRef(false);
const maxHeight = existingMaxHeight || "600px";
React.useEffect(() => {
if (updatingFromExtValueRef.current) return;
setExistingValue?.(value);
changeHandler?.(value);
}, [value]);
React.useEffect(() => {
if (!existingValue) return;
updatingFromExtValueRef.current = true;
setValue(existingValue);
setTimeout(() => {
updatingFromExtValueRef.current = false;
setRefresh((prev) => prev + 1);
}, 500);
}, [existingValue]);
return (
<Stack className="w-full items-stretch">
{!noToggleButtons && (
<MarkdownEditorSelectorButtons
{...{ preview, setPreview, setSideBySide, sideBySide }}
/>
)}
{sideBySide ? (
<Row
className={twMerge(
`w-full grid xl:grid-cols-2 gap-6 max-h-[${maxHeight}]`,
"overflow-auto",
)}
>
<MarkdownEditorComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
<MarkdownEditorPreviewComponent
value={value}
maxHeight={maxHeight}
/>
</Row>
) : (
<Stack
className={twMerge(`w-full max-h-[${maxHeight}] h-full`)}
>
{preview ? (
<MarkdownEditorPreviewComponent
value={value}
maxHeight={maxHeight}
/>
) : (
<MarkdownEditorComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
)}
</Stack>
)}
</Stack>
);
}
@@ -0,0 +1,32 @@
import React, { ComponentProps } from "react";
import AceEditor from "../../editors/AceEditor";
type Props = ComponentProps<typeof AceEditor> & {
value: string;
setValue: React.Dispatch<any>;
maxHeight: string;
};
export default function MarkdownEditorComponent({
value,
setValue,
maxHeight,
...props
}: Props) {
return (
<AceEditor
mode="markdown"
content={value}
onChange={(newValue) => {
setValue(newValue);
}}
wrapperProps={{
style: { height: maxHeight },
className: `max-h-[${maxHeight}]`,
}}
placeholder="## Write Some markdown ..."
fontSize="14px"
{...props}
/>
);
}
@@ -0,0 +1,77 @@
import React, { ComponentProps, RefObject } from "react";
import { twMerge } from "tailwind-merge";
import { serialize } from "next-mdx-remote/serialize";
import remarkGfm from "remark-gfm";
import rehypePrismPlus from "rehype-prism-plus";
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import Border from "../../elements/Border";
import { useMDXComponents } from "../mdx-components";
import EmptyContent from "../../elements/EmptyContent";
type Props = {
value: string;
maxHeight: string;
wrapperProps?: ComponentProps<typeof Border>;
};
export default function MarkdownEditorPreviewComponent({
value,
maxHeight,
wrapperProps,
}: Props) {
try {
const [mdxSource, setMdxSource] =
React.useState<
MDXRemoteSerializeResult<
Record<string, unknown>,
Record<string, unknown>
>
>();
const { components } = useMDXComponents();
React.useEffect(() => {
try {
// const { data, content } = matter(value);
const parsedValue = value
.replace(/---([^(---)]+)---/, "")
.trim();
serialize(parsedValue, {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrismPlus],
},
// scope: data,
})
.then((mdxSrc) => {
setMdxSource(mdxSrc);
})
.catch((err) => {});
} catch (error) {}
}, [value]);
return (
<Border
{...wrapperProps}
className={twMerge(
`w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`,
"overflow-auto",
wrapperProps?.className
)}
>
{mdxSource ? (
<MDXRemote
{...mdxSource}
components={{
...components,
}}
/>
) : null}
</Border>
);
} catch (error) {
return <EmptyContent title={`Markdown Syntax Error.`} />;
}
}
@@ -0,0 +1,95 @@
import React from "react";
import Button from "../../layout/Button";
import Border from "../../elements/Border";
type Props = {
preview: boolean;
setPreview: React.Dispatch<React.SetStateAction<boolean>>;
sideBySide: boolean;
setSideBySide: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function MarkdownEditorSelectorButtons({
preview,
setPreview,
setSideBySide,
sideBySide,
}: Props) {
const [ready, setReady] = React.useState(false);
const defSbsName = "TWUIMarkdownEditorDefaultSideBySide";
const defPrevName = "TWUIMarkdownEditorDefaultPreview";
React.useEffect(() => {
if (!ready) return;
if (sideBySide) {
localStorage.setItem(defSbsName, "true");
} else {
localStorage.removeItem(defSbsName);
}
if (preview) {
localStorage.setItem(defPrevName, "true");
} else {
localStorage.removeItem(defPrevName);
}
}, [sideBySide, preview, ready]);
React.useEffect(() => {
if (localStorage.getItem(defPrevName)) {
setPreview(true);
}
if (!localStorage.getItem(defSbsName)) {
setSideBySide(false);
}
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<Border className="p-1.5">
<Button
title="Code"
size="smaller"
color={sideBySide ? "gray" : preview ? "gray" : "primary"}
variant={
sideBySide ? "outlined" : preview ? "outlined" : "normal"
}
onClick={() => {
setSideBySide(false);
setPreview(false);
}}
>
Code
</Button>
<Button
title="Preview"
size="smaller"
color={sideBySide ? "gray" : preview ? "primary" : "gray"}
variant={
sideBySide ? "outlined" : preview ? "normal" : "outlined"
}
onClick={() => {
setSideBySide(false);
setPreview(true);
}}
>
Preview
</Button>
<Button
title="Side By Side"
size="smaller"
color={sideBySide ? "primary" : "gray"}
variant={sideBySide ? "normal" : "outlined"}
onClick={() => {
setSideBySide(true);
}}
>
Side By Side
</Button>
</Border>
);
}
+81
View File
@@ -0,0 +1,81 @@
import { MDXComponents } from "mdx/types";
import CodeBlock from "../elements/CodeBlock";
import H1 from "../layout/H1";
import H4 from "../layout/H4";
import React from "react";
import twuiSlugify from "../utils/slugify";
import H2 from "../layout/H2";
import P from "../layout/P";
import H3 from "../layout/H3";
type Params = {
components: MDXComponents;
codeBgColor?: string;
};
export function useMDXComponents(params?: Params) {
const codeBgColor = params?.codeBgColor || "#000000";
return {
components: {
h1: ({ children }) => <H1>{children}</H1>,
h4: ({ children }) => <H4>{children}</H4>,
pre: ({ children, ...props }) => {
if (React.isValidElement(children) && children.props) {
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{/* @ts-ignore */}
{children.props.children}
</CodeBlock>
);
}
return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}>
{children}
</CodeBlock>
);
},
h2: ({ children }) => {
const slug = twuiSlugify(children?.toString());
return (
<a
href={`#${slug}`}
id={slug}
className="twui-docs-header-anchor"
>
<H2 className="pt-4 m-0 mb-4 text-2xl">{children}</H2>
</a>
);
},
h3: ({ children }) => {
const slug = twuiSlugify(children?.toString());
return (
<a
href={`#${slug}`}
id={slug}
className="twui-docs-header-anchor"
>
<H3 className="pt-4 m-0 mb-4 text-xl">{children}</H3>
</a>
);
},
img: (props) => (
// @ts-ignore
<img
{...props}
className="w-full h-auto shadow-lg rounded-default overflow-hidden"
/>
),
p: ({ children }) => (
<P className="mt-2 mb-6 max-w-none py-0">{children}</P>
),
li: ({ children }) => <li className="w-full">{children}</li>,
...params?.components,
} as MDXComponents,
codeBgColor: "rgb(7 12 22)",
};
}