From 9117a99cc3f560f5be131fe8c54735bf7fd29fce Mon Sep 17 00:00:00 2001 From: Ardeman Date: Wed, 5 Mar 2025 23:39:09 +0800 Subject: [PATCH] feat: add TextEditor component --- app/components/text-editor/editor-button.tsx | 34 ++ app/components/text-editor/editor-menubar.tsx | 432 ++++++++++++++++++ .../text-editor/editor-textarea.tsx | 56 +++ app/components/text-editor/index.tsx | 151 ++++++ app/components/ui/input.tsx | 4 +- app/hooks/use-click-outside.ts | 27 ++ app/pages/contents-create/index.tsx | 35 +- app/utils/color.ts | 39 ++ package.json | 8 + pnpm-lock.yaml | 121 +++++ 10 files changed, 900 insertions(+), 7 deletions(-) create mode 100644 app/components/text-editor/editor-button.tsx create mode 100644 app/components/text-editor/editor-menubar.tsx create mode 100644 app/components/text-editor/editor-textarea.tsx create mode 100644 app/components/text-editor/index.tsx create mode 100644 app/hooks/use-click-outside.ts create mode 100644 app/utils/color.ts diff --git a/app/components/text-editor/editor-button.tsx b/app/components/text-editor/editor-button.tsx new file mode 100644 index 0000000..4712e8d --- /dev/null +++ b/app/components/text-editor/editor-button.tsx @@ -0,0 +1,34 @@ +import type { CSSProperties, MouseEventHandler, ReactNode } from 'react' +import { twMerge } from 'tailwind-merge' + +type TProperties = { + children: ReactNode + onClick: MouseEventHandler + disabled?: boolean + isActive?: boolean + className?: string + title: string + style?: CSSProperties +} + +export const EditorButton = (properties: TProperties) => { + const { children, onClick, disabled, className, isActive, title, style } = + properties + + return ( + + ) +} diff --git a/app/components/text-editor/editor-menubar.tsx b/app/components/text-editor/editor-menubar.tsx new file mode 100644 index 0000000..a588219 --- /dev/null +++ b/app/components/text-editor/editor-menubar.tsx @@ -0,0 +1,432 @@ +import { + ArrowUturnLeftIcon, + ArrowUturnRightIcon, + Bars3BottomLeftIcon, + Bars3BottomRightIcon, + Bars3Icon, + Bars4Icon, + BoldIcon, + CodeBracketIcon, + DocumentTextIcon, + H1Icon, + H2Icon, + H3Icon, + ItalicIcon, + LinkIcon, + LinkSlashIcon, + ListBulletIcon, + MoonIcon, + NumberedListIcon, + PhotoIcon, + StrikethroughIcon, + SunIcon, + SwatchIcon, +} from '@heroicons/react/20/solid' +import type { Editor } from '@tiptap/react' +import { + type SetStateAction, + type Dispatch, + useState, + useRef, + useCallback, +} from 'react' +import { HexColorInput, HexColorPicker } from 'react-colorful' + +import { useClickOutside } from '~/hooks/use-click-outside' +import { isHexCompatible, rgbToHex } from '~/utils/color' + +import { EditorButton } from './editor-button' + +type TProperties = { + editor: Editor | null + setIsPlainHTML: Dispatch> + category: string + darkMode: boolean + setDarkMode: Dispatch> + disabled?: boolean +} + +export const EditorMenuBar = (properties: TProperties) => { + const { + editor, + setIsPlainHTML, + // category, + darkMode, + setDarkMode, + disabled = false, + } = properties + // const [isOpenImage, setIsOpenImage] = useState(false) + const [isOpenColor, setIsOpenColor] = useState(false) + const popover = useRef(null) + const close = useCallback(() => setIsOpenColor(false), []) + + useClickOutside(popover, close) + + const setLink = useCallback(() => { + const previousUrl = editor?.getAttributes('link').href + const url = globalThis.prompt('URL', previousUrl) + + // cancelled + if (url === null) { + return + } + + // empty + if (url === '') { + editor?.chain().focus().extendMarkRange('link').unsetLink().run() + + return + } + + // update link + editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run() + }, [editor]) + + if (!editor) { + return + } + + const addImage = (url: string) => { + if (url) { + editor.chain().focus().setImage({ src: url }).run() + } + } + + const currentColor: string = editor.getAttributes('textStyle').color + const rgbColor = isHexCompatible(currentColor) + ? currentColor + : rgbToHex(currentColor) + const handleChangeColor = (selectedColor: string) => { + if (selectedColor.length === 7) { + editor.chain().focus().setColor(selectedColor).run() + } + } + + const uploadEnabled = false + const toggleUpload = () => { + if (uploadEnabled) { + // setIsOpenImage(true) + } else { + const urlImage = globalThis.prompt('URL') + if (urlImage) { + addImage(urlImage) + } + } + } + + const toggleDark = () => { + setDarkMode(!darkMode) + // localStorage.setItem(editorKey, JSON.stringify(!darkMode)) + } + + return ( + <> +
+
+
+ editor.chain().focus().toggleBold().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleBold().run() + } + isActive={editor.isActive('bold')} + title="Bold" + > + + + editor.chain().focus().toggleItalic().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleItalic().run() + } + isActive={editor.isActive('italic')} + title="Italic" + > + + + editor.chain().focus().toggleStrike().run()} + disabled={ + disabled || !editor.can().chain().focus().toggleStrike().run() + } + isActive={editor.isActive('strike')} + title="Strike" + > + + +
+ setIsOpenColor(true)} + title="Text Color" + style={{ + color: rgbColor, + }} + isActive={true} + disabled={disabled} + > + + + {isOpenColor && ( +
+ +
+ +
+
+ )} +
+ editor.chain().focus().setTextAlign('left').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('left').run() + } + isActive={editor.isActive({ textAlign: 'left' })} + title="Align Left" + > + + + + editor.chain().focus().setTextAlign('center').run() + } + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('center').run() + } + isActive={editor.isActive({ textAlign: 'center' })} + title="Align Center" + > + + + editor.chain().focus().setTextAlign('right').run()} + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('right').run() + } + isActive={editor.isActive({ textAlign: 'right' })} + title="Align Right" + > + + + + editor.chain().focus().setTextAlign('justify').run() + } + disabled={ + disabled || + !editor.can().chain().focus().setTextAlign('justify').run() + } + isActive={editor.isActive({ textAlign: 'justify' })} + title="Align Justify" + > + + +
+
+ + editor.chain().focus().toggleHeading({ level: 1 }).run() + } + isActive={editor.isActive('heading', { level: 1 })} + title="Heading 1" + disabled={disabled} + > + + + + editor.chain().focus().toggleHeading({ level: 2 }).run() + } + isActive={editor.isActive('heading', { level: 2 })} + title="Heading 2" + disabled={disabled} + > + + + + editor.chain().focus().toggleHeading({ level: 3 }).run() + } + isActive={editor.isActive('heading', { level: 3 })} + title="Heading 3" + disabled={disabled} + > + + + {/* editor.chain().focus().setParagraph().run()} + isActive={editor.isActive('paragraph')} + title="Paragraph" + disabled={disabled} + > + + */} + editor.chain().focus().toggleBulletList().run()} + isActive={editor.isActive('bulletList')} + title="Bullet List" + disabled={disabled} + > + + + editor.chain().focus().toggleOrderedList().run()} + isActive={editor.isActive('orderedList')} + title="Ordered List" + disabled={disabled} + > + + + editor.chain().focus().toggleCodeBlock().run()} + isActive={editor.isActive('codeBlock')} + title="Code Block" + disabled={disabled} + > + + +
+ {/*
+ editor.chain().focus().toggleBlockquote().run()} + isActive={editor.isActive('blockquote')} + title="Blockquote" + disabled={disabled} + > + + + editor.chain().focus().setHorizontalRule().run()} + title="Horizontal Rule" + disabled={disabled} + > + + +
*/} + {/*
+ editor.chain().focus().setHardBreak().run()} + title="Hard Break" + disabled={disabled} + > + + + { + editor.chain().focus().unsetAllMarks().run() + editor.chain().focus().clearNodes().run() + }} + title="Clear Format" + disabled={disabled} + > + + +
*/} +
+ toggleUpload()} + title="Insert Image" + disabled={disabled} + > + + + setLink()} + disabled={ + disabled || + !editor + .can() + .chain() + .focus() + .extendMarkRange('link') + .setLink({ href: '' }) + .run() + } + isActive={editor.isActive('link')} + title="Set Link" + > + + + editor.chain().focus().unsetLink().run()} + disabled={disabled || !editor.isActive('link')} + title="Unset Link" + > + + +
+
+ editor.chain().focus().undo().run()} + disabled={disabled || !editor.can().chain().focus().undo().run()} + title="Undo" + > + + + editor.chain().focus().redo().run()} + disabled={disabled || !editor.can().chain().focus().redo().run()} + title="Redo" + > + + + toggleDark()} + title={darkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'} + isActive={true} + > + {darkMode ? : } + +
+
+
+
+ setIsPlainHTML(true)} + title="Switch to Plain Text" + isActive={true} + > + + +
+
+
+ + {/* + setIsOpenImage(false), + onSave: (file) => { + addImage(file) + setIsOpenImage(false) + }, + category: category, + maxFileSize: 300, + selectedFile: '', + }} + > + + + */} + + ) +} diff --git a/app/components/text-editor/editor-textarea.tsx b/app/components/text-editor/editor-textarea.tsx new file mode 100644 index 0000000..a3b6019 --- /dev/null +++ b/app/components/text-editor/editor-textarea.tsx @@ -0,0 +1,56 @@ +import { CodeBracketSquareIcon } from '@heroicons/react/20/solid' +import MonacoEditor from '@monaco-editor/react' +import type { Dispatch, SetStateAction } from 'react' +import { Controller } from 'react-hook-form' +import { useRemixFormContext } from 'remix-hook-form' + +import { EditorButton } from './editor-button' + +type TProperties = { + name: string + disabled?: boolean + setIsPlainHTML: Dispatch> +} + +// const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { +// ssr: false, +// }) + +export const EditorTextArea = (properties: TProperties) => { + const { setIsPlainHTML, name, disabled = false } = properties + + const { control } = useRemixFormContext() + return ( + <> +
+
+ setIsPlainHTML(false)} + title="Switch to Rich Text" + isActive={true} + > + + +
+
+ ( + { + field.onChange(newValue) + }} + value={field.value} + options={{ + readOnly: disabled, + wordWrap: 'on', + }} + className="mb-1 h-96 max-w-none overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] p-1" + /> + )} + /> + + ) +} diff --git a/app/components/text-editor/index.tsx b/app/components/text-editor/index.tsx new file mode 100644 index 0000000..9a2aedf --- /dev/null +++ b/app/components/text-editor/index.tsx @@ -0,0 +1,151 @@ +import { Color } from '@tiptap/extension-color' +import Highlight from '@tiptap/extension-highlight' +import Image from '@tiptap/extension-image' +import Link from '@tiptap/extension-link' +import TextAlign from '@tiptap/extension-text-align' +import TextStyle from '@tiptap/extension-text-style' +import { EditorContent, useEditor } from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import { useEffect, useId, useState } from 'react' +import { + get, + type FieldValues, + type Path, + type RegisterOptions, +} from 'react-hook-form' +import { useRemixFormContext } from 'remix-hook-form' +import { twMerge } from 'tailwind-merge' + +import { EditorMenuBar } from './editor-menubar' +import { EditorTextArea } from './editor-textarea' + +type TProperties = { + id?: string + name: Path + label?: string + placeholder?: string + labelClassName?: string + className?: string + inputClassName?: string + rules?: RegisterOptions + disabled?: boolean + isRequired?: boolean + category: string +} + +export const TextEditor = >( + properties: TProperties, +) => { + const { + id, + label, + name, + labelClassName, + isRequired, + className, + inputClassName, + category, + disabled = false, + } = properties + + const [isPlainHTML, setIsPlainHTML] = useState(false) + const [init, setInit] = useState(true) + const [darkMode, setDarkMode] = useState(false) + const generatedId = useId() + + const { + setValue, + watch, + formState: { errors }, + } = useRemixFormContext() + + const watchContent = watch(name) + const error = get(errors, name) + + const editor = useEditor({ + editable: !disabled, + extensions: [ + StarterKit, + Highlight, + Image.configure({ + inline: true, + }), + TextStyle, + Color.configure({ + types: ['textStyle'], + }), + Link.configure({ + openOnClick: false, + }), + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + ], + content: watchContent, + onUpdate: ({ editor }) => { + setValue(name, editor.getHTML() as any) // eslint-disable-line @typescript-eslint/no-explicit-any + }, + }) + useEffect(() => { + if ( + watchContent && + watchContent.length > 0 && + editor && + (isPlainHTML || init) + ) { + editor.commands.setContent(watchContent) + setInit(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [watchContent]) + + return ( +
+ {label && ( + + )} + + {isPlainHTML ? ( + + ) : ( + <> + + editor?.commands.focus()} + /> + + )} + + {error && ( +

+ {error?.message?.toString()} +

+ )} +
+ ) +} diff --git a/app/components/ui/input.tsx b/app/components/ui/input.tsx index 8c29ae8..78c0bfb 100644 --- a/app/components/ui/input.tsx +++ b/app/components/ui/input.tsx @@ -22,6 +22,7 @@ type TInputProperties = Omit< label?: ReactNode name: Path rules?: RegisterOptions + containerClassName?: string } export const Input = >( @@ -36,6 +37,7 @@ export const Input = >( placeholder, disabled, className, + containerClassName, ...rest } = properties const [inputType, setInputType] = useState(type) @@ -49,7 +51,7 @@ export const Input = >( return ( diff --git a/app/hooks/use-click-outside.ts b/app/hooks/use-click-outside.ts new file mode 100644 index 0000000..47672e8 --- /dev/null +++ b/app/hooks/use-click-outside.ts @@ -0,0 +1,27 @@ +import { useEffect, type RefObject } from 'react' + +type Event = MouseEvent | TouchEvent + +export const useClickOutside = ( + reference: RefObject, + handler: (event: Event) => void, +) => { + useEffect(() => { + const listener = (event: Event) => { + const element = reference?.current + if (!element || element.contains((event?.target as Node) || undefined)) { + return + } + + handler(event) + } + + document.addEventListener('mousedown', listener) + document.addEventListener('touchstart', listener) + + return () => { + document.removeEventListener('mousedown', listener) + document.removeEventListener('touchstart', listener) + } + }, [reference, handler]) +} diff --git a/app/pages/contents-create/index.tsx b/app/pages/contents-create/index.tsx index 7471ada..6f31ff9 100644 --- a/app/pages/contents-create/index.tsx +++ b/app/pages/contents-create/index.tsx @@ -5,10 +5,10 @@ import { useFetcher, useRouteLoaderData } from 'react-router' import { RemixFormProvider, useRemixForm } from 'remix-hook-form' import { z } from 'zod' +import { TextEditor } from '~/components/text-editor' import { Button } from '~/components/ui/button' import { Combobox } from '~/components/ui/combobox' import { Input } from '~/components/ui/input' -import DefaultTextEditor from '~/components/ui/text-editor' import { TitleDashboard } from '~/components/ui/title-dashboard' import type { loader } from '~/routes/_admin.lg-admin' @@ -38,10 +38,10 @@ export const contentSchema = z.object({ .nullable(), ), title: z.string().min(1, { - message: 'Title is required', + message: 'Judul is required', }), content: z.string().min(1, { - message: 'Content is required', + message: 'Konten is required', }), featured_image: z.string().optional(), is_premium: z.boolean().optional(), @@ -95,6 +95,24 @@ export const CreateContentsPage = () => { {error && (
{error}
)} +
+ + +
{
-
- -
+ diff --git a/app/utils/color.ts b/app/utils/color.ts new file mode 100644 index 0000000..73962a1 --- /dev/null +++ b/app/utils/color.ts @@ -0,0 +1,39 @@ +export const isHexCompatible = (hexColor?: string): boolean => { + if (hexColor === undefined) { + return true + } + + const hexColorRegex = /^#([\dA-Fa-f]{6}|[\dA-Fa-f]{3})$/ + return hexColorRegex.test(hexColor) +} + +export const rgbToHex = (rgb: string): string => { + // Extract the integers by matching against a regex + const result = rgb.match(/\d+/g) + + if (!result) { + return '#000000' // Set default color to #000000 if the RGB string is invalid + } + + const [red, green, blue] = result.map(Number) + + // Ensure the values are valid + if ( + red < 0 || + red > 255 || + green < 0 || + green > 255 || + blue < 0 || + blue > 255 + ) { + return '#000000' // Set default color to #000000 if the RGB values are invalid + } + + // Convert each component to hex + const redHex = red.toString(16).padStart(2, '0') + const greenHex = green.toString(16).padStart(2, '0') + const blueHex = blue.toString(16).padStart(2, '0') + + // Return the combined string + return `#${redHex}${greenHex}${blueHex}` +} diff --git a/package.json b/package.json index 63e5659..707b41b 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,16 @@ "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^4.1.1", + "@monaco-editor/react": "^4.7.0", "@react-router/fs-routes": "^7.1.3", "@react-router/node": "^7.1.3", "@react-router/serve": "^7.1.3", + "@tiptap/extension-color": "^2.11.5", + "@tiptap/extension-highlight": "^2.11.5", + "@tiptap/extension-image": "^2.11.5", + "@tiptap/extension-link": "^2.11.5", + "@tiptap/extension-text-align": "^2.11.5", + "@tiptap/extension-text-style": "^2.11.5", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", "chart.js": "^4.4.8", @@ -32,6 +39,7 @@ "jose": "^6.0.8", "react": "^19.0.0", "react-chartjs-2": "^5.3.0", + "react-colorful": "^5.6.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", "react-router": "^7.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e3ff9d..c41056b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@hookform/resolvers': specifier: ^4.1.1 version: 4.1.1(react-hook-form@7.54.2(react@19.0.0)) + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-router/fs-routes': specifier: ^7.1.3 version: 7.1.3(@react-router/dev@7.1.3(@react-router/serve@7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3))(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(lightningcss@1.29.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.16)(lightningcss@1.29.1)))(typescript@5.7.3) @@ -26,6 +29,24 @@ importers: '@react-router/serve': specifier: ^7.1.3 version: 7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3) + '@tiptap/extension-color': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))) + '@tiptap/extension-highlight': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)) + '@tiptap/extension-image': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)) + '@tiptap/extension-link': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5) + '@tiptap/extension-text-align': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)) + '@tiptap/extension-text-style': + specifier: ^2.11.5 + version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)) '@tiptap/react': specifier: ^2.11.5 version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -62,6 +83,9 @@ importers: react-chartjs-2: specifier: ^5.3.0 version: 5.3.0(chart.js@4.4.8)(react@19.0.0) + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) @@ -708,6 +732,16 @@ packages: '@mjackson/node-fetch-server@0.2.0': resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} + '@monaco-editor/loader@1.5.0': + resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1474,6 +1508,12 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-color@2.11.5': + resolution: {integrity: sha512-9gZF6EIpfOJYUt1TtFY37e8iqwKcOmBl8CkFaxq+4mWVvYd2D7KbA0r4tYTxSO0fOBJ5fA/1qJrpvgRlyocp/A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/extension-document@2.11.5': resolution: {integrity: sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==} peerDependencies: @@ -1507,6 +1547,11 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-highlight@2.11.5': + resolution: {integrity: sha512-VBZfT869L9CiTLF8qr+3FBUtJcmlyUTECORNo0ceEiNDg4H6V9uNPwaROMXrWiQCc+DYVCOkx541QrXwNMzxlg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-history@2.11.5': resolution: {integrity: sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==} peerDependencies: @@ -1519,11 +1564,22 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-image@2.11.5': + resolution: {integrity: sha512-HbUq9AL8gb8eSuQfY/QKkvMc66ZFN/b6jvQAILGArNOgalUfGizoC6baKTJShaExMSPjBZlaAHtJiQKPaGRHaA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-italic@2.11.5': resolution: {integrity: sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-link@2.11.5': + resolution: {integrity: sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/extension-list-item@2.11.5': resolution: {integrity: sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==} peerDependencies: @@ -1544,6 +1600,11 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-align@2.11.5': + resolution: {integrity: sha512-Ei0zDpH5N9EV59ogydK4HTKa4lCPicCsQllM5n/Nf2tUJPir3aiYxzJ73FzhComD4Hpo1ANYnmssBhy8QeoPZA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style@2.11.5': resolution: {integrity: sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==} peerDependencies: @@ -3173,6 +3234,9 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkifyjs@4.2.0: + resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} + lint-staged@15.4.3: resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==} engines: {node: '>=18.12.0'} @@ -3337,6 +3401,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + morgan@1.10.0: resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} engines: {node: '>= 0.8.0'} @@ -3791,6 +3858,12 @@ packages: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-d3-tree@3.6.2: resolution: {integrity: sha512-1ExQlmEnv5iOw9XfZ3EcESDjzGXVKPAmyDJTJbvVfiwkplZtP7CcNEY0tKZf4XSW0FzYJf4aFXprGJen+95yuw==} peerDependencies: @@ -4135,6 +4208,9 @@ packages: stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5214,6 +5290,17 @@ snapshots: '@mjackson/node-fetch-server@0.2.0': {} + '@monaco-editor/loader@1.5.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@monaco-editor/loader': 1.5.0 + monaco-editor: 0.52.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5948,6 +6035,11 @@ snapshots: dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-color@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))': + dependencies: + '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-text-style': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)) + '@tiptap/extension-document@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) @@ -5976,6 +6068,10 @@ snapshots: dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-highlight@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': + dependencies: + '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-history@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)': dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) @@ -5986,10 +6082,20 @@ snapshots: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) '@tiptap/pm': 2.11.5 + '@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': + dependencies: + '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-italic@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)': + dependencies: + '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/pm': 2.11.5 + linkifyjs: 4.2.0 + '@tiptap/extension-list-item@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) @@ -6006,6 +6112,10 @@ snapshots: dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-text-align@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': + dependencies: + '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) + '@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))': dependencies: '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5) @@ -7882,6 +7992,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + linkifyjs@4.2.0: {} + lint-staged@15.4.3: dependencies: chalk: 5.4.1 @@ -8028,6 +8140,8 @@ snapshots: minipass@7.1.2: {} + monaco-editor@0.52.2: {} + morgan@1.10.0: dependencies: basic-auth: 2.0.1 @@ -8457,6 +8571,11 @@ snapshots: chart.js: 4.4.8 react: 19.0.0 + react-colorful@5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-d3-tree@3.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@bkrem/react-transition-group': 1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -8876,6 +8995,8 @@ snapshots: stable-hash@0.0.4: {} + state-local@1.0.7: {} + statuses@2.0.1: {} stream-shift@1.0.3: {}