added seperation of extensions and props

This commit is contained in:
Palanikannan1437 2023-09-28 20:25:31 +05:30
parent 4298b0500e
commit efcc7f20cc
17 changed files with 804 additions and 11 deletions

View File

@ -0,0 +1,34 @@
import { DeleteImage } from "@/types/delete-image";
import { UploadImage } from "@/types/upload-image";
import { TiptapExtensions } from "@/ui/extensions";
import { TiptapEditorProps } from "@/ui/props";
import { useEditor as useTiptapEditor } from "@tiptap/react";
interface ITiptapEditor {
value: string;
uploadFile: UploadImage;
deleteFile: DeleteImage;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
editable?: boolean;
debouncedUpdatesEnabled?: boolean;
debouncedUpdates: ({ onChange, editor }: { onChange?: (json: any, html: string) => void; editor: any }) => void;
}
export const useEditor = ({ uploadFile, debouncedUpdates, setShouldShowAlert, deleteFile, setIsSubmitting, value, onChange, debouncedUpdatesEnabled, editable }: ITiptapEditor) => useTiptapEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps(uploadFile, setIsSubmitting),
extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting),
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor });
} else {
onChange?.(editor.getJSON(), editor.getHTML());
}
},
});

View File

@ -1,5 +1,12 @@
import "@/styles/tailwind.css";
import "@/styles/editor.css";
// export { ImageResizer } from "./ui/extensions/image/image-resize";
// export { TiptapEditorProps } from "./ui/props";
// export { TableMenu } from "./ui/menus/table-menu";
// export { TiptapExtensions } from "./ui/extensions";
// export { cn } from "./lib/utils";
// export { FixedMenu } from "./ui/menus/fixed-menu";
// export { EditorBubbleMenu } from "./ui/menus/bubble-menu";
export { TiptapEditor, TiptapEditorWithRef } from "@/ui";

View File

@ -0,0 +1,4 @@
export interface EditorHandle {
clearEditor: () => void;
setEditorValue: (content: string) => void;
}

View File

@ -0,0 +1,88 @@
import StarterKit from "@tiptap/starter-kit";
import TiptapLink from "@tiptap/extension-link";
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 Gapcursor from "@tiptap/extension-gapcursor";
import UpdatedImage from "@/ui/extensions/image/updated-image";
import { DeleteImage } from "@/types/delete-image";
import isValidHttpUrl from "@/ui/menus/bubble-menu/utils"
export const TiptapExtensions = (
deleteFile: DeleteImage,
) => [
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-custom-border-300",
},
},
code: {
HTMLAttributes: {
class:
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
},
codeBlock: false,
horizontalRule: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
},
gapcursor: false,
}),
Gapcursor,
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: {
class:
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
},
}),
UpdatedImage(deleteFile).configure({
HTMLAttributes: {
class: "rounded-lg border border-custom-border-300",
},
}),
TiptapUnderline,
TextStyle,
Color,
TaskList.configure({
HTMLAttributes: {
class: "not-prose pl-2",
},
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex items-start my-4",
},
nested: true,
}),
Markdown.configure({
html: true,
transformCopiedText: true,
}),
];

View File

