mirror of https://github.com/fantasticit/think.git
Merge pull request #161 from fantasticit/feat/excalidraw
commit
d98fb8d4c5
|
@ -11,6 +11,7 @@
|
|||
"@douyinfe/semi-icons": "^2.3.1",
|
||||
"@douyinfe/semi-next": "^2.3.1",
|
||||
"@douyinfe/semi-ui": "^2.3.1",
|
||||
"@excalidraw/excalidraw": "^0.12.0",
|
||||
"@hocuspocus/provider": "^1.0.0-alpha.29",
|
||||
"@think/config": "workspace:^1.0.0",
|
||||
"@think/constants": "workspace:^1.0.0",
|
||||
|
|
|
@ -141,3 +141,7 @@
|
|||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw.excalidraw-modal-container {
|
||||
z-index: 1010 !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
import { IUser } from '@think/domains';
|
||||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { ExcalidrawWrapper } from 'tiptap/core/wrappers/excalidraw';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
const DEFAULT_MIND_DATA = { elements: [] };
|
||||
|
||||
export interface IExcalidrawAttrs {
|
||||
defaultShowPicker?: boolean;
|
||||
createUser?: IUser['id'];
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
excalidraw: {
|
||||
setExcalidraw: (attrs?: IExcalidrawAttrs) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Excalidraw = Node.create({
|
||||
name: 'excalidraw',
|
||||
group: 'block',
|
||||
selectable: true,
|
||||
atom: true,
|
||||
draggable: true,
|
||||
inline: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
defaultShowPicker: {
|
||||
default: false,
|
||||
},
|
||||
createUser: {
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
default: '100%',
|
||||
parseHTML: getDatasetAttribute('width'),
|
||||
},
|
||||
height: {
|
||||
default: 240,
|
||||
parseHTML: getDatasetAttribute('height'),
|
||||
},
|
||||
data: {
|
||||
default: DEFAULT_MIND_DATA,
|
||||
parseHTML: getDatasetAttribute('data', true),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'mind',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[class=mind]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setExcalidraw:
|
||||
(options) =>
|
||||
({ tr, commands, chain, editor }) => {
|
||||
options = options || {};
|
||||
options.data = options.data || DEFAULT_MIND_DATA;
|
||||
|
||||
// @ts-ignore
|
||||
if (tr.selection?.node?.type?.name == this.name) {
|
||||
return commands.updateAttributes(this.name, options);
|
||||
}
|
||||
|
||||
return chain()
|
||||
.insertContent({
|
||||
type: this.name,
|
||||
attrs: options,
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ExcalidrawWrapper);
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: /^\$excalidraw $/,
|
||||
type: this.type,
|
||||
getAttributes: () => {
|
||||
return { width: '100%' };
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -14,6 +14,7 @@ export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
|
|||
export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
|
||||
export const OPEN_FLOW_SETTING_MODAL = 'OPEN_FLOW_SETTING_MODAL';
|
||||
export const OPEN_MIND_SETTING_MODAL = 'OPEN_MIND_SETTING_MODAL';
|
||||
export const OPEN_EXCALIDRAW_SETTING_MODAL = 'OPEN_EXCALIDRAW_SETTING_MODAL';
|
||||
|
||||
export const subject = (editor: Editor, eventName, handler) => {
|
||||
const event = getEventEmitter(editor);
|
||||
|
@ -44,3 +45,8 @@ export const triggerOpenMindSettingModal = (editor: Editor, data) => {
|
|||
const event = getEventEmitter(editor);
|
||||
event.emit(OPEN_MIND_SETTING_MODAL, data);
|
||||
};
|
||||
|
||||
export const triggerOpenExcalidrawSettingModal = (editor: Editor, data) => {
|
||||
const event = getEventEmitter(editor);
|
||||
event.emit(OPEN_EXCALIDRAW_SETTING_MODAL, data);
|
||||
};
|
||||
|
|
|
@ -135,6 +135,14 @@ export const COMMANDS: ICommand[] = [
|
|||
editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconMind />,
|
||||
label: '绘图',
|
||||
action: (editor, user) => {
|
||||
editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconMath />,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { IconCopy, IconDelete, IconEdit, IconLineHeight } from '@douyinfe/semi-icons';
|
||||
import { Button, Space } from '@douyinfe/semi-ui';
|
||||
import { Divider } from 'components/divider';
|
||||
import { SizeSetter } from 'components/size-setter';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { useUser } from 'data/user';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { BubbleMenu } from 'tiptap/core/bubble-menu';
|
||||
import { Excalidraw, IExcalidrawAttrs } from 'tiptap/core/extensions/excalidraw';
|
||||
import { useAttributes } from 'tiptap/core/hooks/use-attributes';
|
||||
import { copyNode, deleteNode, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||
|
||||
import { triggerOpenExcalidrawSettingModal } from '../_event';
|
||||
|
||||
export const ExcalidrawBubbleMenu = ({ editor }) => {
|
||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||
const attrs = useAttributes<IExcalidrawAttrs>(editor, Excalidraw.name, {
|
||||
defaultShowPicker: false,
|
||||
createUser: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const { defaultShowPicker, createUser, width, height } = attrs;
|
||||
const { user } = useUser();
|
||||
|
||||
const setSize = useCallback(
|
||||
(size) => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Excalidraw.name, size)
|
||||
.setNodeSelection(editor.state.selection.from)
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
const openEditLinkModal = useCallback(() => {
|
||||
triggerOpenExcalidrawSettingModal(editor, attrs);
|
||||
}, [editor, attrs]);
|
||||
const shouldShow = useCallback(() => editor.isActive(Excalidraw.name), [editor]);
|
||||
const copyMe = useCallback(() => copyNode(Excalidraw.name, editor), [editor]);
|
||||
const deleteMe = useCallback(() => deleteNode(Excalidraw.name, editor), [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultShowPicker && user && createUser === user.id) {
|
||||
openEditLinkModal();
|
||||
editor.chain().updateAttributes(Excalidraw.name, { defaultShowPicker: false }).focus().run();
|
||||
}
|
||||
}, [createUser, defaultShowPicker, editor, openEditLinkModal, user]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
className={'bubble-menu'}
|
||||
editor={editor}
|
||||
pluginKey="flow-bubble-menu"
|
||||
shouldShow={shouldShow}
|
||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||
>
|
||||
<Space spacing={4}>
|
||||
<Tooltip content="复制">
|
||||
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="编辑">
|
||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||
</Tooltip>
|
||||
|
||||
<SizeSetter width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
||||
<Tooltip content="设置宽高">
|
||||
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
</SizeSetter>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</BubbleMenu>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Editor } from 'tiptap/core';
|
||||
|
||||
import { ExcalidrawBubbleMenu } from './bubble';
|
||||
import { ExcalidrawSettingModal } from './modal';
|
||||
|
||||
export const Excalidraw: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
return (
|
||||
<>
|
||||
<ExcalidrawBubbleMenu editor={editor} />
|
||||
<ExcalidrawSettingModal editor={editor} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
import { Modal, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Editor } from 'tiptap/core';
|
||||
|
||||
import { cancelSubject, OPEN_EXCALIDRAW_SETTING_MODAL, subject } from '../_event';
|
||||
|
||||
type IProps = { editor: Editor };
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const ExcalidrawSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||
const [Excalidraw, setExcalidraw] = useState(null);
|
||||
const [data, setData] = useState({});
|
||||
const [initialData, setInitialData] = useState({ elements: [], appState: { isLoading: false }, files: null });
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const renderEditor = useCallback(
|
||||
(div) => {
|
||||
if (!div) return;
|
||||
|
||||
import('@excalidraw/excalidraw')
|
||||
.then((res) => {
|
||||
setExcalidraw(res.Excalidraw);
|
||||
})
|
||||
.catch(setError)
|
||||
.finally(() => toggleLoading(false));
|
||||
},
|
||||
[toggleLoading]
|
||||
);
|
||||
|
||||
const renderExcalidraw = useCallback((app) => {
|
||||
console.log('render', app);
|
||||
setTimeout(() => {
|
||||
app.refresh();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback((elements, appState, files) => {
|
||||
// excalidraw 导出的是 {},实际上应该是 []
|
||||
// appState.collaborators = [];
|
||||
setData({
|
||||
elements,
|
||||
appState: { isLoading: false },
|
||||
files,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const save = useCallback(() => {
|
||||
if (!Excalidraw) {
|
||||
toggleVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.chain().focus().setExcalidraw({ data }).run();
|
||||
toggleVisible(false);
|
||||
}, [Excalidraw, editor, data, toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data) => {
|
||||
toggleVisible(true);
|
||||
data && setInitialData(data.data);
|
||||
};
|
||||
|
||||
subject(editor, OPEN_EXCALIDRAW_SETTING_MODAL, handler);
|
||||
|
||||
return () => {
|
||||
cancelSubject(editor, OPEN_EXCALIDRAW_SETTING_MODAL, handler);
|
||||
};
|
||||
}, [editor, toggleVisible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
title="绘图"
|
||||
fullScreen
|
||||
visible={visible}
|
||||
onCancel={toggleVisible}
|
||||
onOk={save}
|
||||
okText="保存"
|
||||
cancelText="退出"
|
||||
motion={false}
|
||||
>
|
||||
<div style={{ height: '100%', margin: '0 -24px', border: '1px solid var(--semi-color-border)' }}>
|
||||
{loading && (
|
||||
<Spin spinning>
|
||||
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||
<div></div>
|
||||
</Spin>
|
||||
)}
|
||||
{error && <Text>{(error && error.message) || '未知错误'}</Text>}
|
||||
<div style={{ width: '100%', height: '100%' }} ref={renderEditor}>
|
||||
{!loading && !error && Excalidraw ? (
|
||||
<Excalidraw ref={renderExcalidraw} onChange={onChange} langCode="zh-CN" initialData={initialData} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -38,7 +38,8 @@
|
|||
.node-flow,
|
||||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference {
|
||||
.node-documentReference,
|
||||
.node-excalidraw {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
|
@ -60,7 +61,8 @@
|
|||
.node-flow,
|
||||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference {
|
||||
.node-documentReference,
|
||||
.node-excalidraw {
|
||||
.render-wrapper {
|
||||
position: relative;
|
||||
user-select: text;
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference,
|
||||
.node-status {
|
||||
.node-status,
|
||||
.node-excalidraw {
|
||||
&.selected-node {
|
||||
&:not(.has-focus) {
|
||||
::selection {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
.wrap {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
line-height: 0;
|
||||
|
||||
.renderWrap {
|
||||
border: 1px solid var(--node-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&::after {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #fff;
|
||||
background-color: #f80;
|
||||
border-radius: 2px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import { Space, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { IconMind } from 'components/icons';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||
|
||||
export const ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const exportToSvgRef = useRef(null);
|
||||
const isEditable = editor.isEditable;
|
||||
const isActive = editor.isActive(Excalidraw.name);
|
||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||
const { data, width, height } = node.attrs;
|
||||
const [Svg, setSvg] = useState<SVGElement | null>(null);
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const onResize = useCallback(
|
||||
(size) => {
|
||||
updateAttributes({ width: size.width, height: size.height });
|
||||
},
|
||||
[updateAttributes]
|
||||
);
|
||||
|
||||
const onViewportChange = useCallback(
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
toggleVisible(true);
|
||||
}
|
||||
},
|
||||
[toggleVisible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
import('@excalidraw/excalidraw')
|
||||
.then((res) => {
|
||||
exportToSvgRef.current = res.exportToSvg;
|
||||
})
|
||||
.catch(setError)
|
||||
.finally(() => toggleLoading(false));
|
||||
}, [toggleLoading, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const setContent = async () => {
|
||||
if (loading || error || !visible || !data) return;
|
||||
|
||||
const svg: SVGElement = await exportToSvgRef.current(data);
|
||||
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('display', 'block');
|
||||
|
||||
setSvg(svg);
|
||||
};
|
||||
setContent();
|
||||
}, [data, loading, error, visible]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
|
||||
>
|
||||
{error && (
|
||||
<div style={INHERIT_SIZE_STYLE}>
|
||||
<Text>{error.message || error}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>}
|
||||
|
||||
{!loading && !error && visible && (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
padding: 24,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: Svg?.outerHTML ?? '' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.title}>
|
||||
<Space>
|
||||
<span className={styles.icon}>
|
||||
<IconMind />
|
||||
</span>
|
||||
绘图
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Resizeable>
|
||||
</VisibilitySensor>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
|
@ -16,6 +16,7 @@ import { Countdonw } from 'tiptap/core/menus/countdown';
|
|||
import { DocumentChildren } from 'tiptap/core/menus/document-children';
|
||||
import { DocumentReference } from 'tiptap/core/menus/document-reference';
|
||||
import { Emoji } from 'tiptap/core/menus/emoji';
|
||||
import { Excalidraw } from 'tiptap/core/menus/excalidraw';
|
||||
import { Flow } from 'tiptap/core/menus/flow';
|
||||
import { FontSize } from 'tiptap/core/menus/fontsize';
|
||||
import { Heading } from 'tiptap/core/menus/heading';
|
||||
|
@ -107,6 +108,7 @@ const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
<Table editor={editor} />
|
||||
<Katex editor={editor} />
|
||||
<Mind editor={editor} />
|
||||
<Excalidraw editor={editor} />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Dragable } from 'tiptap/core/extensions/dragable';
|
|||
import { Dropcursor } from 'tiptap/core/extensions/dropcursor';
|
||||
import { Emoji } from 'tiptap/core/extensions/emoji';
|
||||
import { EventEmitter } from 'tiptap/core/extensions/event-emitter';
|
||||
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||
import { Flow } from 'tiptap/core/extensions/flow';
|
||||
import { Focus } from 'tiptap/core/extensions/focus';
|
||||
import { FontSize } from 'tiptap/core/extensions/font-size';
|
||||
|
@ -102,6 +103,7 @@ export const CollaborationKit = [
|
|||
Color,
|
||||
ColorHighlighter,
|
||||
Dropcursor,
|
||||
Excalidraw,
|
||||
EventEmitter,
|
||||
Focus,
|
||||
FontSize,
|
||||
|
|
|
@ -44,6 +44,7 @@ importers:
|
|||
'@douyinfe/semi-icons': ^2.3.1
|
||||
'@douyinfe/semi-next': ^2.3.1
|
||||
'@douyinfe/semi-ui': ^2.3.1
|
||||
'@excalidraw/excalidraw': ^0.12.0
|
||||
'@hocuspocus/provider': ^1.0.0-alpha.29
|
||||
'@think/config': workspace:^1.0.0
|
||||
'@think/constants': workspace:^1.0.0
|
||||
|
@ -157,6 +158,7 @@ importers:
|
|||
'@douyinfe/semi-icons': 2.3.1_react@17.0.2
|
||||
'@douyinfe/semi-next': 2.3.1
|
||||
'@douyinfe/semi-ui': 2.3.1_wnecvl2xit6hykxlpfa3byfhr4
|
||||
'@excalidraw/excalidraw': 0.12.0_sfoxds7t5ydpegc3knd667wn6m
|
||||
'@hocuspocus/provider': 1.0.0-alpha.29
|
||||
'@think/config': link:../config
|
||||
'@think/constants': link:../constants
|
||||
|
@ -1953,6 +1955,17 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@excalidraw/excalidraw/0.12.0_sfoxds7t5ydpegc3knd667wn6m:
|
||||
resolution: {integrity: sha512-xMPmKmOEgKij43k5m6Koaevb+SBw6La7MT9UDY8Iq7nQCMhA1HQwcUURfSkZ3ERibdQmMsAGtjSLbkX7hrA3+A==}
|
||||
peerDependencies:
|
||||
react: ^17.0.2
|
||||
react-dom: ^17.0.2
|
||||
dependencies:
|
||||
dotenv: 10.0.0
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
dev: false
|
||||
|
||||
/@hocuspocus/common/1.0.0-alpha.4:
|
||||
resolution: {integrity: sha512-LvKj+ASSWnvjFB7n2bl7BUGFKF9XFFP1oA3/XmKl3c7wUIvoN1Ir3sX8XnN6qBA3S2CoruEHHk+KPS6zMSMfHA==}
|
||||
dev: false
|
||||
|
|
Loading…
Reference in New Issue