layout optimization

This commit is contained in:
Anton Tranelis 2025-06-09 16:39:57 +02:00
parent ff835d677c
commit 212456e5c2
5 changed files with 203 additions and 28 deletions

39
package-lock.json generated
View File

@ -11,8 +11,10 @@
"dependencies": {
"@heroicons/react": "^2.0.17",
"@tanstack/react-query": "^5.17.8",
"@tiptap/extension-bubble-menu": "^2.14.0",
"@tiptap/extension-color": "^2.12.0",
"@tiptap/extension-image": "^2.12.0",
"@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-youtube": "^2.12.0",
"@tiptap/pm": "^2.12.0",
@ -2410,9 +2412,9 @@
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.12.0.tgz",
"integrity": "sha512-DYijoE0igV0Oi+ZppFsp2UrQsM/4HZtmmpD78BJM9zfCbd1YvAUIxmzmXr8uqU18OHd1uQy+/zvuNoUNYyw67g==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.14.0.tgz",
"integrity": "sha512-sN15n0RjPh+2Asvxs7l47hVEvX6c0aPempU8QQWcPUlHoGf1D/XkyHXy6GWVPSxZ5Rj5uAwgKvhHsG/FJ/YGKQ==",
"license": "MIT",
"dependencies": {
"tippy.js": "^6.3.7"
@ -2593,9 +2595,9 @@
}
},
"node_modules/@tiptap/extension-image": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.12.0.tgz",
"integrity": "sha512-wO+yrfMlnW3SYCb1Q1qAb+nt5WH6jnlQPTV6qdoIabRtW0puwMWULZDUgclPN5hxn8EXb9vBEu44egvH6hgkfQ==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.14.0.tgz",
"integrity": "sha512-pYCUzZBgsxIvVGTzuW03cPz6PIrAo26xpoxqq4W090uMVoK0SgY5W5y0IqCdw4QyLkJ2/oNSFNc2EP9jVi1CcQ==",
"license": "MIT",
"funding": {
"type": "github",
@ -2618,6 +2620,23 @@
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-link": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.14.0.tgz",
"integrity": "sha512-fsqW7eRD2xoD6xy7eFrNPAdIuZ3eicA4jKC45Vcft/Xky0DJoIehlVBLxsPbfmv3f27EBrtPkg5+msLXkLyzJA==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.12.0.tgz",
@ -8378,6 +8397,12 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/linkifyjs": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz",
"integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==",
"license": "MIT"
},
"node_modules/listr2": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",

View File

