forked from github/plane
added bubblemenu support with extensions
This commit is contained in:
parent
d62ac268c6
commit
b078e24d82
116
apps/app/components/issues/EditorBubbleMenu.tsx
Normal file
116
apps/app/components/issues/EditorBubbleMenu.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { BubbleMenu, BubbleMenuProps } from "@tiptap/react";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
UnderlineIcon,
|
||||
StrikethroughIcon,
|
||||
CodeIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { NodeSelector } from "./node-selector";
|
||||
import { LinkSelector } from "./link-selector";
|
||||
import { cn } from "./utils";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
command: () => void;
|
||||
icon: typeof BoldIcon;
|
||||
}
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "bold",
|
||||
isActive: () => props.editor.isActive("bold"),
|
||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||
icon: BoldIcon,
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
isActive: () => props.editor.isActive("italic"),
|
||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||
icon: ItalicIcon,
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
isActive: () => props.editor.isActive("underline"),
|
||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||
icon: UnderlineIcon,
|
||||
},
|
||||
{
|
||||
name: "strike",
|
||||
isActive: () => props.editor.isActive("strike"),
|
||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||
icon: StrikethroughIcon,
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
isActive: () => props.editor.isActive("code"),
|
||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||
icon: CodeIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const bubbleMenuProps: EditorBubbleMenuProps = {
|
||||
...props,
|
||||
shouldShow: ({ editor }) => {
|
||||
if (editor.isActive("image")) {
|
||||
return false;
|
||||
}
|
||||
return editor.view.state.selection.content().size > 0;
|
||||
},
|
||||
tippyOptions: {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
onHidden: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
{...bubbleMenuProps}
|
||||
className="flex w-fit divide-x divide-stone-200 rounded border border-stone-200 bg-white shadow-xl"
|
||||
>
|
||||
<NodeSelector
|
||||
editor={props.editor}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
<LinkSelector
|
||||
editor={props.editor}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="flex">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={item.command}
|
||||
className="p-2 text-stone-600 hover:bg-stone-100 active:bg-stone-200"
|
||||
>
|
||||
<item.icon
|
||||
className={cn("h-4 w-4", {
|
||||
"text-blue-500": item.isActive(),
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</BubbleMenu>
|
||||
);
|
||||
};
|
127
apps/app/components/issues/extensions.tsx
Normal file
127
apps/app/components/issues/extensions.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import TiptapImage from "@tiptap/extension-image";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
|
||||
// import SlashCommand from "./slash-command";
|
||||
import { InputRule } from "@tiptap/core";
|
||||
|
||||
export const TiptapExtensions = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "leading-normal -mb-2",
|
||||
},
|
||||
},
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: "border-l-4 border-stone-700",
|
||||
},
|
||||
},
|
||||
codeBlock: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-sm bg-stone-100 p-5 font-mono font-medium text-stone-800",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-stone-200 px-1.5 py-1 font-mono font-medium text-stone-900",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "#DBEAFE",
|
||||
width: 4,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
HorizontalRule.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range }) => {
|
||||
const attributes = {};
|
||||
|
||||
const { tr } = state;
|
||||
const start = range.from;
|
||||
const end = range.to;
|
||||
|
||||
tr.insert(start - 1, this.type.create(attributes)).delete(
|
||||
tr.mapping.map(start),
|
||||
tr.mapping.map(end),
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "mt-4 mb-6 border-t border-stone-300",
|
||||
},
|
||||
}),
|
||||
TiptapLink.configure({
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
TiptapImage.configure({
|
||||
allowBase64: true,
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-stone-200",
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
return "Press '/' for commands, or '++' for AI autocomplete...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
// SlashCommand,
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({
|
||||
multicolor: true,
|
||||
}),
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex items-start my-4",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: false,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
];
|
78
apps/app/components/issues/link-selector.tsx
Normal file
78
apps/app/components/issues/link-selector.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Trash } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
||||
import { cn } from './utils';
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: Editor;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Autofocus on input by default
|
||||
useEffect(() => {
|
||||
inputRef.current && inputRef.current?.focus();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
className="flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-stone-600 hover:bg-stone-100 active:bg-stone-200"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<p className="text-base">↗</p>
|
||||
<p
|
||||
className={cn("underline decoration-stone-400 underline-offset-4", {
|
||||
"text-blue-500": editor.isActive("link"),
|
||||
})}
|
||||
>
|
||||
Link
|
||||
</p>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const input = e.target[0] as HTMLInputElement;
|
||||
editor.chain().focus().setLink({ href: input.value }).run();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-stone-200 bg-white p-1 shadow-xl animate-in fade-in slide-in-from-top-1"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
placeholder="Paste a link"
|
||||
className="flex-1 bg-white p-1 text-sm outline-none"
|
||||
defaultValue={editor.getAttributes("link").href || ""}
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
<button
|
||||
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
|
||||
onClick={() => {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button className="flex items-center rounded-sm p-1 text-stone-600 transition-all hover:bg-stone-100">
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
135
apps/app/components/issues/node-selector.tsx
Normal file
135
apps/app/components/issues/node-selector.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
TextQuote,
|
||||
ListOrdered,
|
||||
TextIcon,
|
||||
Code,
|
||||
CheckSquare,
|
||||
} from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
|
||||
import { BubbleMenuItem } from "./EditorBubbleMenu";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
editor: Editor;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Text",
|
||||
icon: TextIcon,
|
||||
command: () =>
|
||||
editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||
// I feel like there has to be a more efficient way to do this – feel free to PR if you know how!
|
||||
isActive: () =>
|
||||
editor.isActive("paragraph") &&
|
||||
!editor.isActive("bulletList") &&
|
||||
!editor.isActive("orderedList"),
|
||||
},
|
||||
{
|
||||
name: "Heading 1",
|
||||
icon: Heading1,
|
||||
command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
||||
},
|
||||
{
|
||||
name: "Heading 2",
|
||||
icon: Heading2,
|
||||
command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
||||
},
|
||||
{
|
||||
name: "Heading 3",
|
||||
icon: Heading3,
|
||||
command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
||||
},
|
||||
{
|
||||
name: "To-do List",
|
||||
icon: CheckSquare,
|
||||
command: () => editor.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => editor.isActive("taskItem"),
|
||||
},
|
||||
{
|
||||
name: "Bullet List",
|
||||
icon: ListOrdered,
|
||||
command: () => editor.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => editor.isActive("bulletList"),
|
||||
},
|
||||
{
|
||||
name: "Numbered List",
|
||||
icon: ListOrdered,
|
||||
command: () => editor.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => editor.isActive("orderedList"),
|
||||
},
|
||||
{
|
||||
name: "Quote",
|
||||
icon: TextQuote,
|
||||
command: () =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run(),
|
||||
isActive: () => editor.isActive("blockquote"),
|
||||
},
|
||||
{
|
||||
name: "Code",
|
||||
icon: Code,
|
||||
command: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => editor.isActive("codeBlock"),
|
||||
},
|
||||
];
|
||||
|
||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
||||
name: "Multiple",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<button
|
||||
className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-stone-600 hover:bg-stone-100 active:bg-stone-200"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span>{activeItem?.name}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<section className="fixed top-full z-[99999] mt-1 flex w-48 flex-col overflow-hidden rounded border border-stone-200 bg-white p-1 shadow-xl animate-in fade-in slide-in-from-top-1">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
item.command();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="flex items-center justify-between rounded-sm px-2 py-1 text-sm text-stone-600 hover:bg-stone-100"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="rounded-sm border border-stone-200 p-1">
|
||||
<item.icon className="h-3 w-3" />
|
||||
</div>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
{activeItem.name === item.name && <Check className="h-4 w-4" />}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,5 +3,16 @@ import { EditorProps } from "@tiptap/pm/view";
|
||||
export const TiptapEditorProps: EditorProps = {
|
||||
attributes: {
|
||||
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
||||
}
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => {
|
||||
// prevent default event listeners from firing when slash command is active
|
||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||
const slashCommand = document.querySelector("#slash-command");
|
||||
if (slashCommand) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { EditorBubbleMenu } from './EditorBubbleMenu';
|
||||
import { TiptapExtensions } from './extensions';
|
||||
import { TiptapEditorProps } from "./props";
|
||||
|
||||
type TiptapProps = {
|
||||
@ -14,12 +15,13 @@ type TiptapProps = {
|
||||
const Tiptap = ({ value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
||||
const editor = useEditor({
|
||||
editorProps: TiptapEditorProps,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
placeholder: 'Description...',
|
||||
})
|
||||
],
|
||||
extensions: TiptapExtensions,
|
||||
// extensions: [
|
||||
// StarterKit,
|
||||
// Placeholder.configure({
|
||||
// placeholder: 'Description...',
|
||||
// })
|
||||
// ],
|
||||
content: value,
|
||||
});
|
||||
|
||||
@ -35,6 +37,7 @@ const Tiptap = ({ value, noBorder, borderOnFocus, customClassName }: TiptapProps
|
||||
}}
|
||||
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
|
||||
>
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
|
6
apps/app/components/issues/utils.ts
Normal file
6
apps/app/components/issues/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
@ -27,17 +27,27 @@
|
||||
"@nivo/scatterplot": "0.80.0",
|
||||
"@sentry/nextjs": "^7.36.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
"@tiptap/extension-highlight": "^2.0.4",
|
||||
"@tiptap/extension-image": "^2.0.4",
|
||||
"@tiptap/extension-link": "^2.0.4",
|
||||
"@tiptap/extension-placeholder": "^2.0.4",
|
||||
"@tiptap/extension-task-item": "^2.0.4",
|
||||
"@tiptap/extension-task-list": "^2.0.4",
|
||||
"@tiptap/extension-text-style": "^2.0.4",
|
||||
"@tiptap/extension-underline": "^2.0.4",
|
||||
"@tiptap/pm": "^2.0.4",
|
||||
"@tiptap/react": "^2.0.4",
|
||||
"@tiptap/starter-kit": "^2.0.4",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"axios": "^1.1.3",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.263.1",
|
||||
"next": "12.3.2",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.2.1",
|
||||
@ -51,6 +61,8 @@
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"swr": "^2.1.3",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tiptap-markdown": "^0.8.2",
|
||||
"tlds": "^1.238.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
|
@ -9,13 +9,100 @@
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: rgb(var(--color-text-400));
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
color: rgb(var(--color-text-400));
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror .is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: rgb(var(--color-text-400));
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
transition: filter 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: 3px solid #5abbf7;
|
||||
filter: brightness(90%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
||||
|
||||
ul[data-type="taskList"] li > label {
|
||||
margin-right: 0.2rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
ul[data-type="taskList"] li > label {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--novel-white);
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
border: 2px solid var(--novel-stone-900);
|
||||
margin-right: 0.3rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--novel-stone-50);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--novel-stone-200);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 0.65em;
|
||||
height: 0.65em;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
box-shadow: inset 1em 1em;
|
||||
transform-origin: center;
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
}
|
||||
|
||||
&:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
color: var(--novel-stone-400);
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
/* Overwrite tippy-box original max-width */
|
||||
|
||||
.tippy-box {
|
||||
max-width: 400px !important;
|
||||
}
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
|
Loading…
Reference in New Issue
Block a user