Text Color
The textColor mark is used to represent text with a specific color. It will be rendered as <span> element with inline color style in HTML.
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-text-color'
import { defineExtension } from './extension'
import InlineMenu from './inline-menu'
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>
<InlineMenu />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineBackgroundColor } from 'prosekit/extensions/background-color'
import { defineTextColor } from 'prosekit/extensions/text-color'
export function defineExtension() {
return union(defineBasicExtension(), defineTextColor(), defineBackgroundColor())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import {
useMemo,
useState,
} from 'preact/hooks'
import type {
Editor,
Keymap,
} from 'prosekit/core'
import {
useEditorDerivedValue,
useKeymap,
} from 'prosekit/preact'
import { InlinePopover } from 'prosekit/preact/inline-popover'
import { Button } from '../../ui/button'
import type { EditorExtension } from './extension'
const textColors = [
{ label: 'Gray', value: '#9ca3af' },
{ label: 'Brown', value: '#92400e' },
{ label: 'Orange', value: '#ea580c' },
{ label: 'Yellow', value: '#ca8a04' },
{ label: 'Green', value: '#16a34a' },
{ label: 'Blue', value: '#2563eb' },
{ label: 'Purple', value: '#9333ea' },
{ label: 'Magenta', value: '#c026d3' },
{ label: 'Red', value: '#dc2626' },
]
const backgroundColors = [
{ label: 'Gray', value: '#f3f4f6' },
{ label: 'Brown', value: '#fef3c7' },
{ label: 'Orange', value: '#ffedd5' },
{ label: 'Yellow', value: '#fef9c3' },
{ label: 'Green', value: '#d1fae5' },
{ label: 'Blue', value: '#dbeafe' },
{ label: 'Purple', value: '#e9d5ff' },
{ label: 'Pink', value: '#fce7f3' },
{ label: 'Red', value: '#fecaca' },
]
function getTextColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'currentColor',
isActive: !editor.marks.textColor.isActive(),
onClick: () => editor.commands.removeTextColor(),
}].concat(textColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.textColor.isActive({ color: color.value }),
onClick: () => editor.commands.addTextColor({ color: color.value }),
})))
}
function getBackgroundColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'canvas',
isActive: !editor.marks.backgroundColor.isActive(),
onClick: () => editor.commands.removeBackgroundColor(),
}].concat(
backgroundColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.backgroundColor.isActive({ color: color.value }),
onClick: () => editor.commands.addBackgroundColor({ color: color.value }),
})),
)
}
export default function InlineMenu() {
const textColorState = useEditorDerivedValue(getTextColorState)
const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)
const [open, setOpen] = useState(false)
const keymap: Keymap = useMemo(() => ({
Escape: () => {
if (open) {
setOpen(false)
return true
}
return false
},
}), [open])
useKeymap(keymap)
return (
<InlinePopover
className="z-10 box-border border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1"
open={open}
onOpenChange={setOpen}
>
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-2">
<div className="text-sm">Text color</div>
<div className="grid grid-cols-5 gap-1">
{textColorState.map((color) => (
<Button
key={color.label}
pressed={color.isActive}
tooltip={`Text: ${color.label}`}
onClick={color.onClick}
>
<span
className="text-base font-medium"
style={{ color: color.value }}
>
A
</span>
</Button>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm">Background color</div>
<div className="grid grid-cols-5 gap-1">
{backgroundColorState.map((color) => (
<Button
key={color.label}
pressed={color.isActive}
tooltip={`Background: ${color.label}`}
onClick={color.onClick}
>
<div
className="w-6 h-6 rounded border border-gray-200 dark:border-gray-700"
style={{ backgroundColor: color.value }}
/>
</Button>
))}
</div>
</div>
</div>
</InlinePopover>
)
}import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#ef4444',
},
},
],
text: 'Select',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#f97316',
},
},
],
text: 'some',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#eab308',
},
},
],
text: 'text',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#22c55e',
},
},
],
text: 'to',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#3b82f6',
},
},
],
text: 'change',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#6366f1',
},
},
],
text: 'the',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#a855f7',
},
},
],
text: 'color',
},
],
},
],
}import type {
ComponentChild,
MouseEventHandler,
} from 'preact'
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/preact/tooltip'
export default function Button(props: {
pressed?: boolean
disabled?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
tooltip?: string
children: ComponentChild
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={props.pressed ? 'on' : 'off'}
disabled={props.disabled}
onClick={props.onClick}
onMouseDown={(event) => {
// Prevent the editor from being blurred when the button is clicked
event.preventDefault()
}}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{props.children}
{props.tooltip ? <span className="sr-only">{props.tooltip}</span> : null}
</button>
</TooltipTrigger>
{props.tooltip
? (
<TooltipContent className="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs [&: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 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}export { default as Button } from './button'import '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-text-color'
import { defineExtension } from './extension'
import InlineMenu from './inline-menu'
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>
<InlineMenu />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineBackgroundColor } from 'prosekit/extensions/background-color'
import { defineTextColor } from 'prosekit/extensions/text-color'
export function defineExtension() {
return union(defineBasicExtension(), defineTextColor(), defineBackgroundColor())
}
export type EditorExtension = ReturnType<typeof defineExtension>'use client'
export { default as ExampleEditor } from './editor'import type {
Editor,
Keymap,
} from 'prosekit/core'
import {
useEditorDerivedValue,
useKeymap,
} from 'prosekit/react'
import { InlinePopover } from 'prosekit/react/inline-popover'
import {
useMemo,
useState,
} from 'react'
import { Button } from '../../ui/button'
import type { EditorExtension } from './extension'
const textColors = [
{ label: 'Gray', value: '#9ca3af' },
{ label: 'Brown', value: '#92400e' },
{ label: 'Orange', value: '#ea580c' },
{ label: 'Yellow', value: '#ca8a04' },
{ label: 'Green', value: '#16a34a' },
{ label: 'Blue', value: '#2563eb' },
{ label: 'Purple', value: '#9333ea' },
{ label: 'Magenta', value: '#c026d3' },
{ label: 'Red', value: '#dc2626' },
]
const backgroundColors = [
{ label: 'Gray', value: '#f3f4f6' },
{ label: 'Brown', value: '#fef3c7' },
{ label: 'Orange', value: '#ffedd5' },
{ label: 'Yellow', value: '#fef9c3' },
{ label: 'Green', value: '#d1fae5' },
{ label: 'Blue', value: '#dbeafe' },
{ label: 'Purple', value: '#e9d5ff' },
{ label: 'Pink', value: '#fce7f3' },
{ label: 'Red', value: '#fecaca' },
]
function getTextColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'currentColor',
isActive: !editor.marks.textColor.isActive(),
onClick: () => editor.commands.removeTextColor(),
}].concat(textColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.textColor.isActive({ color: color.value }),
onClick: () => editor.commands.addTextColor({ color: color.value }),
})))
}
function getBackgroundColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'canvas',
isActive: !editor.marks.backgroundColor.isActive(),
onClick: () => editor.commands.removeBackgroundColor(),
}].concat(
backgroundColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.backgroundColor.isActive({ color: color.value }),
onClick: () => editor.commands.addBackgroundColor({ color: color.value }),
})),
)
}
export default function InlineMenu() {
const textColorState = useEditorDerivedValue(getTextColorState)
const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)
const [open, setOpen] = useState(false)
const keymap: Keymap = useMemo(() => ({
Escape: () => {
if (open) {
setOpen(false)
return true
}
return false
},
}), [open])
useKeymap(keymap)
return (
<InlinePopover
className="z-10 box-border border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1"
open={open}
onOpenChange={setOpen}
>
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-2">
<div className="text-sm">Text color</div>
<div className="grid grid-cols-5 gap-1">
{textColorState.map((color) => (
<Button
key={color.label}
pressed={color.isActive}
tooltip={`Text: ${color.label}`}
onClick={color.onClick}
>
<span
className="text-base font-medium"
style={{ color: color.value }}
>
A
</span>
</Button>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm">Background color</div>
<div className="grid grid-cols-5 gap-1">
{backgroundColorState.map((color) => (
<Button
key={color.label}
pressed={color.isActive}
tooltip={`Background: ${color.label}`}
onClick={color.onClick}
>
<div
className="w-6 h-6 rounded border border-gray-200 dark:border-gray-700"
style={{ backgroundColor: color.value }}
/>
</Button>
))}
</div>
</div>
</div>
</InlinePopover>
)
}import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#ef4444',
},
},
],
text: 'Select',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#f97316',
},
},
],
text: 'some',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#eab308',
},
},
],
text: 'text',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#22c55e',
},
},
],
text: 'to',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#3b82f6',
},
},
],
text: 'change',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#6366f1',
},
},
],
text: 'the',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#a855f7',
},
},
],
text: 'color',
},
],
},
],
}import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/react/tooltip'
import type {
MouseEventHandler,
ReactNode,
} from 'react'
export default function Button(props: {
pressed?: boolean
disabled?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
tooltip?: string
children: ReactNode
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={props.pressed ? 'on' : 'off'}
disabled={props.disabled}
onClick={props.onClick}
onMouseDown={(event) => {
// Prevent the editor from being blurred when the button is clicked
event.preventDefault()
}}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{props.children}
{props.tooltip ? <span className="sr-only">{props.tooltip}</span> : null}
</button>
</TooltipTrigger>
{props.tooltip
? (
<TooltipContent className="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs [&: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 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}export { default as Button } from './button'<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-text-color'
import { defineExtension } from './extension'
import InlineMenu from './inline-menu.svelte'
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>
<InlineMenu />
</div>
</div>
</ProseKit>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineBackgroundColor } from 'prosekit/extensions/background-color'
import { defineTextColor } from 'prosekit/extensions/text-color'
export function defineExtension() {
return union(defineBasicExtension(), defineTextColor(), defineBackgroundColor())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.svelte'<script lang="ts">
import type { Editor } from 'prosekit/core'
import {
useEditorDerivedValue,
useKeymap,
} from 'prosekit/svelte'
import { InlinePopover } from 'prosekit/svelte/inline-popover'
import {
derived,
writable,
} from 'svelte/store'
import { Button } from '../../ui/button'
import type { EditorExtension } from './extension'
const textColors = [
{ label: 'Gray', value: '#9ca3af' },
{ label: 'Brown', value: '#92400e' },
{ label: 'Orange', value: '#ea580c' },
{ label: 'Yellow', value: '#ca8a04' },
{ label: 'Green', value: '#16a34a' },
{ label: 'Blue', value: '#2563eb' },
{ label: 'Purple', value: '#9333ea' },
{ label: 'Magenta', value: '#c026d3' },
{ label: 'Red', value: '#dc2626' },
]
const backgroundColors = [
{ label: 'Gray', value: '#f3f4f6' },
{ label: 'Brown', value: '#fef3c7' },
{ label: 'Orange', value: '#ffedd5' },
{ label: 'Yellow', value: '#fef9c3' },
{ label: 'Green', value: '#d1fae5' },
{ label: 'Blue', value: '#dbeafe' },
{ label: 'Purple', value: '#e9d5ff' },
{ label: 'Pink', value: '#fce7f3' },
{ label: 'Red', value: '#fecaca' },
]
function getTextColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'currentColor',
isActive: !editor.marks.textColor.isActive(),
onClick: () => editor.commands.removeTextColor(),
}].concat(textColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.textColor.isActive({ color: color.value }),
onClick: () => editor.commands.addTextColor({ color: color.value }),
})))
}
function getBackgroundColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'canvas',
isActive: !editor.marks.backgroundColor.isActive(),
onClick: () => editor.commands.removeBackgroundColor(),
}].concat(
backgroundColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.backgroundColor.isActive({ color: color.value }),
onClick: () => editor.commands.addBackgroundColor({ color: color.value }),
})),
)
}
const textColorState = useEditorDerivedValue(getTextColorState)
const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)
let open = $state(false)
// Create a store from the reactive open value
const openStore = writable(false)
// Update store when open changes
$effect(() => {
openStore.set(open)
})
// Create keymap derived from the open store
const keymap = derived(openStore, ($open) => ({
Escape: () => {
if ($open) {
open = false
return true
}
return false
},
}))
useKeymap(keymap)
</script>
<InlinePopover
class="z-10 box-border border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1"
{open}
onOpenChange={(value) => open = value}
>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-2">
<div class="text-sm">Text color</div>
<div class="grid grid-cols-5 gap-1">
{#each $textColorState as color (color.label)}
<Button
pressed={color.isActive}
tooltip={`Text: ${color.label}`}
onClick={color.onClick}
>
<span class="text-base font-medium" style:color={color.value}>
A
</span>
</Button>
{/each}
</div>
</div>
<div class="flex flex-col gap-2">
<div class="text-sm">Background color</div>
<div class="grid grid-cols-5 gap-1">
{#each $backgroundColorState as color (color.label)}
<Button
pressed={color.isActive}
tooltip={`Background: ${color.label}`}
onClick={color.onClick}
>
<div
class="w-6 h-6 rounded border border-gray-200 dark:border-gray-700"
style:background-color={color.value}
>
</div>
</Button>
{/each}
</div>
</div>
</div>
</InlinePopover>import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#ef4444',
},
},
],
text: 'Select',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#f97316',
},
},
],
text: 'some',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#eab308',
},
},
],
text: 'text',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#22c55e',
},
},
],
text: 'to',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#3b82f6',
},
},
],
text: 'change',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#6366f1',
},
},
],
text: 'the',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#a855f7',
},
},
],
text: 'color',
},
],
},
],
}<script lang="ts">
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/svelte/tooltip'
interface Props {
pressed?: boolean
disabled?: boolean
onClick?: () => void
tooltip?: string
children?: import('svelte').Snippet
}
const props: Props = $props()
const pressed = $derived(props.pressed ?? false)
const disabled = $derived(props.disabled ?? false)
</script>
<TooltipRoot>
<TooltipTrigger class="block">
<button
data-state={pressed ? 'on' : 'off'}
{disabled}
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
onclick={props.onClick}
onmousedown={(e) => e.preventDefault()}
>
{@render props.children?.()}
{#if props.tooltip}
<span class="sr-only">{props.tooltip}</span>
{/if}
</button>
</TooltipTrigger>
{#if props.tooltip}
<TooltipContent class="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs [&: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 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
{/if}
</TooltipRoot>export { default as Button } from './button.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-text-color'
import { defineExtension } from './extension'
import InlineMenu from './inline-menu.vue'
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" />
<InlineMenu />
</div>
</div>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineBackgroundColor } from 'prosekit/extensions/background-color'
import { defineTextColor } from 'prosekit/extensions/text-color'
export function defineExtension() {
return union(defineBasicExtension(), defineTextColor(), defineBackgroundColor())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'<script setup lang="ts">
import type { Editor } from 'prosekit/core'
import {
useEditorDerivedValue,
useKeymap,
} from 'prosekit/vue'
import { InlinePopover } from 'prosekit/vue/inline-popover'
import { ref } from 'vue'
import { Button } from '../../ui/button'
import type { EditorExtension } from './extension'
const textColors = [
{ label: 'Gray', value: '#9ca3af' },
{ label: 'Brown', value: '#92400e' },
{ label: 'Orange', value: '#ea580c' },
{ label: 'Yellow', value: '#ca8a04' },
{ label: 'Green', value: '#16a34a' },
{ label: 'Blue', value: '#2563eb' },
{ label: 'Purple', value: '#9333ea' },
{ label: 'Magenta', value: '#c026d3' },
{ label: 'Red', value: '#dc2626' },
]
const backgroundColors = [
{ label: 'Gray', value: '#f3f4f6' },
{ label: 'Brown', value: '#fef3c7' },
{ label: 'Orange', value: '#ffedd5' },
{ label: 'Yellow', value: '#fef9c3' },
{ label: 'Green', value: '#d1fae5' },
{ label: 'Blue', value: '#dbeafe' },
{ label: 'Purple', value: '#e9d5ff' },
{ label: 'Pink', value: '#fce7f3' },
{ label: 'Red', value: '#fecaca' },
]
function getTextColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'currentColor',
isActive: !editor.marks.textColor.isActive(),
onClick: () => editor.commands.removeTextColor(),
}].concat(textColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.textColor.isActive({ color: color.value }),
onClick: () => editor.commands.addTextColor({ color: color.value }),
})))
}
function getBackgroundColorState(editor: Editor<EditorExtension>) {
return [{
label: 'Default',
value: 'canvas',
isActive: !editor.marks.backgroundColor.isActive(),
onClick: () => editor.commands.removeBackgroundColor(),
}].concat(
backgroundColors.map((color) => ({
label: color.label,
value: color.value,
isActive: editor.marks.backgroundColor.isActive({ color: color.value }),
onClick: () => editor.commands.addBackgroundColor({ color: color.value }),
})),
)
}
const textColorState = useEditorDerivedValue(getTextColorState)
const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)
const open = ref(false)
useKeymap({
Escape: () => {
if (open.value) {
open.value = false
return true
}
return false
},
})
</script>
<template>
<InlinePopover
class="z-10 box-border border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1"
:open="open"
@open-change="(value) => open = value"
>
<div class="flex flex-col gap-4 p-4">
<div class="flex flex-col gap-2">
<div class="text-sm">
Text color
</div>
<div class="grid grid-cols-5 gap-1">
<Button
v-for="color in textColorState"
:key="color.label"
:pressed="color.isActive"
:tooltip="`Text: ${color.label}`"
@click="color.onClick"
>
<span
class="text-base font-medium"
:style="{ color: color.value }"
>
A
</span>
</Button>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="text-sm">
Background color
</div>
<div class="grid grid-cols-5 gap-1">
<Button
v-for="color in backgroundColorState"
:key="color.label"
:pressed="color.isActive"
:tooltip="`Background: ${color.label}`"
@click="color.onClick"
>
<div
class="w-6 h-6 rounded border border-gray-200 dark:border-gray-700"
:style="{ backgroundColor: color.value }"
/>
</Button>
</div>
</div>
</div>
</InlinePopover>
</template>import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#ef4444',
},
},
],
text: 'Select',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#f97316',
},
},
],
text: 'some',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#eab308',
},
},
],
text: 'text',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#22c55e',
},
},
],
text: 'to',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#3b82f6',
},
},
],
text: 'change',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#6366f1',
},
},
],
text: 'the',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
marks: [
{
type: 'textColor',
attrs: {
color: '#a855f7',
},
},
],
text: 'color',
},
],
},
],
}<script setup lang="ts">
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/vue/tooltip'
const props = defineProps<{
pressed?: boolean
disabled?: boolean
onClick?: () => void
tooltip?: string
}>()
</script>
<template>
<TooltipRoot>
<TooltipTrigger class="block">
<button
:data-state="props.pressed ? 'on' : 'off'"
:disabled="props.disabled"
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
@click="props.onClick"
@mousedown.prevent
>
<slot />
<span v-if="props.tooltip" class="sr-only">{{ props.tooltip }}</span>
</button>
</TooltipTrigger>
<TooltipContent v-if="props.tooltip" class="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs [&: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 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{{ props.tooltip }}
</TooltipContent>
</TooltipRoot>
</template>export { default as Button } from './button.vue'Commands
Section titled “Commands”addTextColor
Section titled “addTextColor”Add a color to the current selection. This command will override any existing color on the selected text.
editor const editor: Editor<TextColorExtension> .commands Editor<TextColorExtension>.commands: ToCommandAction<{
addTextColor: [attrs: TextColorAttrs];
removeTextColor: [];
}>
All
{@link
CommandAction
}
s defined by the editor. .addTextColor addTextColor: CommandAction
(attrs: TextColorAttrs) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ color TextColorAttrs.color: string : 'red' })
editor const editor: Editor<TextColorExtension> .commands Editor<TextColorExtension>.commands: ToCommandAction<{
addTextColor: [attrs: TextColorAttrs];
removeTextColor: [];
}>
All
{@link
CommandAction
}
s defined by the editor. .addTextColor addTextColor: CommandAction
(attrs: TextColorAttrs) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ color TextColorAttrs.color: string : '#0000ff' })
editor const editor: Editor<TextColorExtension> .commands Editor<TextColorExtension>.commands: ToCommandAction<{
addTextColor: [attrs: TextColorAttrs];
removeTextColor: [];
}>
All
{@link
CommandAction
}
s defined by the editor. .addTextColor addTextColor: CommandAction
(attrs: TextColorAttrs) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ color TextColorAttrs.color: string : 'rgb(255, 0, 0)' })
editor const editor: Editor<TextColorExtension> .commands Editor<TextColorExtension>.commands: ToCommandAction<{
addTextColor: [attrs: TextColorAttrs];
removeTextColor: [];
}>
All
{@link
CommandAction
}
s defined by the editor. .addTextColor addTextColor: CommandAction
(attrs: TextColorAttrs) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ color TextColorAttrs.color: string : 'var(--color-primary)' })removeTextColor
Section titled “removeTextColor”Remove the color from the current selection.
editor const editor: Editor<TextColorExtension> .commands Editor<TextColorExtension>.commands: ToCommandAction<{
addTextColor: [attrs: TextColorAttrs];
removeTextColor: [];
}>
All
{@link
CommandAction
}
s defined by the editor. .removeTextColor removeTextColor: CommandAction
() => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()