forked from github/plane
fix: link preview editor (#3335)
* feat: added link preview plugin in document editor * fix: readonly editor page renderer css * fix: autolink issue with links * chore: added floating UI * feat: added link preview components * feat: added floating UI to page renderer for link previews * feat: added actionCompleteHandler to page renderer * chore: Lock file changes * fix: regex security error * chore: updated radix with lucid icons --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
59fb371e3d
commit
e6b31e2550
@ -71,6 +71,7 @@ export const CoreEditorExtensions = (
|
|||||||
CustomKeymap,
|
CustomKeymap,
|
||||||
ListKeymap,
|
ListKeymap,
|
||||||
TiptapLink.configure({
|
TiptapLink.configure({
|
||||||
|
autolink: false,
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url) => isValidHttpUrl(url),
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.26.4",
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@plane/editor-extensions": "*",
|
"@plane/editor-extensions": "*",
|
||||||
"@plane/ui": "*",
|
"@plane/ui": "*",
|
||||||
@ -36,6 +37,7 @@
|
|||||||
"@tiptap/pm": "^2.1.13",
|
"@tiptap/pm": "^2.1.13",
|
||||||
"@tiptap/suggestion": "^2.1.13",
|
"@tiptap/suggestion": "^2.1.13",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
|
"lucide-react": "^0.309.0",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import { isValidHttpUrl } from "@plane/editor-core";
|
||||||
|
import { Node } from "@tiptap/pm/model";
|
||||||
|
import { Link2Off } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { LinkViewProps } from "./link-view";
|
||||||
|
|
||||||
|
const InputView = ({
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
placeholder,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
defaultValue: string;
|
||||||
|
placeholder: string;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}) => (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="inline-block font-semibold text-xs text-custom-text-400">{label}</label>
|
||||||
|
<input
|
||||||
|
placeholder={placeholder}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="w-[280px] outline-none bg-custom-background-90 text-custom-text-900 text-sm"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LinkEditView = ({
|
||||||
|
viewProps,
|
||||||
|
}: {
|
||||||
|
viewProps: LinkViewProps;
|
||||||
|
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||||
|
}) => {
|
||||||
|
const { editor, from, to } = viewProps;
|
||||||
|
|
||||||
|
const [positionRef, setPositionRef] = useState({ from: from, to: to });
|
||||||
|
const [localUrl, setLocalUrl] = useState(viewProps.url);
|
||||||
|
|
||||||
|
const linkRemoved = useRef<Boolean>();
|
||||||
|
|
||||||
|
const getText = (from: number, to: number) => {
|
||||||
|
const text = editor.state.doc.textBetween(from, to, "\n");
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidUrl = (urlString: string) => {
|
||||||
|
var urlPattern = new RegExp(
|
||||||
|
"^(https?:\\/\\/)?" + // validate protocol
|
||||||
|
"([\\w-]+\\.)+[\\w-]{2,}" + // validate domain name
|
||||||
|
"|((\\d{1,3}\\.){3}\\d{1,3})" + // validate IP (v4) address
|
||||||
|
"(\\:\\d+)?(\\/[-\\w.%]+)*" + // validate port and path
|
||||||
|
"(\\?[;&\\w.%=-]*)?" + // validate query string
|
||||||
|
"(\\#[-\\w]*)?$", // validate fragment locator
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
const regexTest = urlPattern.test(urlString);
|
||||||
|
const urlTest = isValidHttpUrl(urlString); // Ensure you have defined isValidHttpUrl
|
||||||
|
return regexTest && urlTest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateLink = (url: string) => {
|
||||||
|
setLocalUrl(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (linkRemoved.current) return;
|
||||||
|
|
||||||
|
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
|
||||||
|
|
||||||
|
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||||
|
editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url })));
|
||||||
|
},
|
||||||
|
[localUrl]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateText = (text: string) => {
|
||||||
|
if (text === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = editor.view.state.doc.nodeAt(from) as Node;
|
||||||
|
if (!node) return;
|
||||||
|
const marks = node.marks;
|
||||||
|
if (!marks) return;
|
||||||
|
|
||||||
|
editor.chain().setTextSelection(from).run();
|
||||||
|
|
||||||
|
editor.chain().deleteRange({ from: positionRef.from, to: positionRef.to }).run();
|
||||||
|
editor.chain().insertContent(text).run();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({
|
||||||
|
from: from,
|
||||||
|
to: from + text.length,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
setPositionRef({ from: from, to: from + text.length });
|
||||||
|
|
||||||
|
marks.forEach((mark) => {
|
||||||
|
editor.chain().setMark(mark.type.name, mark.attrs).run();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLink = () => {
|
||||||
|
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||||
|
linkRemoved.current = true;
|
||||||
|
viewProps.onActionCompleteHandler({
|
||||||
|
title: "Link successfully removed",
|
||||||
|
message: "The link was removed from the text.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
viewProps.closeLinkView();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && viewProps.closeLinkView()}
|
||||||
|
className="shadow-md rounded p-2 flex flex-col gap-3 bg-custom-background-90 border-custom-border-100 border-2"
|
||||||
|
>
|
||||||
|
<InputView
|
||||||
|
label={"URL"}
|
||||||
|
placeholder={"Enter or paste URL"}
|
||||||
|
defaultValue={localUrl}
|
||||||
|
onChange={(e) => handleUpdateLink(e.target.value)}
|
||||||
|
/>
|
||||||
|
<InputView
|
||||||
|
label={"Text"}
|
||||||
|
placeholder={"Enter Text to display"}
|
||||||
|
defaultValue={getText(from, to)}
|
||||||
|
onChange={(e) => handleUpdateText(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="mb-1 bg-custom-border-300 h-[1px] w-full gap-2" />
|
||||||
|
<div className="flex text-sm text-custom-text-800 gap-2 items-center">
|
||||||
|
<Link2Off size={14} className="inline-block" />
|
||||||
|
<button onClick={() => removeLink()} className="cursor-pointer">
|
||||||
|
Remove Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { LinkViewProps } from "./link-view";
|
||||||
|
|
||||||
|
export const LinkInputView = ({
|
||||||
|
viewProps,
|
||||||
|
switchView,
|
||||||
|
}: {
|
||||||
|
viewProps: LinkViewProps;
|
||||||
|
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||||
|
}) => <p>LinkInputView</p>;
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Copy, GlobeIcon, Link2Off, PencilIcon } from "lucide-react";
|
||||||
|
import { LinkViewProps } from "./link-view";
|
||||||
|
|
||||||
|
export const LinkPreview = ({
|
||||||
|
viewProps,
|
||||||
|
switchView,
|
||||||
|
}: {
|
||||||
|
viewProps: LinkViewProps;
|
||||||
|
switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void;
|
||||||
|
}) => {
|
||||||
|
const { editor, from, to, url } = viewProps;
|
||||||
|
|
||||||
|
const removeLink = () => {
|
||||||
|
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||||
|
viewProps.onActionCompleteHandler({
|
||||||
|
title: "Link successfully removed",
|
||||||
|
message: "The link was removed from the text.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
viewProps.closeLinkView();
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLinkToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
viewProps.onActionCompleteHandler({
|
||||||
|
title: "Link successfully copied",
|
||||||
|
message: "The link was copied to the clipboard.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
viewProps.closeLinkView();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute left-0 top-0 max-w-max">
|
||||||
|
<div className="shadow-md items-center rounded p-2 flex gap-3 bg-custom-background-90 border-custom-border-100 border-2 text-custom-text-300 text-xs">
|
||||||
|
<GlobeIcon size={14} className="inline-block" />
|
||||||
|
<p>{url.length > 40 ? url.slice(0, 40) + "..." : url}</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={copyLinkToClipboard} className="cursor-pointer">
|
||||||
|
<Copy size={14} className="inline-block" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => switchView("LinkEditView")} className="cursor-pointer">
|
||||||
|
<PencilIcon size={14} className="inline-block" />
|
||||||
|
</button>
|
||||||
|
<button onClick={removeLink} className="cursor-pointer">
|
||||||
|
<Link2Off size={14} className="inline-block" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Editor } from "@tiptap/react";
|
||||||
|
import { CSSProperties, useEffect, useState } from "react";
|
||||||
|
import { LinkEditView } from "./link-edit-view";
|
||||||
|
import { LinkInputView } from "./link-input-view";
|
||||||
|
import { LinkPreview } from "./link-preview";
|
||||||
|
|
||||||
|
export interface LinkViewProps {
|
||||||
|
view?: "LinkPreview" | "LinkEditView" | "LinkInputView";
|
||||||
|
editor: Editor;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
url: string;
|
||||||
|
closeLinkView: () => void;
|
||||||
|
onActionCompleteHandler: (action: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
type: "success" | "error" | "warning" | "info";
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkView = (props: LinkViewProps & { style: CSSProperties }) => {
|
||||||
|
const [currentView, setCurrentView] = useState(props.view ?? "LinkInputView");
|
||||||
|
const [prevFrom, setPrevFrom] = useState(props.from);
|
||||||
|
|
||||||
|
const switchView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => {
|
||||||
|
setCurrentView(view);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.from !== prevFrom) {
|
||||||
|
setCurrentView("LinkPreview");
|
||||||
|
setPrevFrom(props.from);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderView = () => {
|
||||||
|
switch (currentView) {
|
||||||
|
case "LinkPreview":
|
||||||
|
return <LinkPreview viewProps={props} switchView={switchView} />;
|
||||||
|
case "LinkEditView":
|
||||||
|
return <LinkEditView viewProps={props} switchView={switchView} />;
|
||||||
|
case "LinkInputView":
|
||||||
|
return <LinkInputView viewProps={props} switchView={switchView} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderView();
|
||||||
|
};
|
@ -1,12 +1,30 @@
|
|||||||
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core";
|
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core";
|
||||||
import { Editor } from "@tiptap/react";
|
import { Node } from "@tiptap/pm/model";
|
||||||
import { useState } from "react";
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
import { Editor, ReactRenderer } from "@tiptap/react";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { DocumentDetails } from "src/types/editor-types";
|
import { DocumentDetails } from "src/types/editor-types";
|
||||||
|
import { LinkView, LinkViewProps } from "./links/link-view";
|
||||||
|
import {
|
||||||
|
autoUpdate,
|
||||||
|
computePosition,
|
||||||
|
flip,
|
||||||
|
hide,
|
||||||
|
shift,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useInteractions,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
|
||||||
type IPageRenderer = {
|
type IPageRenderer = {
|
||||||
documentDetails: DocumentDetails;
|
documentDetails: DocumentDetails;
|
||||||
updatePageTitle: (title: string) => Promise<void>;
|
updatePageTitle: (title: string) => Promise<void>;
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
onActionCompleteHandler: (action: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
type: "success" | "error" | "warning" | "info";
|
||||||
|
}) => void;
|
||||||
editorClassNames: string;
|
editorClassNames: string;
|
||||||
editorContentCustomClassNames?: string;
|
editorContentCustomClassNames?: string;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
@ -29,6 +47,23 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||||||
|
|
||||||
const [pageTitle, setPagetitle] = useState(documentDetails.title);
|
const [pageTitle, setPagetitle] = useState(documentDetails.title);
|
||||||
|
|
||||||
|
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [coordinates, setCoordinates] = useState<{ x: number; y: number }>();
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
middleware: [flip(), shift(), hide({ strategy: "referenceHidden" })],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismiss = useDismiss(context, {
|
||||||
|
ancestorScroll: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getFloatingProps } = useInteractions([dismiss]);
|
||||||
|
|
||||||
const debouncedUpdatePageTitle = debounce(updatePageTitle, 300);
|
const debouncedUpdatePageTitle = debounce(updatePageTitle, 300);
|
||||||
|
|
||||||
const handlePageTitleChange = (title: string) => {
|
const handlePageTitleChange = (title: string) => {
|
||||||
@ -36,8 +71,101 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||||||
debouncedUpdatePageTitle(title);
|
debouncedUpdatePageTitle(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [cleanup, setcleanup] = useState(() => () => {});
|
||||||
|
|
||||||
|
const floatingElementRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const closeLinkView = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchLinkView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => {
|
||||||
|
if (!linkViewProps) return;
|
||||||
|
setLinkViewProps({
|
||||||
|
...linkViewProps,
|
||||||
|
view: view,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLinkHover = useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
if (!editor) return;
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const view = editor.view as EditorView;
|
||||||
|
|
||||||
|
if (!target || !view) return;
|
||||||
|
const pos = view.posAtDOM(target, 0);
|
||||||
|
if (!pos || pos < 0) return;
|
||||||
|
|
||||||
|
if (target.nodeName !== "A") return;
|
||||||
|
|
||||||
|
const node = view.state.doc.nodeAt(pos) as Node;
|
||||||
|
if (!node || !node.isAtom) return;
|
||||||
|
|
||||||
|
// we need to check if any of the marks are links
|
||||||
|
const marks = node.marks;
|
||||||
|
|
||||||
|
if (!marks) return;
|
||||||
|
|
||||||
|
const linkMark = marks.find((mark) => mark.type.name === "link");
|
||||||
|
|
||||||
|
if (!linkMark) return;
|
||||||
|
|
||||||
|
if (floatingElementRef.current) {
|
||||||
|
floatingElementRef.current?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanup) cleanup();
|
||||||
|
|
||||||
|
const href = linkMark.attrs.href;
|
||||||
|
const componentLink = new ReactRenderer(LinkView, {
|
||||||
|
props: {
|
||||||
|
view: "LinkPreview",
|
||||||
|
url: href,
|
||||||
|
editor: editor,
|
||||||
|
from: pos,
|
||||||
|
to: pos + node.nodeSize,
|
||||||
|
},
|
||||||
|
editor,
|
||||||
|
});
|
||||||
|
|
||||||
|
const referenceElement = target as HTMLElement;
|
||||||
|
const floatingElement = componentLink.element as HTMLElement;
|
||||||
|
|
||||||
|
floatingElementRef.current = floatingElement;
|
||||||
|
|
||||||
|
const cleanupFunc = autoUpdate(referenceElement, floatingElement, () => {
|
||||||
|
computePosition(referenceElement, floatingElement, {
|
||||||
|
placement: "bottom",
|
||||||
|
middleware: [
|
||||||
|
flip(),
|
||||||
|
shift(),
|
||||||
|
hide({
|
||||||
|
strategy: "referenceHidden",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}).then(({ x, y }) => {
|
||||||
|
setCoordinates({ x: x - 300, y: y - 50 });
|
||||||
|
setIsOpen(true);
|
||||||
|
setLinkViewProps({
|
||||||
|
onActionCompleteHandler: props.onActionCompleteHandler,
|
||||||
|
closeLinkView: closeLinkView,
|
||||||
|
view: "LinkPreview",
|
||||||
|
url: href,
|
||||||
|
editor: editor,
|
||||||
|
from: pos,
|
||||||
|
to: pos + node.nodeSize,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setcleanup(cleanupFunc);
|
||||||
|
},
|
||||||
|
[editor, cleanup]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full pb-64 pl-7 pt-5">
|
<div className="w-full pb-64 pl-7 pt-5 page-renderer">
|
||||||
{!readonly ? (
|
{!readonly ? (
|
||||||
<input
|
<input
|
||||||
onChange={(e) => handlePageTitleChange(e.target.value)}
|
onChange={(e) => handlePageTitleChange(e.target.value)}
|
||||||
@ -52,11 +180,20 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex h-full w-full flex-col pr-5">
|
<div className="flex relative h-full w-full flex-col pr-5 editor-renderer" onMouseOver={handleLinkHover}>
|
||||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
</div>
|
</div>
|
||||||
|
{isOpen && linkViewProps && coordinates && (
|
||||||
|
<div
|
||||||
|
style={{ ...floatingStyles, left: `${coordinates.x}px`, top: `${coordinates.y}px` }}
|
||||||
|
className={`absolute`}
|
||||||
|
ref={refs.setFloating}
|
||||||
|
>
|
||||||
|
<LinkView {...linkViewProps} style={floatingStyles} {...getFloatingProps()} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -151,6 +151,7 @@ const DocumentEditor = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||||
<PageRenderer
|
<PageRenderer
|
||||||
|
onActionCompleteHandler={onActionCompleteHandler}
|
||||||
readonly={false}
|
readonly={false}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||||
|
@ -109,8 +109,9 @@ const DocumentReadOnlyEditor = ({
|
|||||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
|
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
|
||||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||||
<PageRenderer
|
<PageRenderer
|
||||||
|
onActionCompleteHandler={onActionCompleteHandler}
|
||||||
updatePageTitle={() => Promise.resolve()}
|
updatePageTitle={() => Promise.resolve()}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
|
30
yarn.lock
30
yarn.lock
@ -1358,14 +1358,23 @@
|
|||||||
"@floating-ui/core" "^1.4.2"
|
"@floating-ui/core" "^1.4.2"
|
||||||
"@floating-ui/utils" "^0.1.3"
|
"@floating-ui/utils" "^0.1.3"
|
||||||
|
|
||||||
"@floating-ui/react-dom@^2.0.4":
|
"@floating-ui/react-dom@^2.0.3", "@floating-ui/react-dom@^2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.4.tgz#b076fafbdfeb881e1d86ae748b7ff95150e9f3ec"
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.4.tgz#b076fafbdfeb881e1d86ae748b7ff95150e9f3ec"
|
||||||
integrity sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==
|
integrity sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom" "^1.5.1"
|
"@floating-ui/dom" "^1.5.1"
|
||||||
|
|
||||||
"@floating-ui/utils@^0.1.3":
|
"@floating-ui/react@^0.26.4":
|
||||||
|
version "0.26.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.4.tgz#7626667d2dabc80e2696b500df7f1a348d7ec7a8"
|
||||||
|
integrity sha512-pRiEz+SiPyfTcckAtLkEf3KJ/sUbB4X4fWMcDm27HT2kfAq+dH+hMc2VoOkNaGpDE35a2PKo688ugWeHaToL3g==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.0.3"
|
||||||
|
"@floating-ui/utils" "^0.1.5"
|
||||||
|
tabbable "^6.0.1"
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.1.3", "@floating-ui/utils@^0.1.5":
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
||||||
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
||||||
@ -1919,6 +1928,11 @@
|
|||||||
"@radix-ui/react-primitive" "1.0.0"
|
"@radix-ui/react-primitive" "1.0.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-icons@^1.3.0":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
|
||||||
|
integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==
|
||||||
|
|
||||||
"@radix-ui/react-id@1.0.0":
|
"@radix-ui/react-id@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
|
||||||
@ -2793,7 +2807,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
"@types/react@*", "@types/react@^18.2.42":
|
||||||
version "18.2.42"
|
version "18.2.42"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||||
@ -6198,6 +6212,11 @@ lucide-react@^0.294.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.294.0.tgz#dc406e1e7e2f722cf93218fe5b31cf3c95778817"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.294.0.tgz#dc406e1e7e2f722cf93218fe5b31cf3c95778817"
|
||||||
integrity sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==
|
integrity sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==
|
||||||
|
|
||||||
|
lucide-react@^0.309.0:
|
||||||
|
version "0.309.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.309.0.tgz#7369893cb4b074a0a0b1d3acdc6fd9a8bdb5add1"
|
||||||
|
integrity sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==
|
||||||
|
|
||||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||||
version "0.25.9"
|
version "0.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||||
@ -8323,6 +8342,11 @@ swr@^2.1.3, swr@^2.2.2:
|
|||||||
client-only "^0.0.1"
|
client-only "^0.0.1"
|
||||||
use-sync-external-store "^1.2.0"
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
|
tabbable@^6.0.1:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||||
|
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||||
|
|
||||||
table@^6.0.9:
|
table@^6.0.9:
|
||||||
version "6.8.1"
|
version "6.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
|
resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
|
||||||
|
Loading…
Reference in New Issue
Block a user