@ -0,0 +1,150 @@
"use client"
import * as React from 'react';
import { useImperativeHandle, useRef, forwardRef } from "react";
import { useEditor, EditorContent, Editor, Extension } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce";
import { TableMenu } from '@/ui/menus/table-menu';
import { TiptapExtensions } from '@/ui/extensions';
import { EditorBubbleMenu } from '@/ui/menus/bubble-menu';
import { ImageResizer } from '@/ui/extensions/image/image-resize';
import { TiptapEditorProps } from '@/ui/props';
import { UploadImage } from '@/types/upload-image';
import { DeleteImage } from '@/types/delete-image';
import { cn } from '@/lib/utils';
import { FixedMenu } from './menus/fixed-menu';
import { EditorProps } from '@tiptap/pm/view';
interface ITiptapEditor {
value: string;
uploadFile: UploadImage;
deleteFile: DeleteImage;
noBorder?: boolean;
borderOnFocus?: boolean;
customClassName?: string;
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
editable?: boolean;
forwardedRef?: any;
debouncedUpdatesEnabled?: boolean;
accessValue: string;
onAccessChange: (accessKey: string) => void;
commentAccess: {
icon: string;
key: string;
label: "Private" | "Public";
}[];
extensions?: Extension[];
editorProps?: EditorProps;
}
interface TiptapProps extends ITiptapEditor {
forwardedRef?: React.Ref<EditorHandle>;
}
interface EditorHandle {
clearEditor: () => void;
setEditorValue: (content: string) => void;
}
const DEBOUNCE_DELAY = 1500;
const TiptapEditor = ({
onChange,
debouncedUpdatesEnabled,
editable,
setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames,
value,
uploadFile,
extensions = [],
editorProps = {},
deleteFile,
noBorder,
borderOnFocus,
customClassName,
forwardedRef,
accessValue,
onAccessChange,
commentAccess,
}: TiptapProps) => {
const editor = useEditor({
editable: editable ?? true,
editorProps: {
...TiptapEditorProps(uploadFile, setIsSubmitting),
...editorProps,
},
extensions: [...TiptapExtensions(uploadFile, deleteFile, setIsSubmitting), ...extensions],
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor });
} else {
onChange?.(editor.getJSON(), editor.getHTML());
}
},
});
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor;
useImperativeHandle(forwardedRef, () => ({
clearEditor: () => {
editorRef.current?.commands.clearContent();
},
setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content);
},
}));
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
if (onChange) {
onChange(editor.getJSON(), editor.getHTML());
}
}, DEBOUNCE_DELAY);
const editorClassNames = cn(
'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md',
noBorder ? '' : 'border border-custom-border-200',
borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0',
customClassName
);
if (!editor) return null;
return (
<div
id="tiptap-container"
onClick={() => {
editor?.chain().focus().run();
}}
className={`tiptap-editor-container cursor-text ${editorClassNames}`}
>
<div className="flex flex-col">
<div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
<TableMenu editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div>
{editor && editable !== false &&
(<div className="w-full mt-4">
<FixedMenu editor={editor} commentAccess={commentAccess} accessValue={accessValue} onAccessChange={onAccessChange} />
</div>)
}
</div>
</div>
);
};
const TiptapEditorWithRef = forwardRef<EditorHandle, ITiptapEditor>((props, ref) => (
<TiptapEditor {...props} forwardedRef={ref} />
));
TiptapEditorWithRef.displayName = "TiptapEditorWithRef";
export { TiptapEditor, TiptapEditorWithRef };

View File

@ -68,7 +68,6 @@ const TiptapEditor = ({
const editor = useEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps(uploadFile, setIsSubmitting),
// @ts-expect-err
extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting),
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
onUpdate: async ({ editor }) => {

View File

@ -58,7 +58,6 @@ export function TiptapEditorProps(
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
}

View File

@ -0,0 +1,66 @@
import HorizontalRule from "@tiptap/extension-horizontal-rule";
import { TableRow } from "@tiptap/extension-table-row";
import Placeholder from "@tiptap/extension-placeholder";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { InputRule } from "@tiptap/core";
import ts from "highlight.js/lib/languages/typescript";
import { lowlight } from "lowlight/lib/core";
import "highlight.js/styles/github-dark.css";
import { Table } from "./table/table";
import { TableHeader } from "./table/table-header";
import { CustomTableCell } from "./table/table-cell";
import SlashCommand from "./slash-command";
import { UploadImage } from "./types/upload-image";
lowlight.registerLanguage("ts", ts);
export const TiptapExtensions = (
uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) => [
CodeBlockLowlight.configure({
lowlight,
}),
HorizontalRule.extend({
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, commands }) => {
commands.splitBlock();
const attributes = {};
const { tr } = state;
const start = range.from;
const end = range.to;
// @ts-ignore
tr.replaceWith(start - 1, end, this.type.create(attributes));
},
}),
];
},
}).configure({
HTMLAttributes: {
class: "mb-6 border-t border-custom-border-300",
},
}),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
return "Press '/' for commands...";
},
includeChildren: true,
}),
SlashCommand(uploadFile, setIsSubmitting),
Table,
TableHeader,
CustomTableCell,
TableRow,
];

View File

