feat: add TextEditor component
This commit is contained in:
parent
a6e6e10c69
commit
9117a99cc3
34
app/components/text-editor/editor-button.tsx
Normal file
34
app/components/text-editor/editor-button.tsx
Normal file
@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={twMerge(
|
||||||
|
'flex h-6 w-8 items-center justify-center rounded-md text-xl hover:!bg-[#FCB017] disabled:cursor-not-allowed disabled:text-slate-400 disabled:opacity-50',
|
||||||
|
isActive ? 'bg-[#FCB01755]' : '',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
432
app/components/text-editor/editor-menubar.tsx
Normal file
432
app/components/text-editor/editor-menubar.tsx
Normal file
@ -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<SetStateAction<boolean>>
|
||||||
|
category: string
|
||||||
|
darkMode: boolean
|
||||||
|
setDarkMode: Dispatch<SetStateAction<boolean>>
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start justify-between gap-4 rounded-[5px_5px_0_0] border-x border-t border-[#D2D2D2] px-4 py-3">
|
||||||
|
<div className="flex divide-x">
|
||||||
|
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
disabled={
|
||||||
|
disabled || !editor.can().chain().focus().toggleBold().run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('bold')}
|
||||||
|
title="Bold"
|
||||||
|
>
|
||||||
|
<BoldIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
disabled={
|
||||||
|
disabled || !editor.can().chain().focus().toggleItalic().run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('italic')}
|
||||||
|
title="Italic"
|
||||||
|
>
|
||||||
|
<ItalicIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
disabled={
|
||||||
|
disabled || !editor.can().chain().focus().toggleStrike().run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('strike')}
|
||||||
|
title="Strike"
|
||||||
|
>
|
||||||
|
<StrikethroughIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<div className="relative">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => setIsOpenColor(true)}
|
||||||
|
title="Text Color"
|
||||||
|
style={{
|
||||||
|
color: rgbColor,
|
||||||
|
}}
|
||||||
|
isActive={true}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SwatchIcon />
|
||||||
|
</EditorButton>
|
||||||
|
{isOpenColor && (
|
||||||
|
<div
|
||||||
|
className="border-md absolute top-8 left-0"
|
||||||
|
ref={popover}
|
||||||
|
>
|
||||||
|
<HexColorPicker
|
||||||
|
className="z-10"
|
||||||
|
color={rgbColor}
|
||||||
|
onChange={handleChangeColor}
|
||||||
|
/>
|
||||||
|
<div className="">
|
||||||
|
<HexColorInput
|
||||||
|
color={rgbColor}
|
||||||
|
onChange={handleChangeColor}
|
||||||
|
prefixed
|
||||||
|
className="relative z-10 mt-1 flex w-full rounded-lg border px-3 py-2 text-sm focus:ring-0 focus-visible:outline-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!editor.can().chain().focus().setTextAlign('left').run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive({ textAlign: 'left' })}
|
||||||
|
title="Align Left"
|
||||||
|
>
|
||||||
|
<Bars3BottomLeftIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().setTextAlign('center').run()
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!editor.can().chain().focus().setTextAlign('center').run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive({ textAlign: 'center' })}
|
||||||
|
title="Align Center"
|
||||||
|
>
|
||||||
|
<Bars3Icon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!editor.can().chain().focus().setTextAlign('right').run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive({ textAlign: 'right' })}
|
||||||
|
title="Align Right"
|
||||||
|
>
|
||||||
|
<Bars3BottomRightIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().setTextAlign('justify').run()
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!editor.can().chain().focus().setTextAlign('justify').run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive({ textAlign: 'justify' })}
|
||||||
|
title="Align Justify"
|
||||||
|
>
|
||||||
|
<Bars4Icon />
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('heading', { level: 1 })}
|
||||||
|
title="Heading 1"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<H1Icon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('heading', { level: 2 })}
|
||||||
|
title="Heading 2"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<H2Icon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('heading', { level: 3 })}
|
||||||
|
title="Heading 3"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<H3Icon />
|
||||||
|
</EditorButton>
|
||||||
|
{/* <EditorButton
|
||||||
|
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||||
|
isActive={editor.isActive('paragraph')}
|
||||||
|
title="Paragraph"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<RiParagraph />
|
||||||
|
</EditorButton> */}
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
isActive={editor.isActive('bulletList')}
|
||||||
|
title="Bullet List"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ListBulletIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
isActive={editor.isActive('orderedList')}
|
||||||
|
title="Ordered List"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<NumberedListIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
isActive={editor.isActive('codeBlock')}
|
||||||
|
title="Code Block"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<CodeBracketIcon />
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
isActive={editor.isActive('blockquote')}
|
||||||
|
title="Blockquote"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<RiDoubleQuotesL />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
title="Horizontal Rule"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<RiSeparator />
|
||||||
|
</EditorButton>
|
||||||
|
</div> */}
|
||||||
|
{/* <div className="flex items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||||
|
title="Hard Break"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<RiTextWrap />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => {
|
||||||
|
editor.chain().focus().unsetAllMarks().run()
|
||||||
|
editor.chain().focus().clearNodes().run()
|
||||||
|
}}
|
||||||
|
title="Clear Format"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<RiFormatClear />
|
||||||
|
</EditorButton>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => toggleUpload()}
|
||||||
|
title="Insert Image"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<PhotoIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => setLink()}
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!editor
|
||||||
|
.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.extendMarkRange('link')
|
||||||
|
.setLink({ href: '' })
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
isActive={editor.isActive('link')}
|
||||||
|
title="Set Link"
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||||
|
disabled={disabled || !editor.isActive('link')}
|
||||||
|
title="Unset Link"
|
||||||
|
>
|
||||||
|
<LinkSlashIcon />
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={disabled || !editor.can().chain().focus().undo().run()}
|
||||||
|
title="Undo"
|
||||||
|
>
|
||||||
|
<ArrowUturnLeftIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={disabled || !editor.can().chain().focus().redo().run()}
|
||||||
|
title="Redo"
|
||||||
|
>
|
||||||
|
<ArrowUturnRightIcon />
|
||||||
|
</EditorButton>
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => toggleDark()}
|
||||||
|
title={darkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
||||||
|
isActive={true}
|
||||||
|
>
|
||||||
|
{darkMode ? <MoonIcon /> : <SunIcon />}
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => setIsPlainHTML(true)}
|
||||||
|
title="Switch to Plain Text"
|
||||||
|
isActive={true}
|
||||||
|
>
|
||||||
|
<DocumentTextIcon />
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <Dialog
|
||||||
|
isOpen={isOpenImage}
|
||||||
|
setIsOpen={setIsOpenImage}
|
||||||
|
title="Insert Image"
|
||||||
|
showCloseButton={true}
|
||||||
|
>
|
||||||
|
<UploadProvider
|
||||||
|
data={{
|
||||||
|
onCancel: () => setIsOpenImage(false),
|
||||||
|
onSave: (file) => {
|
||||||
|
addImage(file)
|
||||||
|
setIsOpenImage(false)
|
||||||
|
},
|
||||||
|
category: category,
|
||||||
|
maxFileSize: 300,
|
||||||
|
selectedFile: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Upload />
|
||||||
|
</UploadProvider>
|
||||||
|
</Dialog> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
app/components/text-editor/editor-textarea.tsx
Normal file
56
app/components/text-editor/editor-textarea.tsx
Normal file
@ -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<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
|
||||||
|
// ssr: false,
|
||||||
|
// })
|
||||||
|
|
||||||
|
export const EditorTextArea = (properties: TProperties) => {
|
||||||
|
const { setIsPlainHTML, name, disabled = false } = properties
|
||||||
|
|
||||||
|
const { control } = useRemixFormContext()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-end rounded-[5px_5px_0_0] border-x border-t border-[#D2D2D2] px-4 py-3">
|
||||||
|
<div className="flex gap-1 px-1">
|
||||||
|
<EditorButton
|
||||||
|
onClick={() => setIsPlainHTML(false)}
|
||||||
|
title="Switch to Rich Text"
|
||||||
|
isActive={true}
|
||||||
|
>
|
||||||
|
<CodeBracketSquareIcon />
|
||||||
|
</EditorButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<MonacoEditor
|
||||||
|
language="html"
|
||||||
|
onChange={(newValue) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
151
app/components/text-editor/index.tsx
Normal file
151
app/components/text-editor/index.tsx
Normal file
@ -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<TFormValues extends FieldValues> = {
|
||||||
|
id?: string
|
||||||
|
name: Path<TFormValues>
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
labelClassName?: string
|
||||||
|
className?: string
|
||||||
|
inputClassName?: string
|
||||||
|
rules?: RegisterOptions
|
||||||
|
disabled?: boolean
|
||||||
|
isRequired?: boolean
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
||||||
|
properties: TProperties<TFormValues>,
|
||||||
|
) => {
|
||||||
|
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 (
|
||||||
|
<div className={twMerge('', className)}>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={id ?? generatedId}
|
||||||
|
className={twMerge('mb-2 block text-sm font-bold', labelClassName)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{isRequired && <sup className="text-[#DF0000]">*</sup>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPlainHTML ? (
|
||||||
|
<EditorTextArea
|
||||||
|
setIsPlainHTML={setIsPlainHTML}
|
||||||
|
name={name}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EditorMenuBar
|
||||||
|
disabled={disabled}
|
||||||
|
category={category}
|
||||||
|
editor={editor}
|
||||||
|
setIsPlainHTML={setIsPlainHTML}
|
||||||
|
darkMode={darkMode}
|
||||||
|
setDarkMode={setDarkMode}
|
||||||
|
/>
|
||||||
|
<EditorContent
|
||||||
|
readOnly={disabled}
|
||||||
|
editor={editor}
|
||||||
|
id={id ?? generatedId}
|
||||||
|
className={twMerge(
|
||||||
|
'prose mb-1 max-h-96 max-w-none cursor-text overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] px-4 py-1',
|
||||||
|
darkMode ? 'bg-[#00000055]' : '',
|
||||||
|
inputClassName,
|
||||||
|
)}
|
||||||
|
onClick={() => editor?.commands.focus()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-[#DF0000] italic">
|
||||||
|
{error?.message?.toString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ type TInputProperties<T extends FieldValues> = Omit<
|
|||||||
label?: ReactNode
|
label?: ReactNode
|
||||||
name: Path<T>
|
name: Path<T>
|
||||||
rules?: RegisterOptions
|
rules?: RegisterOptions
|
||||||
|
containerClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = <TFormValues extends Record<string, unknown>>(
|
export const Input = <TFormValues extends Record<string, unknown>>(
|
||||||
@ -36,6 +37,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
|||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
|
containerClassName,
|
||||||
...rest
|
...rest
|
||||||
} = properties
|
} = properties
|
||||||
const [inputType, setInputType] = useState(type)
|
const [inputType, setInputType] = useState(type)
|
||||||
@ -49,7 +51,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className="relative"
|
className={twMerge('relative', containerClassName)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
|
|||||||
27
app/hooks/use-click-outside.ts
Normal file
27
app/hooks/use-click-outside.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, type RefObject } from 'react'
|
||||||
|
|
||||||
|
type Event = MouseEvent | TouchEvent
|
||||||
|
|
||||||
|
export const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
||||||
|
reference: RefObject<T | null>,
|
||||||
|
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])
|
||||||
|
}
|
||||||
@ -5,10 +5,10 @@ import { useFetcher, useRouteLoaderData } from 'react-router'
|
|||||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { TextEditor } from '~/components/text-editor'
|
||||||
import { Button } from '~/components/ui/button'
|
import { Button } from '~/components/ui/button'
|
||||||
import { Combobox } from '~/components/ui/combobox'
|
import { Combobox } from '~/components/ui/combobox'
|
||||||
import { Input } from '~/components/ui/input'
|
import { Input } from '~/components/ui/input'
|
||||||
import DefaultTextEditor from '~/components/ui/text-editor'
|
|
||||||
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
||||||
import type { loader } from '~/routes/_admin.lg-admin'
|
import type { loader } from '~/routes/_admin.lg-admin'
|
||||||
|
|
||||||
@ -38,10 +38,10 @@ export const contentSchema = z.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
),
|
),
|
||||||
title: z.string().min(1, {
|
title: z.string().min(1, {
|
||||||
message: 'Title is required',
|
message: 'Judul is required',
|
||||||
}),
|
}),
|
||||||
content: z.string().min(1, {
|
content: z.string().min(1, {
|
||||||
message: 'Content is required',
|
message: 'Konten is required',
|
||||||
}),
|
}),
|
||||||
featured_image: z.string().optional(),
|
featured_image: z.string().optional(),
|
||||||
is_premium: z.boolean().optional(),
|
is_premium: z.boolean().optional(),
|
||||||
@ -95,6 +95,24 @@ export const CreateContentsPage = () => {
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm text-red-500 capitalize">{error}</div>
|
<div className="text-sm text-red-500 capitalize">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex items-end justify-between gap-4">
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan Judul"
|
||||||
|
name="title"
|
||||||
|
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||||
|
containerClassName="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="featured_image"
|
||||||
|
label="Gambar Unggulan"
|
||||||
|
placeholder="Masukkan Gambar Unggulan"
|
||||||
|
name="featured_image"
|
||||||
|
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||||
|
containerClassName="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex items-end justify-between gap-4">
|
<div className="flex items-end justify-between gap-4">
|
||||||
<Combobox
|
<Combobox
|
||||||
multiple
|
multiple
|
||||||
@ -144,9 +162,14 @@ export const CreateContentsPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<TextEditor
|
||||||
<DefaultTextEditor />
|
id="content"
|
||||||
</section>
|
name="content"
|
||||||
|
label="Konten"
|
||||||
|
placeholder="Masukkan Konten"
|
||||||
|
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||||
|
category="content"
|
||||||
|
/>
|
||||||
</fetcher.Form>
|
</fetcher.Form>
|
||||||
</RemixFormProvider>
|
</RemixFormProvider>
|
||||||
|
|
||||||
|
|||||||
39
app/utils/color.ts
Normal file
39
app/utils/color.ts
Normal file
@ -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}`
|
||||||
|
}
|
||||||
@ -17,9 +17,16 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^4.1.1",
|
"@hookform/resolvers": "^4.1.1",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@react-router/fs-routes": "^7.1.3",
|
"@react-router/fs-routes": "^7.1.3",
|
||||||
"@react-router/node": "^7.1.3",
|
"@react-router/node": "^7.1.3",
|
||||||
"@react-router/serve": "^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/react": "^2.11.5",
|
||||||
"@tiptap/starter-kit": "^2.11.5",
|
"@tiptap/starter-kit": "^2.11.5",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
@ -32,6 +39,7 @@
|
|||||||
"jose": "^6.0.8",
|
"jose": "^6.0.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
|||||||
121
pnpm-lock.yaml
generated
121
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ importers:
|
|||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1(react-hook-form@7.54.2(react@19.0.0))
|
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':
|
'@react-router/fs-routes':
|
||||||
specifier: ^7.1.3
|
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)
|
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':
|
'@react-router/serve':
|
||||||
specifier: ^7.1.3
|
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)
|
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':
|
'@tiptap/react':
|
||||||
specifier: ^2.11.5
|
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)
|
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:
|
react-chartjs-2:
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0(chart.js@4.4.8)(react@19.0.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:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
@ -708,6 +732,16 @@ packages:
|
|||||||
'@mjackson/node-fetch-server@0.2.0':
|
'@mjackson/node-fetch-server@0.2.0':
|
||||||
resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==}
|
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':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1474,6 +1508,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-document@2.11.5':
|
||||||
resolution: {integrity: sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==}
|
resolution: {integrity: sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1507,6 +1547,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-history@2.11.5':
|
||||||
resolution: {integrity: sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==}
|
resolution: {integrity: sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1519,11 +1564,22 @@ packages:
|
|||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
'@tiptap/pm': ^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':
|
'@tiptap/extension-italic@2.11.5':
|
||||||
resolution: {integrity: sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==}
|
resolution: {integrity: sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-list-item@2.11.5':
|
||||||
resolution: {integrity: sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==}
|
resolution: {integrity: sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1544,6 +1600,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-text-style@2.11.5':
|
||||||
resolution: {integrity: sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==}
|
resolution: {integrity: sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3173,6 +3234,9 @@ packages:
|
|||||||
linkify-it@5.0.0:
|
linkify-it@5.0.0:
|
||||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
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:
|
lint-staged@15.4.3:
|
||||||
resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==}
|
resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
@ -3337,6 +3401,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2:
|
||||||
|
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||||
|
|
||||||
morgan@1.10.0:
|
morgan@1.10.0:
|
||||||
resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
|
resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -3791,6 +3858,12 @@ packages:
|
|||||||
chart.js: ^4.1.1
|
chart.js: ^4.1.1
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
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:
|
react-d3-tree@3.6.2:
|
||||||
resolution: {integrity: sha512-1ExQlmEnv5iOw9XfZ3EcESDjzGXVKPAmyDJTJbvVfiwkplZtP7CcNEY0tKZf4XSW0FzYJf4aFXprGJen+95yuw==}
|
resolution: {integrity: sha512-1ExQlmEnv5iOw9XfZ3EcESDjzGXVKPAmyDJTJbvVfiwkplZtP7CcNEY0tKZf4XSW0FzYJf4aFXprGJen+95yuw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4135,6 +4208,9 @@ packages:
|
|||||||
stable-hash@0.0.4:
|
stable-hash@0.0.4:
|
||||||
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
||||||
|
|
||||||
|
state-local@1.0.7:
|
||||||
|
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -5214,6 +5290,17 @@ snapshots:
|
|||||||
|
|
||||||
'@mjackson/node-fetch-server@0.2.0': {}
|
'@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':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@ -5948,6 +6035,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@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))':
|
'@tiptap/extension-document@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -5976,6 +6068,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@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)':
|
'@tiptap/extension-history@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@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/core': 2.11.5(@tiptap/pm@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))':
|
'@tiptap/extension-italic@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@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))':
|
'@tiptap/extension-list-item@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -6006,6 +6112,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@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))':
|
'@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -7882,6 +7992,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro: 2.1.0
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
|
linkifyjs@4.2.0: {}
|
||||||
|
|
||||||
lint-staged@15.4.3:
|
lint-staged@15.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.4.1
|
chalk: 5.4.1
|
||||||
@ -8028,6 +8140,8 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2: {}
|
||||||
|
|
||||||
morgan@1.10.0:
|
morgan@1.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
basic-auth: 2.0.1
|
basic-auth: 2.0.1
|
||||||
@ -8457,6 +8571,11 @@ snapshots:
|
|||||||
chart.js: 4.4.8
|
chart.js: 4.4.8
|
||||||
react: 19.0.0
|
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):
|
react-d3-tree@3.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bkrem/react-transition-group': 1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
'@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: {}
|
stable-hash@0.0.4: {}
|
||||||
|
|
||||||
|
state-local@1.0.7: {}
|
||||||
|
|
||||||
statuses@2.0.1: {}
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
stream-shift@1.0.3: {}
|
stream-shift@1.0.3: {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user