♻️ refactor(core): Restructure project for better module separation
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
'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 { 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;
|
||||
language?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// 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 function (...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,
|
||||
language = 'markdown',
|
||||
className,
|
||||
}: 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 ref={ref} className='size-full'>
|
||||
<div style={size}>
|
||||
<div
|
||||
className={cn('flex size-full min-h-96 flex-col rounded-md border', className, {
|
||||
'bg-background fixed inset-0 z-50 !mt-0 h-screen': isFullscreen,
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center justify-between border-b p-2'>
|
||||
<div>
|
||||
<h1 className='text-left text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='text-muted-foreground text-[0.8rem]'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-2'>
|
||||
{render && (
|
||||
<Button variant='outline' size='icon' type='button' onClick={togglePreview}>
|
||||
{isPreviewVisible ? <EyeOff /> : <EyeIcon />}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant='outline' size='icon' type='button' onClick={toggleFullscreen}>
|
||||
{isFullscreen ? <MinimizeIcon /> : <FullscreenIcon />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn('relative flex flex-1 overflow-hidden')}>
|
||||
<div
|
||||
className={cn('flex-1 overflow-hidden p-4 invert dark:invert-0', {
|
||||
'w-1/2': isPreviewVisible,
|
||||
})}
|
||||
>
|
||||
<Editor
|
||||
language={language}
|
||||
value={internalValue}
|
||||
onChange={(newValue) => {
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
className=''
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
contextmenu: false,
|
||||
folding: false,
|
||||
fontSize: 14,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'off',
|
||||
minimap: { enabled: false },
|
||||
overviewRulerLanes: 0,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
useShadows: false,
|
||||
vertical: 'hidden',
|
||||
},
|
||||
tabSize: 2,
|
||||
wordWrap: 'off',
|
||||
}}
|
||||
theme='transparentTheme'
|
||||
beforeMount={(monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('transparentTheme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!internalValue?.trim() && placeholder && (
|
||||
<pre
|
||||
className='text-muted-foreground pointer-events-none absolute left-7 top-4 text-sm'
|
||||
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