Replace editor #174
3546
package-lock.json
generated
3546
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/lightning-tools": "5.0.3",
|
"@getalby/lightning-tools": "5.0.3",
|
||||||
|
"@mdxeditor/editor": "^3.20.0",
|
||||||
"@nostr-dev-kit/ndk": "2.10.0",
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
|
64
src/components/editor/PlainTextCodeEditorDescriptor.tsx
Normal file
64
src/components/editor/PlainTextCodeEditorDescriptor.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
CodeBlockEditorDescriptor,
|
||||||
|
useCodeBlockEditorContext
|
||||||
|
} from '@mdxeditor/editor'
|
||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const PlainTextCodeEditorDescriptor: CodeBlockEditorDescriptor = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
match: (_language, _meta) => true,
|
||||||
|
priority: 0,
|
||||||
|
Editor: ({ code, focusEmitter }) => {
|
||||||
|
const { parentEditor, lexicalNode, setCode } = useCodeBlockEditorContext()
|
||||||
|
const defaultValue = useRef(code)
|
||||||
|
const codeRef = useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
const handleInput = useCallback(
|
||||||
|
(e: React.FormEvent<HTMLElement>) => {
|
||||||
|
setCode(e.currentTarget.innerHTML)
|
||||||
|
},
|
||||||
|
[setCode]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (codeRef.current) {
|
||||||
|
codeRef.current.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusEmitter.subscribe(handleFocus)
|
||||||
|
}, [focusEmitter])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentRef = codeRef.current
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Backspace' || event.key === 'Delete') {
|
||||||
|
if (codeRef.current?.textContent === '') {
|
||||||
|
parentEditor.update(() => {
|
||||||
|
lexicalNode.remove(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.addEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lexicalNode, parentEditor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre>
|
||||||
|
<code
|
||||||
|
ref={codeRef}
|
||||||
|
contentEditable={true}
|
||||||
|
onInput={handleInput}
|
||||||
|
dangerouslySetInnerHTML={{ __html: defaultValue.current }}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
36
src/components/editor/YoutubeButton.tsx
Normal file
36
src/components/editor/YoutubeButton.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { LeafDirective } from 'mdast-util-directive'
|
||||||
|
import { usePublisher, insertDirective$, DialogButton } from '@mdxeditor/editor'
|
||||||
|
|
||||||
|
function getId(url: string) {
|
||||||
|
const regExp =
|
||||||
|
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
|
||||||
|
const match = url.match(regExp)
|
||||||
|
return match && match[7].length == 11 ? match[7] : false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YouTubeButton = () => {
|
||||||
|
const insertDirective = usePublisher(insertDirective$)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogButton
|
||||||
|
tooltipTitle='Insert Youtube video'
|
||||||
|
submitButtonTitle='Insert video'
|
||||||
|
dialogInputPlaceholder='Paste the youtube video URL'
|
||||||
|
buttonContent='YT'
|
||||||
|
onSubmit={(url) => {
|
||||||
|
const videoId = getId(url)
|
||||||
|
if (videoId) {
|
||||||
|
insertDirective({
|
||||||
|
name: 'youtube',
|
||||||
|
type: 'leafDirective',
|
||||||
|
|
||||||
|
attributes: { id: videoId },
|
||||||
|
children: []
|
||||||
|
} as LeafDirective)
|
||||||
|
} else {
|
||||||
|
alert('Invalid YouTube URL')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
49
src/components/editor/YoutubeDirectiveDescriptor.tsx
Normal file
49
src/components/editor/YoutubeDirectiveDescriptor.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { LeafDirective } from 'mdast-util-directive'
|
||||||
|
import { DirectiveDescriptor } from '@mdxeditor/editor'
|
||||||
|
|
||||||
|
interface YoutubeDirectiveNode extends LeafDirective {
|
||||||
|
name: 'youtube'
|
||||||
|
attributes: { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YoutubeDirectiveDescriptor: DirectiveDescriptor<YoutubeDirectiveNode> =
|
||||||
|
{
|
||||||
|
name: 'youtube',
|
||||||
|
type: 'leafDirective',
|
||||||
|
testNode(node) {
|
||||||
|
return node.name === 'youtube'
|
||||||
|
},
|
||||||
|
attributes: ['id'],
|
||||||
|
hasChildren: false,
|
||||||
|
Editor: ({ mdastNode, lexicalNode, parentEditor }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => {
|
||||||
|
parentEditor.update(() => {
|
||||||
|
lexicalNode.selectNext()
|
||||||
|
lexicalNode.remove()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
<iframe
|
||||||
|
width='560'
|
||||||
|
height='315'
|
||||||
|
src={`https://www.youtube.com/embed/${mdastNode.attributes.id}`}
|
||||||
|
title='YouTube video player'
|
||||||
|
frameBorder='0'
|
||||||
|
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
105
src/components/editor/index.tsx
Normal file
105
src/components/editor/index.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
BlockTypeSelect,
|
||||||
|
BoldItalicUnderlineToggles,
|
||||||
|
codeBlockPlugin,
|
||||||
|
CodeToggle,
|
||||||
|
CreateLink,
|
||||||
|
directivesPlugin,
|
||||||
|
headingsPlugin,
|
||||||
|
imagePlugin,
|
||||||
|
InsertCodeBlock,
|
||||||
|
InsertImage,
|
||||||
|
InsertTable,
|
||||||
|
InsertThematicBreak,
|
||||||
|
linkDialogPlugin,
|
||||||
|
linkPlugin,
|
||||||
|
listsPlugin,
|
||||||
|
ListsToggle,
|
||||||
|
markdownShortcutPlugin,
|
||||||
|
MDXEditor,
|
||||||
|
MDXEditorProps,
|
||||||
|
quotePlugin,
|
||||||
|
Separator,
|
||||||
|
StrikeThroughSupSubToggles,
|
||||||
|
tablePlugin,
|
||||||
|
thematicBreakPlugin,
|
||||||
|
toolbarPlugin,
|
||||||
|
UndoRedo
|
||||||
|
} from '@mdxeditor/editor'
|
||||||
|
import { PlainTextCodeEditorDescriptor } from './PlainTextCodeEditorDescriptor'
|
||||||
|
import { YoutubeDirectiveDescriptor } from './YoutubeDirectiveDescriptor'
|
||||||
|
import { YouTubeButton } from './YoutubeButton'
|
||||||
|
import '@mdxeditor/editor/style.css'
|
||||||
|
import '../../styles/mdxEditor.scss'
|
||||||
|
|
||||||
|
interface EditorProps extends MDXEditorProps {}
|
||||||
|
|
||||||
|
export const Editor = ({
|
||||||
|
readOnly,
|
||||||
|
markdown,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}: EditorProps) => {
|
||||||
|
const plugins = [
|
||||||
|
directivesPlugin({ directiveDescriptors: [YoutubeDirectiveDescriptor] }),
|
||||||
|
codeBlockPlugin({
|
||||||
|
defaultCodeBlockLanguage: '',
|
||||||
|
codeBlockEditorDescriptors: [PlainTextCodeEditorDescriptor]
|
||||||
|
}),
|
||||||
|
headingsPlugin(),
|
||||||
|
quotePlugin(),
|
||||||
|
imagePlugin(),
|
||||||
|
tablePlugin(),
|
||||||
|
linkPlugin(),
|
||||||
|
linkDialogPlugin(),
|
||||||
|
listsPlugin(),
|
||||||
|
thematicBreakPlugin(),
|
||||||
|
markdownShortcutPlugin()
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!readOnly) {
|
||||||
|
plugins.push(
|
||||||
|
toolbarPlugin({
|
||||||
|
toolbarContents: () => (
|
||||||
|
<>
|
||||||
|
<UndoRedo />
|
||||||
|
<Separator />
|
||||||
|
<BoldItalicUnderlineToggles />
|
||||||
|
<CodeToggle />
|
||||||
|
<Separator />
|
||||||
|
<StrikeThroughSupSubToggles />
|
||||||
|
<Separator />
|
||||||
|
<ListsToggle />
|
||||||
|
<Separator />
|
||||||
|
<BlockTypeSelect />
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<CreateLink />
|
||||||
|
<InsertImage />
|
||||||
|
<YouTubeButton />
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<InsertTable />
|
||||||
|
<InsertThematicBreak />
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
<InsertCodeBlock />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MDXEditor
|
||||||
|
contentEditableClassName='editor'
|
||||||
|
className='dark-theme dark-editor'
|
||||||
|
readOnly={readOnly}
|
||||||
|
markdown={markdown}
|
||||||
|
plugins={plugins}
|
||||||
|
onChange={onChange}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -5,12 +5,6 @@ import {
|
|||||||
useNavigation,
|
useNavigation,
|
||||||
useSubmit
|
useSubmit
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import Link from '@tiptap/extension-link'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import { EditorContent, useEditor } from '@tiptap/react'
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
import { marked } from 'marked'
|
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
import { ProfileSection } from 'components/ProfileSection'
|
import { ProfileSection } from 'components/ProfileSection'
|
||||||
import { Comments } from 'components/comment'
|
import { Comments } from 'components/comment'
|
||||||
@ -23,6 +17,7 @@ import { copyTextToClipboard } from 'utils'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { useAppSelector, useBodyScrollDisable } from 'hooks'
|
import { useAppSelector, useBodyScrollDisable } from 'hooks'
|
||||||
import { ReportPopup } from 'components/ReportPopup'
|
import { ReportPopup } from 'components/ReportPopup'
|
||||||
|
import { Editor } from 'components/editor'
|
||||||
|
|
||||||
const BLOG_REPORT_REASONS = [
|
const BLOG_REPORT_REASONS = [
|
||||||
{ label: 'Actually CP', key: 'actuallyCP' },
|
{ label: 'Actually CP', key: 'actuallyCP' },
|
||||||
@ -42,25 +37,6 @@ export const BlogPage = () => {
|
|||||||
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const [commentCount, setCommentCount] = useState(0)
|
const [commentCount, setCommentCount] = useState(0)
|
||||||
const html = marked.parse(blog?.content || '', { async: false })
|
|
||||||
const sanitized = DOMPurify.sanitize(html)
|
|
||||||
const editor = useEditor(
|
|
||||||
{
|
|
||||||
content: sanitized,
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Link,
|
|
||||||
Image.configure({
|
|
||||||
inline: true,
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'IBMSMSMBSSPostImg'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
editable: false
|
|
||||||
},
|
|
||||||
[sanitized]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [showReportPopUp, setShowReportPopUp] = useState<number>()
|
const [showReportPopUp, setShowReportPopUp] = useState<number>()
|
||||||
useBodyScrollDisable(!!showReportPopUp)
|
useBodyScrollDisable(!!showReportPopUp)
|
||||||
@ -266,7 +242,12 @@ export const BlogPage = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBSSPostBody'>
|
<div className='IBMSMSMBSSPostBody'>
|
||||||
<EditorContent editor={editor} />
|
<Editor
|
||||||
|
key={blog.id}
|
||||||
|
markdown={blog?.content || ''}
|
||||||
|
readOnly={true}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBSSTags'>
|
<div className='IBMSMSMBSSTags'>
|
||||||
{blog.nsfw && (
|
{blog.nsfw && (
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
now,
|
now,
|
||||||
parseFormData
|
parseFormData
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import TurndownService from 'turndown'
|
|
||||||
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
@ -57,9 +56,8 @@ export const writeRouteAction =
|
|||||||
// Return earily if there are any errors
|
// Return earily if there are any errors
|
||||||
if (Object.keys(formErrors).length) return formErrors
|
if (Object.keys(formErrors).length) return formErrors
|
||||||
|
|
||||||
// Get the markdown from the html
|
// Get the markdown from formData
|
||||||
const turndownService = new TurndownService()
|
const content = decodeURIComponent(formSubmit.content!)
|
||||||
const content = turndownService.turndown(formSubmit.content!)
|
|
||||||
|
|
||||||
// Check if we are editing or this is a new blog
|
// Check if we are editing or this is a new blog
|
||||||
const { naddr } = params
|
const { naddr } = params
|
||||||
@ -154,11 +152,7 @@ const validateFormData = async (
|
|||||||
errors.title = 'Title field can not be empty'
|
errors.title = 'Title field can not be empty'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!formData.content || formData.content.trim() === '') {
|
||||||
!formData.content ||
|
|
||||||
formData.content.trim() === '' ||
|
|
||||||
formData.content.trim() === '<p></p>'
|
|
||||||
) {
|
|
||||||
errors.content = 'Content field can not be empty'
|
errors.content = 'Content field can not be empty'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CheckboxFieldUncontrolled,
|
CheckboxFieldUncontrolled,
|
||||||
InputError,
|
InputError,
|
||||||
InputFieldUncontrolled,
|
InputFieldUncontrolled
|
||||||
MenuBar
|
|
||||||
} from '../../components/Inputs'
|
} from '../../components/Inputs'
|
||||||
import { ProfileSection } from '../../components/ProfileSection'
|
import { ProfileSection } from '../../components/ProfileSection'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
@ -18,14 +17,10 @@ import '../../styles/innerPage.css'
|
|||||||
import '../../styles/styles.css'
|
import '../../styles/styles.css'
|
||||||
import '../../styles/write.css'
|
import '../../styles/write.css'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
import { marked } from 'marked'
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
import { EditorContent, useEditor } from '@tiptap/react'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import Link from '@tiptap/extension-link'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import { AlertPopup } from 'components/AlertPopup'
|
import { AlertPopup } from 'components/AlertPopup'
|
||||||
|
|
||||||
|
import { Editor } from 'components/editor'
|
||||||
|
|
||||||
export const WritePage = () => {
|
export const WritePage = () => {
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
const data = useLoaderData() as BlogPageLoaderResult
|
const data = useLoaderData() as BlogPageLoaderResult
|
||||||
@ -34,26 +29,7 @@ export const WritePage = () => {
|
|||||||
|
|
||||||
const blog = data?.blog
|
const blog = data?.blog
|
||||||
const title = data?.blog ? 'Edit blog post' : 'Submit a blog post'
|
const title = data?.blog ? 'Edit blog post' : 'Submit a blog post'
|
||||||
const html = marked.parse(blog?.content || '', { async: false })
|
const [content, setContent] = useState(blog?.content || '')
|
||||||
const sanitized = DOMPurify.sanitize(html)
|
|
||||||
const [content, setContent] = useState<string>(sanitized)
|
|
||||||
const editor = useEditor({
|
|
||||||
content: content,
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Link,
|
|
||||||
Image.configure({
|
|
||||||
inline: true,
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'IBMSMSMBSSPostImg'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
setContent(editor.getHTML())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@ -94,19 +70,27 @@ export const WritePage = () => {
|
|||||||
defaultValue={blog?.title}
|
defaultValue={blog?.title}
|
||||||
error={formErrors?.title}
|
error={formErrors?.title}
|
||||||
/>
|
/>
|
||||||
{editor && (
|
|
||||||
<div className='inputLabelWrapperMain'>
|
<div className='inputLabelWrapperMain'>
|
||||||
<label className='form-label labelMain'>Content</label>
|
<label className='form-label labelMain'>Content</label>
|
||||||
<div className='inputMain'>
|
<div className='inputMain'>
|
||||||
<MenuBar editor={editor} />
|
<Editor
|
||||||
<EditorContent editor={editor} />
|
markdown={content}
|
||||||
|
onChange={(md) => {
|
||||||
|
setContent(md)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{typeof formErrors?.content !== 'undefined' && (
|
{typeof formErrors?.content !== 'undefined' && (
|
||||||
<InputError message={formErrors?.content} />
|
<InputError message={formErrors?.content} />
|
||||||
)}
|
)}
|
||||||
<input name='content' hidden value={content} readOnly />
|
{/* encode to keep the markdown formatting */}
|
||||||
|
<input
|
||||||
|
name='content'
|
||||||
|
hidden
|
||||||
|
value={encodeURIComponent(content)}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<InputFieldUncontrolled
|
<InputFieldUncontrolled
|
||||||
label='Featured Image URL'
|
label='Featured Image URL'
|
||||||
name='image'
|
name='image'
|
||||||
|
103
src/styles/mdxEditor.scss
Normal file
103
src/styles/mdxEditor.scss
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
.editor {
|
||||||
|
padding: 0;
|
||||||
|
--basePageBg: var(--slate-3);
|
||||||
|
|
||||||
|
> {
|
||||||
|
p {
|
||||||
|
margin: 5px 0 10px 0;
|
||||||
|
}
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||||
|
|
||||||
|
li p {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 15px 0 15px 0;
|
||||||
|
border-bottom: solid 1px rgb(255 255 255 / 10%);
|
||||||
|
padding: 0px 0 10px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
border-left: solid 6px rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 25px;
|
||||||
|
background: #232323;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--purple-light); // todo: fix the color
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25em 0.3em;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:empty:before {
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--black); // todo: fix the color
|
||||||
|
color: var(--white);
|
||||||
|
font-family: 'JetBrainsMono', monospace;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: #00000030;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: solid 2px rebeccapurple;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
margin: 15px 0;
|
||||||
|
background: #232323;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdxeditor,
|
||||||
|
.mdxeditor-popup-container {
|
||||||
|
--basePageBg: var(--slate-3);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user