@ -99,8 +99,10 @@
"dependencies": {
"@heroicons/react": "^2.0.17",
"@tanstack/react-query": "^5.17.8",
"@tiptap/extension-bubble-menu": "^2.14.0",
"@tiptap/extension-color": "^2.12.0",
"@tiptap/extension-image": "^2.12.0",
"@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-youtube": "^2.12.0",
"@tiptap/pm": "^2.12.0",

View File

@ -5,6 +5,7 @@ import { Color } from '@tiptap/extension-color'
import { Image } from '@tiptap/extension-image'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Youtube } from '@tiptap/extension-youtube'
import { Link } from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { useEffect } from 'react'
@ -56,6 +57,7 @@ export function RichTextEditor({
}),
Markdown,
Image,
Link,
Youtube.configure({
controls: false,
nocookie: true,

View File

@ -1,3 +1,5 @@
import LinkIcon from '@heroicons/react/24/outline/LinkIcon'
import PhotoIcon from '@heroicons/react/24/outline/PhotoIcon'
import BoldIcon from '@heroicons/react/24/solid/BoldIcon'
import H1Icon from '@heroicons/react/24/solid/H1Icon'
import H2Icon from '@heroicons/react/24/solid/H2Icon'
@ -6,6 +8,7 @@ import ItalicIcon from '@heroicons/react/24/solid/ItalicIcon'
import ListBulletIcon from '@heroicons/react/24/solid/ListBulletIcon'
import NumberedListIcon from '@heroicons/react/24/solid/NumberedListIcon'
import { useEditorState } from '@tiptap/react'
import { useCallback, useEffect, useState } from 'react'
import { MdUndo, MdRedo, MdHorizontalRule } from 'react-icons/md'
import type { Editor } from '@tiptap/react'
@ -31,6 +34,7 @@ export const TextEditorMenu = ({ editor }: { editor: Editor }) => {
isHeading4: ctx.editor.isActive('heading', { level: 4 }),
isHeading5: ctx.editor.isActive('heading', { level: 5 }),
isHeading6: ctx.editor.isActive('heading', { level: 6 }),
isHeading: ctx.editor.isActive('heading'),
isBulletList: ctx.editor.isActive('bulletList'),
isOrderedList: ctx.editor.isActive('orderedList'),
isCodeBlock: ctx.editor.isActive('codeBlock'),
@ -41,31 +45,29 @@ export const TextEditorMenu = ({ editor }: { editor: Editor }) => {
},
})
const addImage = useCallback(() => {
const url = window.prompt('URL')
if (url) {
editor.chain().focus().setImage({ src: url }).run()
}
}, [editor])
const [url, setUrl] = useState<string>('')
const setLink = (e: React.MouseEvent) => {
e.preventDefault()
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
return (
<>
<ul
className={
'tw:menu tw:p-1 tw:menu-horizontal tw:flex-nowrap tw:overflow-x-hidden tw:flex-none tw:bg-base-200 tw:rounded-box tw:w-full tw:rounded-b-none'
'tw:menu tw:overflow-x-hidden tw:sm:overflow-visible tw:md:overflow-x-hidden tw:lg:overflow-visible tw:p-1 tw:menu-horizontal tw:flex-nowrap tw:flex-none tw:bg-base-200 tw:rounded-box tw:w-full tw:rounded-b-none'
}
>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isBold ? 'tw:bg-base-content/10' : ''}`}
data-tip='Bold'
onClick={() => editor.chain().focus().toggleBold().run()}
>
<BoldIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isItalic ? 'tw:bg-base-content/10' : ''}`}
data-tip='Italic'
onClick={() => editor.chain().focus().toggleItalic().run()}
>
<ItalicIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isHeading1 ? 'tw:bg-base-content/10' : ''}`}
@ -93,7 +95,30 @@ export const TextEditorMenu = ({ editor }: { editor: Editor }) => {
<H3Icon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div className='tw:w-[1px] tw:p-0 tw:mx-1 tw:bg-base-content/10 tw:my-1' />
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isBold ? 'tw:bg-base-content/10' : ''}`}
data-tip='Bold'
onClick={() => editor.chain().focus().toggleBold().run()}
>
<BoldIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isItalic ? 'tw:bg-base-content/10' : ''}`}
data-tip='Italic'
onClick={() => editor.chain().focus().toggleItalic().run()}
>
<ItalicIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div className='tw:w-[1px] tw:p-0 tw:mx-1 tw:bg-base-content/10 tw:my-1' />
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editorState.isBulletList ? 'tw:bg-base-content/10' : ''}`}
@ -112,6 +137,63 @@ export const TextEditorMenu = ({ editor }: { editor: Editor }) => {
<NumberedListIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div className='tw:w-[1px] tw:p-0 tw:mx-1 tw:bg-base-content/10 tw:my-1' />
</li>
<li className='tw:hidden tw:sm:block tw:md:hidden tw:lg:block'>
{!editor.isActive('link') ? (
<div
tabIndex={0}
role='button'
className='tw:dropdown tw:dropdown-end tw:px-1.5 tw:mx-0.5 tw:pt-1.5 tw:pb-0 tw:cursor-pointer '
>
<div
className={`tw:tooltip tw:h-full ${editor.isActive('link') ? 'tw:bg-base-content/10' : ''}`}
data-tip='Link'
>
<LinkIcon className='tw:h-full tw:w-5' />
</div>
<div
tabIndex={0}
className='tw:dropdown-content tw:bg-base-200 tw:card tw:card-body tw:card-sm tw:bg-base-100 z-1 w-64 shadow-md'
>
<div className='tw:join'>
<div>
<label className='tw:input tw:validator tw:join-item tw:w-58'>
<LinkIcon className='tw:h-full tw:w-4' />
<input
onChange={(e) => setUrl(e.target.value)}
type='url'
placeholder='https://...'
/>
</label>
<div className='tw:validator-hint tw:hidden'>Enter valid url</div>
</div>
<button
className='tw:btn tw:btn-neutral tw:join-item'
onClick={(e) => setLink(e)}
>
Set Link
</button>
</div>
</div>
</div>
) : (
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 ${editor.isActive('link') ? 'tw:bg-base-content/10' : ''}`}
data-tip='List'
onClick={() => editor.chain().focus().extendMarkRange('link').unsetLink().run()}
>
<LinkIcon className='tw:w-5 tw:h-5' />
</div>
)}
</li>
<li>
<div className='tw:tooltip tw:px-1.5 tw:mx-0.5' data-tip='Image' onClick={addImage}>
<PhotoIcon className='tw:w-5 tw:h-5' />
</div>
</li>
<li>
<div
className='tw:tooltip tw:px-1'
@ -148,6 +230,43 @@ export const TextEditorMenu = ({ editor }: { editor: Editor }) => {
</button>
</div>
</div> */}
{/** <BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div className='bubble-menu tw:card tw:card-body tw:rounded-box tw:border tw:border-base-content/20 tw:bg-base-200 tw:shadow'>
<ul
className={
'tw:menu tw:p-1 tw:menu-horizontal tw:flex-nowrap tw:flex-none tw:bg-base-200 tw:rounded-box tw:w-full tw:rounded-b-none'
}
>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-0.5 tw:cursor-pointer ${editorState.isBold ? 'tw:bg-base-content/10' : ''}`}
data-tip='Bold'
onClick={() => editor.chain().focus().toggleBold().run()}
>
<BoldIcon className='tw:w-4 tw:h-4' />
</div>
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-1 tw:cursor-pointer ${editorState.isItalic ? 'tw:bg-base-content/10' : ''}`}
data-tip='Italic'
onClick={() => editor.chain().focus().toggleItalic().run()}
>
<ItalicIcon className='tw:w-4 tw:h-4' />
</div>
</li>
<li>
<div
className={`tw:tooltip tw:px-1.5 tw:mx-1 tw:cursor-pointer ${editor.isActive('link') ? 'tw:bg-base-content/10' : ''}`}
data-tip='Link'
onClick={setLink}
>
<LinkIcon className='tw:w-4 tw:h-4' />
</div>
</li>
</ul>
</div>
</BubbleMenu> */}
</>
)
}

View File

@ -14,3 +14,30 @@
height: 0;
pointer-events: none;
}
/* Bubble menu */
.bubble-menu {
background-color: var(--color-base-100);
border: 1px solid var(--color-base-200);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
padding: 0.2rem;
button {
background-color: unset;
&:hover {
background-color: var(--color-base-300);
}
&.is-active {
background-color: var(--color-base-200);
&:hover {
background-color: var(--color-base-300);
}
}
}
}