@ -0,0 +1,363 @@
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js";
import {
Heading1,
Heading2,
Heading3,
List,
ListOrdered,
Text,
TextQuote,
Code,
MinusSquare,
CheckSquare,
ImageIcon,
Table,
} from "lucide-react";
import { startImageUpload } from "@/ui/plugins/upload-image";
import { cn } from "@/lib/utils";
import { UploadImage } from "@/types/upload-image";
interface CommandItemProps {
title: string;
description: string;
icon: ReactNode;
}
interface CommandProps {
editor: Editor;
range: Range;
}
const Command = Extension.create({
name: "slash-command",
addOptions() {
return {
suggestion: {
char: "/",
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
},
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion,
}),
];
},
});
const getSuggestionItems =
(
uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) =>
({ query }: { query: string }) =>
[
{
title: "Text",
description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"],
icon: <Text size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
},
},
{
title: "Heading 1",
description: "Big section heading.",
searchTerms: ["title", "big", "large"],
icon: <Heading1 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
},
},
{
title: "Heading 2",
description: "Medium section heading.",
searchTerms: ["subtitle", "medium"],
icon: <Heading2 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
},
},
{
title: "Heading 3",
description: "Small section heading.",
searchTerms: ["subtitle", "small"],
icon: <Heading3 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
},
},
{
title: "To-do List",
description: "Track tasks with a to-do list.",
searchTerms: ["todo", "task", "list", "check", "checkbox"],
icon: <CheckSquare size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: "Bullet List",
description: "Create a simple bullet list.",
searchTerms: ["unordered", "point"],
icon: <List size={18} />,
command: ({ editor, range }: CommandProps) => {
// @ts-ignore
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: "Divider",
description: "Visually divide blocks",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
icon: <MinusSquare size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
{
title: "Table",
description: "Create a Table",
searchTerms: ["table", "cell", "db", "data", "tabular"],
icon: <Table size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
},
},
{
title: "Numbered List",
description: "Create a list with numbering.",
searchTerms: ["ordered"],
icon: <ListOrdered size={18} />,
command: ({ editor, range }: CommandProps) => {
// @ts-ignore
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
title: "Quote",
description: "Capture a quote.",
searchTerms: ["blockquote"],
icon: <TextQuote size={18} />,
command: ({ editor, range }: CommandProps) =>
// @ts-ignore
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
},
{
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code size={18} />,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).run();
// upload image
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting);
}
};
input.click();
},
},
].filter((item) => {
if (typeof query === "string" && query.length > 0) {
const search = query.toLowerCase();
return (
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
);
}
return true;
});
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
const itemHeight = item ? item.offsetHeight : 0;
const top = item.offsetTop;
const bottom = top + itemHeight;
if (top < container.scrollTop) {
container.scrollTop -= container.scrollTop - top + 5;
} else if (bottom > containerHeight + container.scrollTop) {
container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
}
};
const CommandList = ({
items,
command,
}: {
items: CommandItemProps[];
command: any;
editor: any;
range: any;
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = useCallback(
(index: number) => {
const item = items[index];
if (item) {
command(item);
}
},
[command, items]
);
useEffect(() => {
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
const onKeyDown = (e: KeyboardEvent) => {
if (navigationKeys.includes(e.key)) {
e.preventDefault();
if (e.key === "ArrowUp") {
setSelectedIndex((selectedIndex + items.length - 1) % items.length);
return true;
}
if (e.key === "ArrowDown") {
setSelectedIndex((selectedIndex + 1) % items.length);
return true;
}
if (e.key === "Enter") {
selectItem(selectedIndex);
return true;
}
return false;
}
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [items, selectedIndex, setSelectedIndex, selectItem]);
useEffect(() => {
setSelectedIndex(0);
}, [items]);
const commandListContainer = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const container = commandListContainer?.current;
const item = container?.children[selectedIndex] as HTMLElement;
if (item && container) updateScrollView(container, item);
}, [selectedIndex]);
return items.length > 0 ? (
<div
id="slash-command"
ref={commandListContainer}
className="z-50 fixed h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
>
{items.map((item: CommandItemProps, index: number) => (
<button
className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{ "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex }
)}
key={index}
onClick={() => selectItem(index)}
>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-xs text-custom-text-300">{item.description}</p>
</div>
</button>
))}
</div>
) : null;
};
const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, {
props,
editor: props.editor,
});
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#tiptap-container"),
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
component?.updateProps(props);
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
// @ts-ignore
return component?.ref?.onKeyDown(props);
},
onExit: () => {
popup?.[0].destroy();
component?.destroy();
},
};
};
export const SlashCommand = (
uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) =>
Command.configure({
suggestion: {
items: getSuggestionItems(uploadFile, setIsSubmitting),
render: renderItems,
},
});
export default SlashCommand;

