🎉 feat: initialization
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
type MonacoEditorProps,
|
||||
} from "@workspace/ui/composed/editor/monaco-editor";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function HTMLEditor(props: MonacoEditorProps) {
|
||||
return (
|
||||
<MonacoEditor
|
||||
description="Support HTML"
|
||||
title="HTML Editor"
|
||||
{...props}
|
||||
language="markdown"
|
||||
render={(value) => <HTMLPreview value={value} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface HTMLPreviewProps {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
function HTMLPreview({ value }: HTMLPreviewProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const iframeDocument = iframeRef.current?.contentDocument;
|
||||
if (iframeDocument) {
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(value || "");
|
||||
iframeDocument.close();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className="h-full w-full border-0"
|
||||
ref={iframeRef}
|
||||
title="HTML Preview"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { GoTemplateEditor } from "@workspace/ui/composed/editor/go-template";
|
||||
export { HTMLEditor } from "@workspace/ui/composed/editor/html";
|
||||
export { JSONEditor } from "@workspace/ui/composed/editor/json";
|
||||
export { MarkdownEditor } from "@workspace/ui/composed/editor/markdown";
|
||||
export { MonacoEditor } from "@workspace/ui/composed/editor/monaco-editor";
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
type MonacoEditorProps,
|
||||
} from "@workspace/ui/composed/editor/monaco-editor";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface JSONEditorProps
|
||||
extends Omit<MonacoEditorProps, "placeholder" | "value" | "onChange"> {
|
||||
schema?: Record<string, unknown>;
|
||||
placeholder?: Record<string, unknown>;
|
||||
value?: Record<string, unknown> | string;
|
||||
onChange?: (value: Record<string, unknown> | string | undefined) => void;
|
||||
}
|
||||
|
||||
export function JSONEditor(props: JSONEditorProps) {
|
||||
const { schema, placeholder = {}, ...rest } = props;
|
||||
|
||||
const editorKey = useMemo(
|
||||
() => JSON.stringify({ schema, placeholder }),
|
||||
[schema, placeholder]
|
||||
);
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
key={editorKey}
|
||||
title="Edit JSON"
|
||||
{...rest}
|
||||
language="json"
|
||||
onChange={(value) => {
|
||||
if (props.onChange && typeof value === "string") {
|
||||
try {
|
||||
props.onChange(
|
||||
props.value && typeof props.value === "string"
|
||||
? value
|
||||
: JSON.parse(value)
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Invalid JSON input:", e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMount={(editor: any, monaco: any) => {
|
||||
if (props.onMount) props.onMount(editor, monaco);
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: "",
|
||||
fileMatch: ["*"],
|
||||
schema: schema || {
|
||||
type: "object",
|
||||
properties: generateSchema(placeholder),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
placeholder={placeholder ? JSON.stringify(placeholder, null, 2) : ""}
|
||||
value={
|
||||
typeof props.value === "string"
|
||||
? props.value
|
||||
: props.value
|
||||
? JSON.stringify(props.value, null, 2)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const generateSchema = (
|
||||
obj: Record<string, unknown>
|
||||
): Record<string, SchemaProperty> => {
|
||||
const properties: Record<string, SchemaProperty> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
properties[key] = {
|
||||
type: "array",
|
||||
items:
|
||||
value.length > 0
|
||||
? generateSchema({ item: value[0] }).item
|
||||
: { type: "null" },
|
||||
};
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
properties[key] = {
|
||||
type: "object",
|
||||
properties: generateSchema(value as Record<string, unknown>),
|
||||
};
|
||||
} else {
|
||||
properties[key] = { type: typeof value as SchemaType };
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
type SchemaType = "string" | "number" | "boolean" | "object" | "array" | "null";
|
||||
interface SchemaProperty {
|
||||
type: SchemaType;
|
||||
items?: SchemaProperty;
|
||||
properties?: Record<string, SchemaProperty>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
type MonacoEditorProps,
|
||||
} from "@workspace/ui/composed/editor/monaco-editor";
|
||||
import { Markdown } from "@workspace/ui/composed/markdown";
|
||||
|
||||
export function MarkdownEditor(props: MonacoEditorProps) {
|
||||
return (
|
||||
<MonacoEditor
|
||||
description="Support markdwon and html syntax"
|
||||
title="Markdown Editor"
|
||||
{...props}
|
||||
language="markdown"
|
||||
render={(value) => <Markdown>{value || ""}</Markdown>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { Editor, type Monaco, type OnMount } from "@monaco-editor/react";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { useSize } from "ahooks";
|
||||
import { EyeIcon, EyeOff, FullscreenIcon, MinimizeIcon } from "lucide-react";
|
||||
import DraculaTheme from "monaco-themes/themes/Dracula.json" with {
|
||||
type: "json",
|
||||
};
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface MonacoEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string | undefined) => void;
|
||||
onBlur?: (value: string | undefined) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
render?: (value?: string) => React.ReactNode;
|
||||
onMount?: OnMount;
|
||||
beforeMount?: (monaco: Monaco) => void;
|
||||
language?: string;
|
||||
className?: string;
|
||||
showLineNumbers?: boolean;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function debounce<T extends (...args: any[]) => void>(func: T, delay: number) {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
export function MonacoEditor({
|
||||
value: propValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
title = "Editor Title",
|
||||
description,
|
||||
placeholder = "Start typing...",
|
||||
render,
|
||||
onMount,
|
||||
beforeMount,
|
||||
language = "markdown",
|
||||
className,
|
||||
showLineNumbers = false,
|
||||
readOnly = false,
|
||||
}: MonacoEditorProps) {
|
||||
const [internalValue, setInternalValue] = useState<string | undefined>(
|
||||
propValue
|
||||
);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isPreviewVisible, setIsPreviewVisible] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(ref);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
const debouncedOnChange = useRef(
|
||||
debounce((newValue: string | undefined) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
}
|
||||
}, 300)
|
||||
).current;
|
||||
|
||||
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
||||
if (onMount) onMount(editor, monaco);
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const newValue = editor.getValue();
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
});
|
||||
|
||||
editor.onDidBlurEditorWidget(() => {
|
||||
if (onBlur) {
|
||||
onBlur(editor.getValue());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => setIsFullscreen(!isFullscreen);
|
||||
const togglePreview = () => setIsPreviewVisible(!isPreviewVisible);
|
||||
|
||||
return (
|
||||
<div className="size-full" ref={ref}>
|
||||
<div style={size}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full min-h-96 flex-col rounded-md border",
|
||||
className,
|
||||
{
|
||||
"!mt-0 fixed inset-0 z-50 h-screen bg-background": isFullscreen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b p-2">
|
||||
<div>
|
||||
<h1 className="text-left font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{render && (
|
||||
<Button
|
||||
onClick={togglePreview}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isPreviewVisible ? <EyeOff /> : <EyeIcon />}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleFullscreen}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isFullscreen ? <MinimizeIcon /> : <FullscreenIcon />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("relative flex flex-1")}>
|
||||
<div
|
||||
className={cn("flex-1 overflow-auto p-4 invert dark:invert-0", {
|
||||
"w-1/2": isPreviewVisible,
|
||||
})}
|
||||
>
|
||||
<Editor
|
||||
beforeMount={(monaco: Monaco) => {
|
||||
monaco.editor.defineTheme("transparentTheme", {
|
||||
base: DraculaTheme.base as "vs" | "vs-dark" | "hc-black",
|
||||
inherit: DraculaTheme.inherit,
|
||||
rules: DraculaTheme.rules,
|
||||
colors: {
|
||||
...DraculaTheme.colors,
|
||||
"editor.background": "#00000000",
|
||||
},
|
||||
});
|
||||
if (beforeMount) {
|
||||
beforeMount(monaco);
|
||||
}
|
||||
}}
|
||||
className=""
|
||||
language={language}
|
||||
onChange={(newValue) => {
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
contextmenu: false,
|
||||
folding: false,
|
||||
fontSize: 14,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: showLineNumbers ? "on" : "off",
|
||||
minimap: { enabled: false },
|
||||
overviewRulerLanes: 0,
|
||||
renderLineHighlight: "none",
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
useShadows: false,
|
||||
vertical: "auto",
|
||||
},
|
||||
tabSize: 2,
|
||||
wordWrap: "off",
|
||||
readOnly,
|
||||
}}
|
||||
theme="transparentTheme"
|
||||
value={internalValue}
|
||||
/>
|
||||
{!internalValue?.trim() && placeholder && (
|
||||
<pre
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-4 left-7 text-muted-foreground text-sm",
|
||||
{
|
||||
"left-16": showLineNumbers,
|
||||
}
|
||||
)}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{placeholder}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
{render && isPreviewVisible && (
|
||||
<div className="w-1/2 flex-1 overflow-auto border-l p-4">
|
||||
{render(internalValue)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user