Example: typography
Typography enhancements like smart quotes and dashes.
Install this example with
shadcn: npx shadcn@latest add @prosekit/react-example-typographynpx shadcn@latest add @prosekit/preact-example-typographynpx shadcn@latest add @prosekit/solid-example-typographynpx shadcn@latest add @prosekit/svelte-example-typographynpx shadcn@latest add @prosekit/vue-example-typographyimport 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps) {
const defaultContent = props.initialContent ?? sampleContent
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [defaultContent])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>'use client'
export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopover } from 'prosekit/react/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'import { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { useMemo } from 'preact/hooks'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps) {
const defaultContent = props.initialContent ?? sampleContent
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [defaultContent])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopover } from 'prosekit/preact/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'import { DropIndicator as BaseDropIndicator } from 'prosekit/preact/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/solid'
import type { JSX } from 'solid-js'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps): JSX.Element {
const defaultContent = props.initialContent ?? sampleContent
const extension = defineExtension()
const editor = createEditor({ extension, defaultContent })
return (
<ProseKit editor={editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopover } from 'prosekit/solid/block-handle'
import type { JSX } from 'solid-js'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props): JSX.Element {
return (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'import { DropIndicator as BaseDropIndicator } from 'prosekit/solid/drop-indicator'
import type { JSX } from 'solid-js'
export default function DropIndicator(): JSX.Element {
return <BaseDropIndicator class="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/svelte'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
const props: {
initialContent?: NodeJSON
} = $props()
const extension = defineExtension()
const defaultContent = props.initialContent ?? sampleContent
const editor = createEditor({ extension, defaultContent })
</script>
<ProseKit {editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div {@attach editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.svelte'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}<script lang="ts">
import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopover } from 'prosekit/svelte/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props: Props = $props()
</script>
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block"></div>
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block"></div>
</BlockHandleDraggable>
</BlockHandlePopover>export { default as BlockHandle } from './block-handle.svelte'<script lang="ts">
import { DropIndicator as BaseDropIndicator } from 'prosekit/svelte/drop-indicator'
</script>
<BaseDropIndicator class="z-50 transition-all bg-blue-500" />export { default as DropIndicator } from './drop-indicator.svelte'<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
const props = defineProps<{
initialContent?: NodeJSON
}>()
const extension = defineExtension()
const defaultContent = props.initialContent ?? sampleContent
const editor = createEditor({ extension, defaultContent })
</script>
<template>
<ProseKit :editor="editor">
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div :ref="(el) => editor.mount(el as HTMLElement | null)" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}<script setup lang="ts">
import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopover } from 'prosekit/vue/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props = defineProps<Props>()
</script>
<template>
<BlockHandlePopover
:placement="props.dir === 'rtl' ? 'right' : 'left'"
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
</template>export { default as BlockHandle } from './block-handle.vue'<script setup lang="ts">
import { DropIndicator as BaseDropIndicator } from 'prosekit/vue/drop-indicator'
</script>
<template>
<BaseDropIndicator class="z-50 transition-all bg-blue-500" />
</template>export { default as DropIndicator } from './drop-indicator.vue'