Skip to content

Commit d4e50e5

Browse files
committed
feat: DraggableBlock
1 parent 3dfc45c commit d4e50e5

34 files changed

+773
-192
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"import/newline-after-import": "error",
2020
"import/no-duplicates": "error",
2121
"react-refresh/only-export-components": [
22-
"warn",
22+
"off",
2323
{ "allowConstantExport": true }
2424
],
2525
"@typescript-eslint/ban-ts-comment": "off",

packages/editor/components.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"tailwind": {
77
"config": "tailwind.config.js",
88
"css": "src/index.css",
9-
"baseColor": "zinc",
9+
"baseColor": "stone",
1010
"cssVariables": true,
1111
"prefix": ""
1212
},
1313
"aliases": {
1414
"components": "@/components",
1515
"utils": "@/lib/utils"
1616
}
17-
}
17+
}

packages/editor/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
66
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
77
<style>
8-
body {
8+
body,
9+
#root {
910
height: 100vh;
1011
}
1112
</style>
1213
</head>
1314
<body>
15+
<div id="root"></div>
1416
<script type="module" src="/src/index.dev.tsx"></script>
1517
</body>
1618
</html>

packages/editor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@lexical/link": "0.17.0",
5151
"@lexical/list": "0.17.0",
5252
"@lexical/mark": "0.17.0",
53+
"@lexical/markdown": "0.17.0",
5354
"@lexical/overflow": "0.17.0",
5455
"@lexical/plain-text": "0.17.0",
5556
"@lexical/react": "0.17.0",
@@ -84,6 +85,7 @@
8485
"katex": "^0.16.11",
8586
"lexical": "0.17.0",
8687
"lodash-es": "^4.17.21",
88+
"lucide-react": "^0.428.0",
8789
"next-themes": "^0.3.0",
8890
"react-error-boundary": "^4.0.13",
8991
"sonner": "^1.5.0",

packages/editor/src/Editor.css.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,18 @@ export const container = style({
1313
height: '100%',
1414
});
1515

16+
export const center = style({
17+
display: 'flex',
18+
width: '100%',
19+
height: '100%',
20+
flexDirection: 'column',
21+
alignItems: 'center',
22+
});
23+
1624
export const editor = style({
1725
position: 'relative',
26+
width: '100%',
27+
maxWidth: 1100,
1828
});
1929

2030
export const minHeightVar = createVar();
@@ -25,7 +35,7 @@ export const contentEditable = style({
2535
display: 'block',
2636
position: 'relative',
2737
outline: 0,
28-
padding: '8px 28px 40px',
38+
padding: '40px 28px 40px',
2939
minHeight: fallbackVar(minHeightVar, '150px'),
3040
});
3141

