diff --git a/packages/editor/core/src/hooks/useEditor.tsx b/packages/editor/core/src/hooks/useEditor.tsx new file mode 100644 index 000000000..9d121fcff --- /dev/null +++ b/packages/editor/core/src/hooks/useEditor.tsx @@ -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 : "

", + onUpdate: async ({ editor }) => { + // for instant feedback loop + setIsSubmitting?.("submitting"); + setShouldShowAlert?.(true); + if (debouncedUpdatesEnabled) { + debouncedUpdates({ onChange, editor }); + } else { + onChange?.(editor.getJSON(), editor.getHTML()); + } + }, +}); diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts index 7e137d3c6..890841cb9 100644 --- a/packages/editor/core/src/index.ts +++ b/packages/editor/core/src/index.ts @@ -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"; diff --git a/packages/editor/core/src/interfaces/index.ts b/packages/editor/core/src/interfaces/index.ts new file mode 100644 index 000000000..7f5492df7 --- /dev/null +++ b/packages/editor/core/src/interfaces/index.ts @@ -0,0 +1,4 @@ +export interface EditorHandle { + clearEditor: () => void; + setEditorValue: (content: string) => void; +} diff --git a/packages/editor/core/src/ui/extensions/index-new.tsx b/packages/editor/core/src/ui/extensions/index-new.tsx new file mode 100644 index 000000000..27639fe06 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/index-new.tsx @@ -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, + }), + ]; diff --git a/packages/editor/core/src/ui/index-new.tsx b/packages/editor/core/src/ui/index-new.tsx new file mode 100644 index 000000000..b6e42348b --- /dev/null +++ b/packages/editor/core/src/ui/index-new.tsx @@ -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; +} + +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 : "

", + 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 = 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 ( +
{ + editor?.chain().focus().run(); + }} + className={`tiptap-editor-container cursor-text ${editorClassNames}`} + > +
+
+ + + {editor?.isActive("image") && } +
+ {editor && editable !== false && + (
+ +
) + } +
+
+ ); +}; + +const TiptapEditorWithRef = forwardRef((props, ref) => ( + +)); + +TiptapEditorWithRef.displayName = "TiptapEditorWithRef"; + +export { TiptapEditor, TiptapEditorWithRef }; diff --git a/packages/editor/core/src/ui/index.tsx b/packages/editor/core/src/ui/index.tsx index fc870f309..28808f649 100644 --- a/packages/editor/core/src/ui/index.tsx +++ b/packages/editor/core/src/ui/index.tsx @@ -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 : "

", onUpdate: async ({ editor }) => { diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 528f664ea..d2a0d8063 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -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); } diff --git a/packages/editor/rich-text-editor/src/extensions.tsx b/packages/editor/rich-text-editor/src/extensions.tsx new file mode 100644 index 000000000..85087e5e1 --- /dev/null +++ b/packages/editor/rich-text-editor/src/extensions.tsx @@ -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, + ]; diff --git a/packages/editor/rich-text-editor/src/index.tsx b/packages/editor/rich-text-editor/src/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor/rich-text-editor/src/slash-command.tsx b/packages/editor/rich-text-editor/src/slash-command.tsx new file mode 100644 index 000000000..844d4c55a --- /dev/null +++ b/packages/editor/rich-text-editor/src/slash-command.tsx @@ -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: , + 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: , + 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: , + 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: , + 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: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).toggleTaskList().run(); + }, + }, + { + title: "Bullet List", + description: "Create a simple bullet list.", + searchTerms: ["unordered", "point"], + icon: , + 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: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setHorizontalRule().run(); + }, + }, + { + title: "Table", + description: "Create a Table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon: , + 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: , + command: ({ editor, range }: CommandProps) => { + // @ts-ignore + editor.chain().focus().deleteRange(range).toggleOrderedList().run(); + }, + }, + { + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], + icon: , + 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: , + 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: , + 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(null); + + useLayoutEffect(() => { + const container = commandListContainer?.current; + + const item = container?.children[selectedIndex] as HTMLElement; + + if (item && container) updateScrollView(container, item); + }, [selectedIndex]); + + return items.length > 0 ? ( +
+ {items.map((item: CommandItemProps, index: number) => ( + + ))} +
+ ) : 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; diff --git a/packages/editor/rich-text-editor/src/table/table-cell.ts b/packages/editor/rich-text-editor/src/table/table-cell.ts new file mode 100644 index 000000000..643cb8c64 --- /dev/null +++ b/packages/editor/rich-text-editor/src/table/table-cell.ts @@ -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]; + }, +}); diff --git a/packages/editor/rich-text-editor/src/table/table-header.ts b/packages/editor/rich-text-editor/src/table/table-header.ts new file mode 100644 index 000000000..f23aa93ef --- /dev/null +++ b/packages/editor/rich-text-editor/src/table/table-header.ts @@ -0,0 +1,7 @@ +import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header"; + +const TableHeader = BaseTableHeader.extend({ + content: "paragraph", +}); + +export { TableHeader }; diff --git a/packages/editor/rich-text-editor/src/table/table.ts b/packages/editor/rich-text-editor/src/table/table.ts new file mode 100644 index 000000000..9b727bb51 --- /dev/null +++ b/packages/editor/rich-text-editor/src/table/table.ts @@ -0,0 +1,9 @@ +import { Table as BaseTable } from "@tiptap/extension-table"; + +const Table = BaseTable.configure({ + resizable: true, + cellMinWidth: 100, + allowTableNodeSelection: true, +}); + +export { Table }; diff --git a/packages/editor/rich-text-editor/src/types/upload-image.ts b/packages/editor/rich-text-editor/src/types/upload-image.ts new file mode 100644 index 000000000..3cf1408d2 --- /dev/null +++ b/packages/editor/rich-text-editor/src/types/upload-image.ts @@ -0,0 +1 @@ +export type UploadImage = (file: File) => Promise; diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json index 1bd5a0e1c..b6c3ec614 100644 --- a/packages/tailwind-config-custom/package.json +++ b/packages/tailwind-config-custom/package.json @@ -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" } } diff --git a/web/postcss.config.js b/web/postcss.config.js index 129aa7f59..6887c8262 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -1 +1,8 @@ -module.exports = require("tailwind-config-custom/postcss.config"); +module.exports = { + plugins: { + "postcss-import": {}, + "tailwindcss/nesting": {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/yarn.lock b/yarn.lock index d92d076bf..b2d3e099b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==