forked from github/plane
fix: rich text editor (#1008)
* fix: undo/redo, placeholder overlapping with text, horizontal cursor refractor: removed a lot of state-management that was not required * fix: forwarding ref to remirror for getting extra helper functions * fix: icon type error * fix: value type not supported error on page block * style: spacing, and UX for add link --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
df96d40cfa
commit
4f2b106852
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, forwardRef, useRef } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@ -35,6 +35,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||||
|
|
||||||
|
const WrappedRemirrorRichTextEditor = forwardRef<IRemirrorRichTextEditor, IRemirrorRichTextEditor>(
|
||||||
|
(props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />
|
||||||
|
);
|
||||||
|
|
||||||
|
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||||
|
|
||||||
export const GptAssistantModal: React.FC<Props> = ({
|
export const GptAssistantModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
@ -125,7 +133,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
isOpen ? "block" : "hidden"
|
isOpen ? "block" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{((content && content !== "") || htmlContent !== "<p></p>") && (
|
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||||
<div className="remirror-section text-sm">
|
<div className="remirror-section text-sm">
|
||||||
Content:
|
Content:
|
||||||
<RemirrorRichTextEditor
|
<RemirrorRichTextEditor
|
||||||
|
@ -13,7 +13,7 @@ import { FormProvider, useForm } from "react-hook-form";
|
|||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||||
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
|
import { CogIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import jiraImporterService from "services/integration/jira.service";
|
import jiraImporterService from "services/integration/jira.service";
|
||||||
@ -40,7 +40,7 @@ import { IJiraImporterForm } from "types";
|
|||||||
const integrationWorkflowData: Array<{
|
const integrationWorkflowData: Array<{
|
||||||
title: string;
|
title: string;
|
||||||
key: TJiraIntegrationSteps;
|
key: TJiraIntegrationSteps;
|
||||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
icon: React.FC<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
title: "Configure",
|
title: "Configure",
|
||||||
|
@ -26,6 +26,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
|||||||
</Loader>
|
</Loader>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||||
|
|
||||||
|
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||||
|
IRemirrorRichTextEditor,
|
||||||
|
IRemirrorRichTextEditor
|
||||||
|
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||||
|
|
||||||
|
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueComment> = {
|
const defaultValues: Partial<IIssueComment> = {
|
||||||
comment_json: "",
|
comment_json: "",
|
||||||
@ -41,6 +49,8 @@ export const AddComment: React.FC = () => {
|
|||||||
reset,
|
reset,
|
||||||
} = useForm<IIssueComment>({ defaultValues });
|
} = useForm<IIssueComment>({ defaultValues });
|
||||||
|
|
||||||
|
const editorRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
@ -61,6 +71,7 @@ export const AddComment: React.FC = () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
|
editorRef.current?.clearEditor();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -79,11 +90,12 @@ export const AddComment: React.FC = () => {
|
|||||||
name="comment_json"
|
name="comment_json"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<RemirrorRichTextEditor
|
<WrappedRemirrorRichTextEditor
|
||||||
value={value}
|
value={value}
|
||||||
onJSONChange={(jsonValue) => setValue("comment_json", jsonValue)}
|
onJSONChange={(jsonValue) => setValue("comment_json", jsonValue)}
|
||||||
onHTMLChange={(htmlValue) => setValue("comment_html", htmlValue)}
|
onHTMLChange={(htmlValue) => setValue("comment_html", htmlValue)}
|
||||||
placeholder="Enter your comment..."
|
placeholder="Enter your comment..."
|
||||||
|
ref={editorRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -18,6 +18,15 @@ import type { IIssueComment } from "types";
|
|||||||
|
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false });
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false });
|
||||||
|
|
||||||
|
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||||
|
|
||||||
|
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||||
|
IRemirrorRichTextEditor,
|
||||||
|
IRemirrorRichTextEditor
|
||||||
|
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||||
|
|
||||||
|
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comment: IIssueComment;
|
comment: IIssueComment;
|
||||||
onSubmit: (comment: IIssueComment) => void;
|
onSubmit: (comment: IIssueComment) => void;
|
||||||
@ -27,6 +36,9 @@ type Props = {
|
|||||||
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
|
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const editorRef = React.useRef<any>(null);
|
||||||
|
const showEditorRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -42,6 +54,10 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
|||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
onSubmit(formData);
|
onSubmit(formData);
|
||||||
|
console.log(formData);
|
||||||
|
|
||||||
|
editorRef.current?.setEditorValue(formData.comment_json);
|
||||||
|
showEditorRef.current?.setEditorValue(formData.comment_json);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,15 +101,18 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="issue-comments-section p-0">
|
<div className="issue-comments-section p-0">
|
||||||
{isEditing ? (
|
<form
|
||||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
|
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
||||||
<RemirrorRichTextEditor
|
onSubmit={handleSubmit(onEnter)}
|
||||||
|
>
|
||||||
|
<WrappedRemirrorRichTextEditor
|
||||||
value={comment.comment_html}
|
value={comment.comment_html}
|
||||||
onBlur={(jsonValue, htmlValue) => {
|
onBlur={(jsonValue, htmlValue) => {
|
||||||
setValue("comment_json", jsonValue);
|
setValue("comment_json", jsonValue);
|
||||||
setValue("comment_html", htmlValue);
|
setValue("comment_html", htmlValue);
|
||||||
}}
|
}}
|
||||||
placeholder="Enter Your comment..."
|
placeholder="Enter Your comment..."
|
||||||
|
ref={editorRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-1 self-end">
|
<div className="flex gap-1 self-end">
|
||||||
<button
|
<button
|
||||||
@ -112,14 +131,15 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||||
<RemirrorRichTextEditor
|
<WrappedRemirrorRichTextEditor
|
||||||
value={comment.comment_html}
|
value={comment.comment_html}
|
||||||
editable={false}
|
editable={false}
|
||||||
noBorder
|
noBorder
|
||||||
customClassName="text-xs border border-brand-base bg-brand-base"
|
customClassName="text-xs border border-brand-base bg-brand-base"
|
||||||
|
ref={showEditorRef}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.id === comment.actor && (
|
{user?.id === comment.actor && (
|
||||||
|
@ -6,6 +6,8 @@ import dynamic from "next/dynamic";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// contexts
|
// contexts
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
// hooks
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
// components
|
// components
|
||||||
import { Loader, TextArea } from "components/ui";
|
import { Loader, TextArea } from "components/ui";
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
@ -36,6 +38,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
|||||||
|
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
@ -82,7 +86,10 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
|
|
||||||
reset(issue);
|
reset({
|
||||||
|
...issue,
|
||||||
|
description: issue.description,
|
||||||
|
});
|
||||||
}, [issue, reset]);
|
}, [issue, reset]);
|
||||||
|
|
||||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
||||||
@ -131,7 +138,10 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
|||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => {
|
||||||
|
if (!value || !watch("description_html")) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
<RemirrorRichTextEditor
|
<RemirrorRichTextEditor
|
||||||
value={
|
value={
|
||||||
!value ||
|
!value ||
|
||||||
@ -140,13 +150,20 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
|||||||
? watch("description_html")
|
? watch("description_html")
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
onJSONChange={(jsonValue) => {
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
setShowAlert(true);
|
||||||
|
setValue("description", jsonValue);
|
||||||
|
}}
|
||||||
|
onHTMLChange={(htmlValue) => {
|
||||||
|
setShowAlert(true);
|
||||||
|
setValue("description_html", htmlValue);
|
||||||
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
handleSubmit(handleDescriptionFormSubmit)()
|
handleSubmit(handleDescriptionFormSubmit)()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
setShowAlert(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@ -155,7 +172,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
|||||||
placeholder="Describe the issue..."
|
placeholder="Describe the issue..."
|
||||||
editable={!isNotAllowed}
|
editable={!isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute -bottom-8 right-0 text-sm text-brand-secondary ${
|
className={`absolute -bottom-8 right-0 text-sm text-brand-secondary ${
|
||||||
|
@ -53,7 +53,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
|||||||
const defaultValues: Partial<IIssue> = {
|
const defaultValues: Partial<IIssue> = {
|
||||||
project: "",
|
project: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: { type: "doc", content: [] },
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
description_html: "<p></p>",
|
description_html: "<p></p>",
|
||||||
estimate_point: null,
|
estimate_point: null,
|
||||||
state: "",
|
state: "",
|
||||||
@ -132,7 +139,14 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
description: "",
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
description_html: "<p></p>",
|
description_html: "<p></p>",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -34,8 +34,8 @@ type Props = {
|
|||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: "",
|
name: "",
|
||||||
description: { type: "doc", content: [] },
|
description: null,
|
||||||
description_html: "<p></p>",
|
description_html: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
@ -46,6 +46,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
|||||||
</Loader>
|
</Loader>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||||
|
|
||||||
|
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||||
|
IRemirrorRichTextEditor,
|
||||||
|
IRemirrorRichTextEditor
|
||||||
|
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||||
|
|
||||||
|
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||||
|
|
||||||
export const CreateUpdateBlockInline: React.FC<Props> = ({
|
export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||||
handleClose,
|
handleClose,
|
||||||
@ -57,6 +65,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, pageId } = router.query;
|
const { workspaceSlug, projectId, pageId } = router.query;
|
||||||
|
|
||||||
@ -97,6 +107,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
(prevData) => [...(prevData as IPageBlock[]), res],
|
(prevData) => [...(prevData as IPageBlock[]), res],
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
editorRef.current?.clearEditor();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -135,6 +146,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(PAGE_BLOCKS_LIST(pageId as string));
|
mutate(PAGE_BLOCKS_LIST(pageId as string));
|
||||||
|
editorRef.current?.setEditorValue(res.description);
|
||||||
if (data.issue && data.sync)
|
if (data.issue && data.sync)
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, data.issue, {
|
.patchIssue(workspaceSlug as string, projectId as string, data.issue, {
|
||||||
@ -200,8 +212,14 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description:
|
||||||
description_html: data.description_html,
|
!data.description || data.description === ""
|
||||||
|
? {
|
||||||
|
type: "doc",
|
||||||
|
content: [{ type: "paragraph" }],
|
||||||
|
}
|
||||||
|
: data.description,
|
||||||
|
description_html: data.description_html ?? "<p></p>",
|
||||||
});
|
});
|
||||||
}, [reset, data, focus, setFocus]);
|
}, [reset, data, focus, setFocus]);
|
||||||
|
|
||||||
@ -254,12 +272,33 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => {
|
||||||
|
if (!data)
|
||||||
|
return (
|
||||||
|
<WrappedRemirrorRichTextEditor
|
||||||
|
value={value}
|
||||||
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
|
placeholder="Write something..."
|
||||||
|
customClassName="text-sm"
|
||||||
|
noBorder
|
||||||
|
borderOnFocus={false}
|
||||||
|
ref={editorRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
else if (!value || !watch("description_html"))
|
||||||
|
return (
|
||||||
|
<div className="h-32 w-full flex items-center justify-center text-brand-secondary text-sm" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<RemirrorRichTextEditor
|
<RemirrorRichTextEditor
|
||||||
value={
|
value={
|
||||||
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
value && value !== "" && Object.keys(value).length > 0
|
||||||
|
? value
|
||||||
|
: watch("description_html") && watch("description_html") !== ""
|
||||||
? watch("description_html")
|
? watch("description_html")
|
||||||
: value
|
: { type: "doc", content: [{ type: "paragraph" }] }
|
||||||
}
|
}
|
||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
@ -268,7 +307,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
noBorder
|
noBorder
|
||||||
borderOnFocus={false}
|
borderOnFocus={false}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="m-2 mt-6 flex">
|
<div className="m-2 mt-6 flex">
|
||||||
<button
|
<button
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, FC, useState, useEffect } from "react";
|
import { useCallback, useState, useImperativeHandle } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { InvalidContentHandler } from "remirror";
|
import { InvalidContentHandler } from "remirror";
|
||||||
import {
|
import {
|
||||||
BoldExtension,
|
BoldExtension,
|
||||||
@ -27,14 +28,14 @@ import {
|
|||||||
EditorComponent,
|
EditorComponent,
|
||||||
OnChangeJSON,
|
OnChangeJSON,
|
||||||
OnChangeHTML,
|
OnChangeHTML,
|
||||||
|
FloatingToolbar,
|
||||||
|
FloatingWrapper,
|
||||||
} from "@remirror/react";
|
} from "@remirror/react";
|
||||||
import { TableExtension } from "@remirror/extension-react-tables";
|
import { TableExtension } from "@remirror/extension-react-tables";
|
||||||
// tlds
|
// tlds
|
||||||
import tlds from "tlds";
|
import tlds from "tlds";
|
||||||
// services
|
// services
|
||||||
import fileService from "services/file.service";
|
import fileService from "services/file.service";
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// components
|
// components
|
||||||
import { CustomFloatingToolbar } from "./toolbar/float-tool-tip";
|
import { CustomFloatingToolbar } from "./toolbar/float-tool-tip";
|
||||||
import { MentionAutoComplete } from "./mention-autocomplete";
|
import { MentionAutoComplete } from "./mention-autocomplete";
|
||||||
@ -53,12 +54,10 @@ export interface IRemirrorRichTextEditor {
|
|||||||
gptOption?: boolean;
|
gptOption?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
borderOnFocus?: boolean;
|
borderOnFocus?: boolean;
|
||||||
|
forwardedRef?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-duplicate-imports
|
const RemirrorRichTextEditor: React.FC<IRemirrorRichTextEditor> = (props) => {
|
||||||
import { FloatingWrapper, FloatingToolbar } from "@remirror/react";
|
|
||||||
|
|
||||||
const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|
||||||
const {
|
const {
|
||||||
placeholder,
|
placeholder,
|
||||||
mentions = [],
|
mentions = [],
|
||||||
@ -73,11 +72,10 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
gptOption = false,
|
gptOption = false,
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
borderOnFocus = true,
|
borderOnFocus = true,
|
||||||
|
forwardedRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [imageLoader, setImageLoader] = useState(false);
|
const [disableToolbar, setDisableToolbar] = useState(false);
|
||||||
const [jsonValue, setJsonValue] = useState<any>();
|
|
||||||
const [htmlValue, setHtmlValue] = useState<any>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -91,15 +89,11 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const uploadImageHandler = (value: any): any => {
|
const uploadImageHandler = (value: any): any => {
|
||||||
setImageLoader(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("asset", value[0].file);
|
formData.append("asset", value[0].file);
|
||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
setImageLoader(true);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
() =>
|
() =>
|
||||||
new Promise(async (resolve, reject) => {
|
new Promise(async (resolve, reject) => {
|
||||||
@ -114,7 +108,6 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
src: imageUrl,
|
src: imageUrl,
|
||||||
});
|
});
|
||||||
setImageLoader(false);
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
} catch {
|
} catch {
|
||||||
@ -136,15 +129,25 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
new CalloutExtension({ defaultType: "warn" }),
|
new CalloutExtension({ defaultType: "warn" }),
|
||||||
new CodeBlockExtension(),
|
new CodeBlockExtension(),
|
||||||
new CodeExtension(),
|
new CodeExtension(),
|
||||||
new PlaceholderExtension({ placeholder: placeholder || "Enter text..." }),
|
new PlaceholderExtension({
|
||||||
|
placeholder: placeholder || "Enter text...",
|
||||||
|
emptyNodeClass: "empty-node",
|
||||||
|
}),
|
||||||
new HistoryExtension(),
|
new HistoryExtension(),
|
||||||
new LinkExtension({
|
new LinkExtension({
|
||||||
autoLink: true,
|
autoLink: true,
|
||||||
autoLinkAllowedTLDs: tlds,
|
autoLinkAllowedTLDs: tlds,
|
||||||
|
selectTextOnClick: true,
|
||||||
|
defaultTarget: "_blank",
|
||||||
}),
|
}),
|
||||||
new ImageExtension({
|
new ImageExtension({
|
||||||
enableResizing: true,
|
enableResizing: true,
|
||||||
uploadHandler: uploadImageHandler,
|
uploadHandler: uploadImageHandler,
|
||||||
|
createPlaceholder() {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "w-full aspect-video bg-brand-surface-2 animate-pulse";
|
||||||
|
return div;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
new DropCursorExtension(),
|
new DropCursorExtension(),
|
||||||
new StrikeExtension(),
|
new StrikeExtension(),
|
||||||
@ -156,38 +159,25 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
}),
|
}),
|
||||||
new TableExtension(),
|
new TableExtension(),
|
||||||
],
|
],
|
||||||
content: !value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
|
content: value,
|
||||||
selection: "start",
|
selection: "start",
|
||||||
stringHandler: "html",
|
stringHandler: "html",
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateState = useCallback(
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
(value: any) => {
|
clearEditor: () => {
|
||||||
|
manager.view.updateState(manager.createState({ content: "", selection: "start" }));
|
||||||
|
},
|
||||||
|
setEditorValue: (value: any) => {
|
||||||
manager.view.updateState(
|
manager.view.updateState(
|
||||||
manager.createState({
|
manager.createState({
|
||||||
content:
|
content: value,
|
||||||
!value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
|
selection: "end",
|
||||||
selection: value === "" ? "start" : manager.view.state.selection,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[manager]
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateState(value);
|
|
||||||
}, [updateState, value]);
|
|
||||||
|
|
||||||
const handleJSONChange = (json: any) => {
|
|
||||||
setJsonValue(json);
|
|
||||||
onJSONChange(json);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHTMLChange = (value: string) => {
|
|
||||||
setHtmlValue(value);
|
|
||||||
onHTMLChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -195,50 +185,51 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
manager={manager}
|
manager={manager}
|
||||||
initialContent={state}
|
initialContent={state}
|
||||||
classNames={[
|
classNames={[
|
||||||
`p-4 relative focus:outline-none rounded-md focus:border-brand-base ${
|
`p-3 relative focus:outline-none rounded-md focus:border-brand-base ${
|
||||||
noBorder ? "" : "border border-brand-base"
|
noBorder ? "" : "border border-brand-base"
|
||||||
} ${
|
} ${
|
||||||
borderOnFocus ? "focus:border border-brand-base" : "focus:border-0"
|
borderOnFocus ? "focus:border border-brand-base" : "focus:border-0"
|
||||||
} ${customClassName}`,
|
} ${customClassName}`,
|
||||||
]}
|
]}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
onBlur={() => {
|
onBlur={(event) => {
|
||||||
onBlur(jsonValue, htmlValue);
|
const html = event.helpers.getHTML();
|
||||||
|
const json = event.helpers.getJSON();
|
||||||
|
|
||||||
|
setDisableToolbar(true);
|
||||||
|
|
||||||
|
onBlur(json, html);
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => setDisableToolbar(false)}
|
||||||
>
|
>
|
||||||
{(!value || value === "" || value?.content?.[0]?.content === undefined) &&
|
<div className="prose prose-brand max-w-full prose-p:my-1">
|
||||||
!(typeof value === "string" && value.includes("<")) &&
|
|
||||||
placeholder && (
|
|
||||||
<p className="pointer-events-none absolute top-4 left-4 text-sm text-brand-secondary">
|
|
||||||
{placeholder}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<EditorComponent />
|
<EditorComponent />
|
||||||
|
|
||||||
{imageLoader && (
|
|
||||||
<div className="p-4">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{editable && (
|
{editable && !disableToolbar && (
|
||||||
<FloatingWrapper
|
<FloatingWrapper
|
||||||
positioner="always"
|
positioner="always"
|
||||||
floatingLabel="Custom Floating Toolbar"
|
|
||||||
renderOutsideEditor
|
renderOutsideEditor
|
||||||
|
floatingLabel="Custom Floating Toolbar"
|
||||||
>
|
>
|
||||||
<FloatingToolbar className="z-[9999] overflow-hidden rounded">
|
<FloatingToolbar className="z-50 overflow-hidden rounded">
|
||||||
<CustomFloatingToolbar gptOption={gptOption} editorState={state} />
|
<CustomFloatingToolbar
|
||||||
|
gptOption={gptOption}
|
||||||
|
editorState={state}
|
||||||
|
setDisableToolbar={setDisableToolbar}
|
||||||
|
/>
|
||||||
</FloatingToolbar>
|
</FloatingToolbar>
|
||||||
</FloatingWrapper>
|
</FloatingWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MentionAutoComplete mentions={mentions} tags={tags} />
|
<MentionAutoComplete mentions={mentions} tags={tags} />
|
||||||
{<OnChangeJSON onChange={handleJSONChange} />}
|
{<OnChangeJSON onChange={onJSONChange} />}
|
||||||
{<OnChangeHTML onChange={handleHTMLChange} />}
|
{<OnChangeHTML onChange={onHTMLChange} />}
|
||||||
</Remirror>
|
</Remirror>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RemirrorRichTextEditor.displayName = "RemirrorRichTextEditor";
|
||||||
|
|
||||||
export default RemirrorRichTextEditor;
|
export default RemirrorRichTextEditor;
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
|
import React, {
|
||||||
|
ChangeEvent,
|
||||||
|
HTMLProps,
|
||||||
|
KeyboardEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from "remirror/extensions";
|
||||||
// buttons
|
// buttons
|
||||||
import {
|
import {
|
||||||
ToggleBoldButton,
|
ToggleBoldButton,
|
||||||
@ -9,19 +22,195 @@ import {
|
|||||||
ToggleCodeButton,
|
ToggleCodeButton,
|
||||||
ToggleHeadingButton,
|
ToggleHeadingButton,
|
||||||
useActive,
|
useActive,
|
||||||
|
CommandButton,
|
||||||
|
useAttrs,
|
||||||
|
useChainedCommands,
|
||||||
|
useCurrentSelection,
|
||||||
|
useExtensionEvent,
|
||||||
|
useUpdateReason,
|
||||||
} from "@remirror/react";
|
} from "@remirror/react";
|
||||||
import { EditorState } from "remirror";
|
import { EditorState } from "remirror";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
gptOption?: boolean;
|
gptOption?: boolean;
|
||||||
editorState: Readonly<EditorState>;
|
editorState: Readonly<EditorState>;
|
||||||
|
setDisableToolbar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomFloatingToolbar: React.FC<Props> = ({ gptOption, editorState }) => {
|
const useLinkShortcut = () => {
|
||||||
const active = useActive();
|
const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
useExtensionEvent(
|
||||||
|
LinkExtension,
|
||||||
|
"onShortcut",
|
||||||
|
useCallback(
|
||||||
|
(props) => {
|
||||||
|
if (!isEditing) {
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setLinkShortcut(props);
|
||||||
|
},
|
||||||
|
[isEditing]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { linkShortcut, isEditing, setIsEditing };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFloatingLinkState = () => {
|
||||||
|
const chain = useChainedCommands();
|
||||||
|
const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut();
|
||||||
|
const { to, empty } = useCurrentSelection();
|
||||||
|
|
||||||
|
const url = (useAttrs().link()?.href as string) ?? "";
|
||||||
|
const [href, setHref] = useState<string>(url);
|
||||||
|
|
||||||
|
// A positioner which only shows for links.
|
||||||
|
const linkPositioner = useMemo(() => createMarkPositioner({ type: "link" }), []);
|
||||||
|
|
||||||
|
const onRemove = useCallback(() => chain.removeLink().focus().run(), [chain]);
|
||||||
|
|
||||||
|
const updateReason = useUpdateReason();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isEditing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateReason.doc || updateReason.selection) {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHref(url);
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const submitHref = useCallback(() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
const range = linkShortcut ?? undefined;
|
||||||
|
|
||||||
|
if (href === "") {
|
||||||
|
chain.removeLink();
|
||||||
|
} else {
|
||||||
|
chain.updateLink({ href, auto: false }, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.focus(range?.to ?? to).run();
|
||||||
|
}, [setIsEditing, linkShortcut, chain, href, to]);
|
||||||
|
|
||||||
|
const cancelHref = useCallback(() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
}, [setIsEditing]);
|
||||||
|
|
||||||
|
const clickEdit = useCallback(() => {
|
||||||
|
if (empty) {
|
||||||
|
chain.selectLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditing(true);
|
||||||
|
}, [chain, empty, setIsEditing]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
href,
|
||||||
|
setHref,
|
||||||
|
linkShortcut,
|
||||||
|
linkPositioner,
|
||||||
|
isEditing,
|
||||||
|
setIsEditing,
|
||||||
|
clickEdit,
|
||||||
|
onRemove,
|
||||||
|
submitHref,
|
||||||
|
cancelHref,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
href,
|
||||||
|
linkShortcut,
|
||||||
|
linkPositioner,
|
||||||
|
isEditing,
|
||||||
|
clickEdit,
|
||||||
|
onRemove,
|
||||||
|
submitHref,
|
||||||
|
cancelHref,
|
||||||
|
setIsEditing,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DelayAutoFocusInput = ({
|
||||||
|
autoFocus,
|
||||||
|
setDisableToolbar,
|
||||||
|
...rest
|
||||||
|
}: HTMLProps<HTMLInputElement> & {
|
||||||
|
setDisableToolbar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisableToolbar(false);
|
||||||
|
|
||||||
|
const frame = window.requestAnimationFrame(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frame);
|
||||||
|
};
|
||||||
|
}, [autoFocus, setDisableToolbar]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisableToolbar(false);
|
||||||
|
}, [setDisableToolbar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-[99999] flex items-center gap-y-2 divide-x divide-brand-base rounded border border-brand-base bg-brand-surface-2 p-1 px-0.5 shadow-md">
|
<>
|
||||||
|
<label htmlFor="link-input" className="text-sm">
|
||||||
|
Add Link
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
{...rest}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (rest.onKeyDown) rest.onKeyDown(e);
|
||||||
|
setDisableToolbar(false);
|
||||||
|
}}
|
||||||
|
className={`${rest.className} mt-1`}
|
||||||
|
onFocus={() => {
|
||||||
|
setDisableToolbar(false);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setDisableToolbar(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomFloatingToolbar: React.FC<Props> = ({
|
||||||
|
gptOption,
|
||||||
|
editorState,
|
||||||
|
setDisableToolbar,
|
||||||
|
}) => {
|
||||||
|
const { isEditing, setIsEditing, clickEdit, onRemove, submitHref, href, setHref, cancelHref } =
|
||||||
|
useFloatingLinkState();
|
||||||
|
|
||||||
|
const active = useActive();
|
||||||
|
const activeLink = active.link();
|
||||||
|
|
||||||
|
const handleClickEdit = useCallback(() => {
|
||||||
|
clickEdit();
|
||||||
|
}, [clickEdit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-[99999] flex flex-col items-center gap-y-2 divide-x divide-y divide-brand-base rounded border border-brand-base bg-brand-surface-2 p-1 px-0.5 shadow-md">
|
||||||
|
<div className="flex items-center gap-y-2 divide-x divide-brand-base">
|
||||||
<div className="flex items-center gap-x-1 px-2">
|
<div className="flex items-center gap-x-1 px-2">
|
||||||
<ToggleHeadingButton
|
<ToggleHeadingButton
|
||||||
attrs={{
|
attrs={{
|
||||||
@ -63,6 +252,65 @@ export const CustomFloatingToolbar: React.FC<Props> = ({ gptOption, editorState
|
|||||||
<div className="flex items-center gap-x-1 px-2">
|
<div className="flex items-center gap-x-1 px-2">
|
||||||
<ToggleCodeButton />
|
<ToggleCodeButton />
|
||||||
</div>
|
</div>
|
||||||
|
{activeLink ? (
|
||||||
|
<div className="flex items-center gap-x-1 px-2">
|
||||||
|
<CommandButton
|
||||||
|
commandName="openLink"
|
||||||
|
onSelect={() => {
|
||||||
|
window.open(href, "_blank");
|
||||||
|
}}
|
||||||
|
icon="externalLinkFill"
|
||||||
|
enabled
|
||||||
|
/>
|
||||||
|
<CommandButton
|
||||||
|
commandName="updateLink"
|
||||||
|
onSelect={handleClickEdit}
|
||||||
|
icon="pencilLine"
|
||||||
|
enabled
|
||||||
|
/>
|
||||||
|
<CommandButton commandName="removeLink" onSelect={onRemove} icon="linkUnlink" enabled />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommandButton
|
||||||
|
commandName="updateLink"
|
||||||
|
onSelect={() => {
|
||||||
|
if (isEditing) {
|
||||||
|
setIsEditing(false);
|
||||||
|
} else {
|
||||||
|
handleClickEdit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon="link"
|
||||||
|
enabled
|
||||||
|
active={isEditing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="p-2 w-full">
|
||||||
|
<DelayAutoFocusInput
|
||||||
|
autoFocus
|
||||||
|
placeholder="Paste your link here..."
|
||||||
|
id="link-input"
|
||||||
|
setDisableToolbar={setDisableToolbar}
|
||||||
|
className="w-full px-2 py-0.5"
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setHref(e.target.value)}
|
||||||
|
value={href}
|
||||||
|
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const { code } = e;
|
||||||
|
|
||||||
|
if (code === "Enter") {
|
||||||
|
submitHref();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === "Escape") {
|
||||||
|
cancelHref();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
28
apps/app/hooks/use-reload-confirmation.tsx
Normal file
28
apps/app/hooks/use-reload-confirmation.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const useReloadConfirmations = (message?: string) => {
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
|
||||||
|
const handleBeforeUnload = useCallback(
|
||||||
|
(event: BeforeUnloadEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = "";
|
||||||
|
return message ?? "Are you sure you want to leave?";
|
||||||
|
},
|
||||||
|
[message]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAlert) {
|
||||||
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
}, [handleBeforeUnload, showAlert]);
|
||||||
|
|
||||||
|
return { setShowAlert };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useReloadConfirmations;
|
@ -19,6 +19,7 @@
|
|||||||
"@remirror/pm": "^2.0.3",
|
"@remirror/pm": "^2.0.3",
|
||||||
"@remirror/react": "^2.0.24",
|
"@remirror/react": "^2.0.24",
|
||||||
"@sentry/nextjs": "^7.36.0",
|
"@sentry/nextjs": "^7.36.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/react-datepicker": "^4.8.0",
|
"@types/react-datepicker": "^4.8.0",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
@ -61,7 +62,6 @@
|
|||||||
"eslint-config-next": "12.2.2",
|
"eslint-config-next": "12.2.2",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
|
||||||
"tailwindcss": "^3.1.6",
|
"tailwindcss": "^3.1.6",
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
@ -1,205 +1,59 @@
|
|||||||
.ProseMirror {
|
.empty-node::after {
|
||||||
position: relative;
|
content: attr(data-placeholder);
|
||||||
|
color: #ccc;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 15px;
|
||||||
|
margin-left: 1px;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
|
position: relative;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
white-space: break-spaces;
|
-moz-tab-size: 4;
|
||||||
-webkit-font-variant-ligatures: none;
|
tab-size: 4;
|
||||||
font-variant-ligatures: none;
|
-webkit-user-select: text;
|
||||||
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
|
-moz-user-select: text;
|
||||||
}
|
-ms-user-select: text;
|
||||||
|
user-select: text;
|
||||||
.ProseMirror pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror li {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-hideselection *::selection {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.ProseMirror-hideselection *::-moz-selection {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.ProseMirror-hideselection {
|
|
||||||
caret-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-selectednode {
|
|
||||||
outline: 2px solid #8cf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure li selections wrap around markers */
|
|
||||||
|
|
||||||
li.ProseMirror-selectednode {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
cursor: text;
|
||||||
|
line-height: 1.2;
|
||||||
li.ProseMirror-selectednode:after {
|
font-family: inherit;
|
||||||
content: "";
|
font-size: 14px;
|
||||||
position: absolute;
|
color: inherit;
|
||||||
left: -32px;
|
|
||||||
right: -2px;
|
|
||||||
top: -2px;
|
|
||||||
bottom: -2px;
|
|
||||||
border: 2px solid #8cf;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Protect against generic img rules */
|
|
||||||
|
|
||||||
img.ProseMirror-separator {
|
|
||||||
display: inline !important;
|
|
||||||
border: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.ProseMirror-textblock-dropdown {
|
|
||||||
min-width: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu {
|
|
||||||
margin: 0 -4px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-tooltip .ProseMirror-menu {
|
|
||||||
width: -webkit-fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menuitem {
|
|
||||||
margin-right: 3px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menuseparator {
|
|
||||||
border-right: 1px solid #ddd;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown,
|
|
||||||
.ProseMirror-menu-dropdown-menu {
|
|
||||||
font-size: 90%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown {
|
|
||||||
vertical-align: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
padding-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-wrap {
|
|
||||||
padding: 1px 0 1px 4px;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown:after {
|
|
||||||
content: "";
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
border-right: 4px solid transparent;
|
|
||||||
border-top: 4px solid currentColor;
|
|
||||||
opacity: 0.6;
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
top: calc(50% - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-menu,
|
|
||||||
.ProseMirror-menu-submenu {
|
|
||||||
position: absolute;
|
|
||||||
background: white;
|
|
||||||
color: #666;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-menu {
|
|
||||||
z-index: 15;
|
|
||||||
min-width: 6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-item {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 8px 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-item:hover {
|
|
||||||
background: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-submenu-wrap {
|
|
||||||
position: relative;
|
|
||||||
margin-right: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-submenu-label:after {
|
|
||||||
content: "";
|
|
||||||
border-top: 4px solid transparent;
|
|
||||||
border-bottom: 4px solid transparent;
|
|
||||||
border-left: 4px solid currentColor;
|
|
||||||
opacity: 0.6;
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
top: calc(50% - 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-submenu {
|
|
||||||
display: none;
|
|
||||||
min-width: 4em;
|
|
||||||
left: 100%;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-active {
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
|
|
||||||
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-menubar {
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
border-top-right-radius: inherit;
|
|
||||||
position: relative;
|
|
||||||
min-height: 1em;
|
|
||||||
color: #666;
|
|
||||||
padding: 1px 6px;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
border-bottom: 1px solid silver;
|
|
||||||
background: white;
|
|
||||||
z-index: 10;
|
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: visible;
|
appearance: textfield;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-icon {
|
.ProseMirror-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 0.8;
|
line-height: 0.8;
|
||||||
vertical-align: -2px; /* Compensate for padding */
|
vertical-align: -2px;
|
||||||
padding: 2px 8px;
|
color: #666;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: 0 3px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: background 50ms ease-in-out;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||||
|
opacity: 0.3;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-icon svg {
|
.ProseMirror-icon svg {
|
||||||
@ -210,149 +64,6 @@ img.ProseMirror-separator {
|
|||||||
.ProseMirror-icon span {
|
.ProseMirror-icon span {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
.ProseMirror-gapcursor {
|
|
||||||
display: none;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-gapcursor:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
width: 20px;
|
|
||||||
border-top: 1px solid black;
|
|
||||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ProseMirror-cursor-blink {
|
|
||||||
to {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
/* Add space around the hr to make clicking it easier */
|
|
||||||
|
|
||||||
.ProseMirror-example-setup-style hr {
|
|
||||||
padding: 2px 10px;
|
|
||||||
border: none;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-example-setup-style hr:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 1px;
|
|
||||||
background-color: silver;
|
|
||||||
line-height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror ul,
|
|
||||||
.ProseMirror ol {
|
|
||||||
padding-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror blockquote {
|
|
||||||
padding-left: 1em;
|
|
||||||
border-left: 3px solid #eee;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-example-setup-style img {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt {
|
|
||||||
background: white;
|
|
||||||
padding: 5px 10px 5px 15px;
|
|
||||||
border: 1px solid silver;
|
|
||||||
position: fixed;
|
|
||||||
border-radius: 3px;
|
|
||||||
z-index: 11;
|
|
||||||
box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt h5 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 100%;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt input[type="text"],
|
|
||||||
.ProseMirror-prompt textarea {
|
|
||||||
background: #eee;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt input[type="text"] {
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt-close {
|
|
||||||
position: absolute;
|
|
||||||
left: 2px;
|
|
||||||
top: 1px;
|
|
||||||
color: #666;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt-close:after {
|
|
||||||
content: "✕";
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-invalid {
|
|
||||||
background: #ffc;
|
|
||||||
border: 1px solid #cc7;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
position: absolute;
|
|
||||||
min-width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-prompt-buttons {
|
|
||||||
margin-top: 5px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#editor,
|
|
||||||
.editor {
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
background-clip: padding-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
|
||||||
padding: 5px 0;
|
|
||||||
margin-bottom: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror p:first-child,
|
|
||||||
.ProseMirror h1:first-child,
|
|
||||||
.ProseMirror h2:first-child,
|
|
||||||
.ProseMirror h3:first-child,
|
|
||||||
.ProseMirror h4:first-child,
|
|
||||||
.ProseMirror h5:first-child,
|
|
||||||
.ProseMirror h6:first-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror {
|
|
||||||
padding: 8px 8px 4px 14px;
|
|
||||||
line-height: 1.2;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror p {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper .remirror-editor {
|
.remirror-editor-wrapper .remirror-editor {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
@ -371,76 +82,16 @@ img.ProseMirror-separator {
|
|||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* heading styling */
|
|
||||||
.remirror-editor-wrapper h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper h3 {
|
|
||||||
font-size: 1.17rem;
|
|
||||||
}
|
|
||||||
/* end heading styling */
|
|
||||||
|
|
||||||
/* list styling */
|
|
||||||
.remirror-editor-wrapper ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
/* end list styling */
|
|
||||||
|
|
||||||
/* table styling */
|
|
||||||
.remirror-editor-wrapper table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper table td {
|
|
||||||
border: 1px solid #000;
|
|
||||||
}
|
|
||||||
/* end table styling */
|
|
||||||
|
|
||||||
/* link styling */
|
|
||||||
.remirror-floating-popover {
|
|
||||||
z-index: 20 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-floating-popover input {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #a8a6a6;
|
|
||||||
box-shadow: 1px 1px 5px #c0bebe;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remirror-editor-wrapper a {
|
|
||||||
color: blue;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
/* end link styling */
|
|
||||||
|
|
||||||
/* format buttons styling */
|
|
||||||
.MuiButtonBase-root {
|
.MuiButtonBase-root {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 0.25rem !important;
|
border-radius: 0.25rem !important;
|
||||||
padding: 0.25rem !important;
|
padding: 0.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiButtonBase-root:hover {
|
|
||||||
background-color: rgb(229 231 235);
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiButtonBase-root svg {
|
.MuiButtonBase-root svg {
|
||||||
fill: #000 !important;
|
fill: rgb(var(--color-text-base)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiButtonBase-root.Mui-selected {
|
.MuiButtonBase-root.Mui-selected, .MuiButtonBase-root:hover {
|
||||||
background-color: rgb(229 231 235) !important;
|
background-color: rgb(var(--color-bg-base)) !important;
|
||||||
}
|
}
|
||||||
/* end format buttons styling */
|
|
||||||
|
@ -6,6 +6,10 @@ function withOpacity(variableName) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertToRGB(variableName) {
|
||||||
|
return `rgb(var(${variableName}))`;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
||||||
@ -49,9 +53,32 @@ module.exports = {
|
|||||||
"100%": { right: "0" },
|
"100%": { right: "0" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
typography: ({ theme }) => ({
|
||||||
|
brand: {
|
||||||
|
css: {
|
||||||
|
"--tw-prose-body": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-p": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-headings": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-lead": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-links": `${convertToRGB("--color-accent")}`,
|
||||||
|
"--tw-prose-bold": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-counters": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-bullets": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-hr": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-quotes": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-quote-borders": `${convertToRGB("--color-border")}`,
|
||||||
|
"--tw-prose-code": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-pre-code": `${convertToRGB("--color-text-base")}`,
|
||||||
|
"--tw-prose-pre-bg": `${convertToRGB("--color-bg-base")}`,
|
||||||
|
"--tw-prose-th-borders": `${convertToRGB("--color-border")}`,
|
||||||
|
"--tw-prose-td-borders": `${convertToRGB("--color-border")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
custom: ["Inter", "sans-serif"],
|
custom: ["Inter", "sans-serif"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [require("@tailwindcss/typography")],
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user