@@ -34,7 +44,7 @@ export const placeholder = style({
3444
overflow: 'hidden',
3545
position: 'absolute',
3646
textOverflow: 'ellipsis',
37-
top: 8,
47+
top: 40,
3848
left: 28,
3949
right: 28,
4050
userSelect: 'none',

packages/editor/src/Editor.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import {
55
} from '@lexical/react/LexicalComposer';
66
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
77
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
8+
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
89
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
910
import { assignInlineVars } from '@vanilla-extract/dynamic';
10-
import clsx from 'clsx';
11-
import { useMemo, useRef } from 'react';
11+
import { useMemo, useState } from 'react';
1212

1313
import { AppContext } from '@/components/app-context';
1414
import { ThemeProvider } from '@/components/theme-provider';
1515
import { ScrollArea } from '@/components/ui/scroll-area';
1616
import { extensions } from '@/extensions';
1717
import { ExtensionManagerContext } from '@/extensions/context';
1818
import { configureExtensions } from '@/extensions/extensionManager';
19+
import { cn } from '@/lib/utils';
1920

2021
import * as styles from './Editor.css';
2122

@@ -37,36 +38,46 @@ const Editor: React.FC<EditorProps> = ({ minHeight }) => {
3738
throw error;
3839
},
3940
};
40-
const root = useRef<HTMLDivElement>(null);
41-
const appContext = useMemo(() => ({ root }), []);
41+
42+
const [root, setRoot] = useState<HTMLDivElement | null>(null);
43+
const [editor, setEditor] = useState<HTMLDivElement | null>(null);
44+
const appContext = useMemo(() => ({ root, editor }), [editor, root]);
4245

4346
return (
4447
<ExtensionManagerContext value={extensionManager}>
4548
<LexicalComposer initialConfig={initialConfig}>
4649
<AppContext value={appContext}>
4750
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
48-
<div ref={root} className={clsx(['wysidoc-editor', styles.shell])}>
51+
<div ref={setRoot} className={cn('wysidoc-editor', styles.shell)}>
4952
<ScrollArea className={styles.container}>
50-
<div className={styles.editor}>
51-
<RichTextPlugin
52-
contentEditable={
53-
<ContentEditable
54-
className={styles.contentEditable}
55-
style={assignInlineVars({
56-
[styles.minHeightVar]: minHeight,
57-
})}
58-
aria-placeholder={'placeholder...'}
59-
placeholder={
60-
<div className={styles.placeholder}>
61-
placeholder...
62-
</div>
63-
}
64-
/>
65-
}
66-
ErrorBoundary={LexicalErrorBoundary}
67-
/>
68-
<Plugins />
69-
<AutoFocusPlugin />
53+
<div className={styles.center}>
54+
<div ref={setEditor} className={styles.editor}>
55+
<RichTextPlugin
56+
contentEditable={
57+
<ContentEditable
58+
className={styles.contentEditable}
59+
style={assignInlineVars({
60+
[styles.minHeightVar]: minHeight,
61+
})}
62+
aria-placeholder={'placeholder...'}
63+
placeholder={
64+
<div
65+
className={cn(
66+
styles.placeholder,
67+
'text-muted-foreground'
68+
)}
69+
>
70+
placeholder...
71+
</div>
72+
}
73+
/>
74+
}
75+
ErrorBoundary={LexicalErrorBoundary}
76+
/>
77+
<Plugins />
78+
<AutoFocusPlugin />
79+
<HistoryPlugin />
80+
</div>
7081
</div>
7182
</ScrollArea>
7283
</div>

packages/editor/src/components/app-context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { createContext, createRef, useContext } from 'react';
1+
import { createContext, useContext } from 'react';
22

33
type AppContextState = {
4-
root: React.RefObject<HTMLDivElement>;
4+
root: HTMLDivElement | null;
5+
editor: HTMLDivElement | null;
56
};
67

78
const _AppContext = createContext<AppContextState>({
8-
root: createRef(),
9+
root: null,
10+
editor: null,
911
});
1012

1113
export const AppContext = _AppContext.Provider;

packages/editor/src/components/menu/index.tsx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1+
import { forwardRef } from 'react';
2+
13
import { cn } from '@/lib/utils';
24

35
type MenuItemProps = React.HTMLAttributes<HTMLDivElement> &
46
React.PropsWithChildren<{}>;
57

6-
export const MenuItem: React.FC<MenuItemProps> = ({
7-
className,
8-
children,
9-
...props
10-
}) => {
11-
return (
12-
<div
13-
className={cn(
14-
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50',
15-
className
16-
)}
17-
{...props}
18-
>
19-
{children}
20-
</div>
21-
);
22-
};
8+
export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
9+
({ className, children, ...props }, ref) => {
10+
return (
11+
<div
12+
className={cn(
13+
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none aria-[selected=true]:bg-accent aria-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50',
14+
className
15+
)}
16+
ref={ref}
17+
{...props}
18+
>
19+
{children}
20+
</div>
21+
);
22+
}
23+
);
2324

2425
type MenuContentProps = React.HTMLAttributes<HTMLDivElement> &
2526
React.PropsWithChildren<{}>;
@@ -32,7 +33,7 @@ export const MenuContent: React.FC<MenuContentProps> = ({
3233
return (
3334
<div
3435
className={cn(
35-
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
36+
'z-50 min-w-[8rem] w-max overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
3637
className
3738
)}
3839
{...props}

packages/editor/src/components/theme-provider.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,21 @@ export function ThemeProvider({
3434
);
3535

3636
useEffect(() => {
37-
const $root = root.current;
38-
if (!$root) return;
37+
if (!root) return;
3938

40-
$root.classList.remove('light', 'dark');
39+
root.classList.remove('light', 'dark');
4140

4241
if (theme === 'system') {
4342
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
4443
.matches
4544
? 'dark'
4645
: 'light';
4746

48-
$root.classList.add(systemTheme);
47+
root.classList.add(systemTheme);
4948
return;
5049
}
5150

52-
$root.classList.add(theme);
51+
root.classList.add(theme);
5352
}, [root, theme]);
5453

5554
const value = {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { registerCodeHighlighting } from '@lexical/code';
2+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3+
import { useEffect } from 'react';
4+
5+
const CodePlugin: React.FC = () => {
6+
const [editor] = useLexicalComposerContext();
7+
8+
useEffect(() => registerCodeHighlighting(editor), [editor]);
9+
10+
return null;
11+
};
12+
13+
CodePlugin.displayName = 'CodePlugin';
14+
15+
export default CodePlugin;

packages/editor/src/extensions/code/code.css.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { style } from '@vanilla-extract/css';
22

33
export const code = style({
4-
backgroundColor: 'rgb(240, 242, 245)',
4+
backgroundColor: 'hsl(var(--muted))',
55
fontFamily: 'Menlo, Consolas, Monaco, monospace',
66
display: 'block',
77
padding: '8px 8px 8px 52px',
@@ -16,12 +16,12 @@ export const code = style({
1616
':before': {
1717
content: 'attr(data-gutter)',
1818
position: 'absolute',
19-
backgroundColor: '#eee',
19+
backgroundColor: 'hsl(var(--muted))',
2020
left: 0,
2121
top: 0,
22-
borderRight: '1px solid #ccc',
22+
borderRight: '1px solid hsl(var(--border))',
2323
padding: 8,
24-
color: '#777',
24+
color: 'hsl(var(--muted-foreground))',
2525
whiteSpace: 'pre-wrap',
2626
textAlign: 'right',
2727
minWidth: 25,

packages/editor/src/extensions/code/extension.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { CodeHighlightNode, CodeNode } from '@lexical/code';
1+
import { $createCodeNode, CodeHighlightNode, CodeNode } from '@lexical/code';
2+
import { $setBlocksType } from '@lexical/selection';
3+
import { $getSelection, $isRangeSelection } from 'lexical';
4+
import { Code } from 'lucide-react';
25

36
import type { RegisterExtension } from '@/extensions/extensionManager';
47

58
import * as styles from './code.css';
9+
import CodePlugin from './CodePlugin';
610

711
export const registerExtensionCode: RegisterExtension = context => {
812
context.subscriptions
913
.add(context.registerNode(CodeNode, CodeHighlightNode))
14+
.add(context.registerPlugin(CodePlugin))
1015
.add(
1116
context.registerTheme({
1217
code: styles.code,
@@ -43,5 +48,35 @@ export const registerExtensionCode: RegisterExtension = context => {
4348
variable: styles.tokenVariable,
4449
},
4550
})
51+
)
52+
.add(
53+
context.registerSlashCommand(({ editor, registerCommands }) => {
54+
registerCommands(
55+
[
56+
{
57+
title: 'Code',
58+
icon: Code,
59+
keywords: ['javascript', 'python', 'js', 'codeblock'],
60+
onSelect: () => {
61+
editor.update(() => {
62+
const selection = $getSelection();
63+
64+
if ($isRangeSelection(selection)) {
65+
if (selection.isCollapsed()) {
66+
$setBlocksType(selection, () => $createCodeNode());
67+
} else {
68+
const textContent = selection.getTextContent();
69+
const codeNode = $createCodeNode();
70+
selection.insertNodes([codeNode]);
71+
selection.insertRawText(textContent);
72+
}
73+
}
74+
});
75+
},
76+
},
77+
],
78+
1
79+
);
80+
})
4681
);
4782
};

0 commit comments

Comments
 (0)