View File

@ -0,0 +1,32 @@
import { TableCell } from "@tiptap/extension-table-cell";
export const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
isHeader: {
default: false,
parseHTML: (element) => {
isHeader: element.tagName === "TD";
},
renderHTML: (attributes) => {
tag: attributes.isHeader ? "th" : "td";
},
},
};
},
renderHTML({ HTMLAttributes }) {
if (HTMLAttributes.isHeader) {
return [
"th",
{
...HTMLAttributes,
class: `relative ${HTMLAttributes.class}`,
},
["span", { class: "absolute top-0 right-0" }],
0,
];
}
return ["td", HTMLAttributes, 0];
},
});

View File

@ -0,0 +1,7 @@
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
const TableHeader = BaseTableHeader.extend({
content: "paragraph",
});
export { TableHeader };

View File

@ -0,0 +1,9 @@
import { Table as BaseTable } from "@tiptap/extension-table";
const Table = BaseTable.configure({
resizable: true,
cellMinWidth: 100,
allowTableNodeSelection: true,
});
export { Table };

View File

@ -0,0 +1 @@
export type UploadImage = (file: File) => Promise<string>;

View File

@ -4,7 +4,12 @@
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"tailwindcss-animate": "^1.0.7"
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.2.7",
"tailwindcss-animate": "^1.0.6"
}
}

View File

@ -1 +1,8 @@
module.exports = require("tailwind-config-custom/postcss.config");
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -2340,7 +2340,7 @@
dependencies:
tslib "^2.4.0"
"@tailwindcss/typography@^0.5.10":
"@tailwindcss/typography@^0.5.9":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.10.tgz#2abde4c6d5c797ab49cf47610830a301de4c1e0a"
integrity sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==
@ -3193,6 +3193,18 @@ attr-accept@^2.2.2:
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@^10.4.14:
version "10.4.16"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==
dependencies:
browserslist "^4.21.10"
caniuse-lite "^1.0.30001538"
fraction.js "^4.3.6"
normalize-range "^0.1.2"
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
autoprefixer@^10.4.15:
version "10.4.15"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.15.tgz#a1230f4aeb3636b89120b34a1f513e2f6834d530"
@ -3406,6 +3418,11 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.300015
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz#9dbc6b9af1ff06b5eb12350c2012b3af56744f3f"
integrity sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==
caniuse-lite@^1.0.30001538:
version "1.0.30001541"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz#b1aef0fadd87fb72db4dcb55d220eae17b81cdb1"
integrity sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==
capital-case@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
@ -4860,7 +4877,7 @@ format@^0.2.0:
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
fraction.js@^4.2.0:
fraction.js@^4.2.0, fraction.js@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d"
integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==
@ -6811,7 +6828,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.23, postcss@^8.4.29:
postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
version "8.4.30"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.30.tgz#0e0648d551a606ef2192a26da4cabafcc09c1aa7"
integrity sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==
@ -6843,12 +6860,17 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-plugin-tailwindcss@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.3.0.tgz#8299b307c7f6467f52732265579ed9375be6c818"
integrity sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==
prettier-plugin-tailwindcss@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.4.tgz#ebfacbcb90e2ca1df671ffe4083e99f81d72040d"
integrity sha512-QZzzB1bID6qPsKHTeA9qPo1APmmxfFrA5DD3LQ+vbTmAnY40eJI7t9Q1ocqel2EKMWNPLJqdTDWZj1hKYgqSgg==
prettier@^2.8.7:
prettier@^2.8.7, prettier@^2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@ -7977,12 +7999,12 @@ tailwind-merge@^1.14.0:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
tailwindcss-animate@^1.0.7:
tailwindcss-animate@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
tailwindcss@^3.3.3:
tailwindcss@^3.2.7, tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==