🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+44
View File
@@ -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";
+102
View File
@@ -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>
);
}