O componente Editor oferece uma poderosa experiência de edição de texto rico construída sobre o TipTap. Ele suporta múltiplos formatos de conteúdo (JSON, HTML, Markdown), barras de ferramentas personalizáveis, reordenação de blocos por arrastar e soltar, comandos de barra, menções, seletor de emoji e uma arquitetura extensível para adicionar funcionalidades personalizadas.
Adding different instances of a keyed plugin, ao usar o componente Editor ou suas extensões, talvez seja necessário adicionar os pacotes do prosemirror à lista vite.optimizeDeps.include no seu arquivo nuxt.config.ts. Isso garante que o Vite pré-empacote essas dependências para evitar o carregamento de múltiplas instâncias.export default defineNuxtConfig({
vite: {
optimizeDeps: {
include: [
'@nitro/ui > prosemirror-state',
'@nitro/ui > prosemirror-transform',
'@nitro/ui > prosemirror-model',
'@nitro/ui > prosemirror-view',
'@nitro/ui > prosemirror-gapcursor'
]
}
}
})
Use a diretiva v-model para controlar o valor do Editor.
<script setup lang="ts">
const value = ref({
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is a '
},
{
type: 'text',
marks: [
{
type: 'bold'
}
],
text: 'rich text'
},
{
type: 'text',
text: ' editor.'
}
]
}
]
})
</script>
<template>
<NEditor v-model="value" class="w-full min-h-21" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref({
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is a '
},
{
type: 'text',
marks: [
{
type: 'bold'
}
],
text: 'rich text'
},
{
type: 'text',
text: ' editor.'
}
]
}
]
})
</script>
<template>
<NEditor v-model="value" class="w-full min-h-21" />
</template>
O Editor detecta automaticamente o formato do conteúdo com base no tipo do v-model: strings são tratadas como html e objetos como json.
Você pode definir explicitamente o formato usando a prop content-type: json, html ou markdown.
<script setup lang="ts">
const value = ref('<h1>Hello World</h1>\n<p>This is a <strong>rich text</strong> editor.</p>\n')
</script>
<template>
<NEditor v-model="value" content-type="html" class="w-full min-h-21" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref('<h1>Hello World</h1>\n<p>This is a <strong>rich text</strong> editor.</p>\n')
</script>
<template>
<NEditor v-model="value" content-type="html" class="w-full min-h-21" />
</template>
O Editor inclui as seguintes extensões por padrão:
starter-kit, placeholder, image, mention, markdown) para personalizar seu comportamento com as opções do TipTap.Você pode usar a prop extensions para adicionar extensões adicionais do TipTap e ampliar os recursos do Editor:
<script setup lang="ts">
import { Emoji } from '@tiptap/extension-emoji'
import { TextAlign } from '@tiptap/extension-text-align'
const value = ref('<h1>Hello World</h1>\n')
</script>
<template>
<NEditor
v-model="value"
:extensions="[
Emoji,
TextAlign.configure({
types: ['heading', 'paragraph']
})
]"
/>
</template>
Use a prop placeholder para definir um texto de placeholder que aparece em parágrafos vazios.
<script setup lang="ts">
const value = ref('')
</script>
<template>
<NEditor v-model="value" placeholder="Start writing..." class="w-full min-h-7" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
</script>
<template>
<NEditor v-model="value" placeholder="Start writing..." class="w-full min-h-7" />
</template>
placeholder aceita uma string ou um objeto com PlaceholderOptions e uma propriedade adicional mode:everyLine: Display placeholder on every empty line when focused (default).firstLine: Display placeholder only on the first line when the editor is empty.<template>
<NEditor :placeholder="{ placeholder: 'Start writing...', mode: 'firstLine' }" />
</template>
includeChildren como true:<template>
<NEditor :placeholder="{ placeholder: 'Start writing...', includeChildren: true }" />
</template>
Use a prop starter-kit para configurar a extensão StarterKit embutida do TipTap, que inclui recursos comuns do editor como negrito, itálico, títulos, listas, citações, blocos de código e mais.
<script setup lang="ts">
const value = ref('<h1>Hello World</h1>\n')
</script>
<template>
<NEditor
v-model="value"
:starter-kit="{
blockquote: false,
headings: {
levels: [1, 2, 3, 4]
},
dropcursor: {
color: 'var(--ui-primary)',
width: 2
},
link: {
openOnClick: false
}
}"
/>
</template>
starter-kit como false para um editor de texto simples. Ele mantém os nodes essenciais (parágrafo, texto, histórico) e desabilita todos os recursos de formatação, como negrito, itálico, títulos, listas, código, citação, links e réguas horizontais.Os handlers envolvem os comandos embutidos do TipTap para oferecer uma interface unificada para as ações do editor. Quando você adiciona uma propriedade kind a um item de EditorToolbar ou EditorSuggestionMenu, o handler correspondente executa o comando do TipTap e gerencia seu estado (ativo, desabilitado, etc.).
O componente Editor fornece estes handlers padrão, que você pode referenciar em itens da barra de ferramentas ou do menu de sugestões usando a propriedade kind:
| Handler | Description | Usage |
|---|---|---|
mark | Toggle text marks (bold, italic, strike, code, underline) | Requires mark property in item |
textAlign | Set text alignment (left, center, right, justify) | Requires align property in item |
heading | Toggle heading levels (1-6) | Requires level property in item |
link | Add, edit, or remove links | Prompts for URL if not provided |
image | Insert images | Prompts for URL if not provided |
blockquote | Toggle blockquotes | |
bulletList | Toggle bullet lists | Handles list conversions |
orderedList | Toggle ordered lists | Handles list conversions |
taskList | Toggle task lists | Handles list conversions |
codeBlock | Toggle code blocks | |
horizontalRule | Insert horizontal rules | |
paragraph | Set paragraph format | |
undo | Undo last change | |
redo | Redo last undone change | |
clearFormatting | Remove all formatting | Works with selection or position |
duplicate | Duplicate a node | Requires pos property in item |
delete | Delete a node | Requires pos property in item |
moveUp | Move a node up | Requires pos property in item |
moveDown | Move a node down | Requires pos property in item |
suggestion | Trigger suggestion menu | Inserts / character |
mention | Trigger mention menu | Inserts @ character |
emoji | Trigger emoji picker | Inserts : character |
taskList e textAlign só funcionam quando suas respectivas extensões estão instaladas, já que não são incluídos no Editor por padrão.Veja como usar os handlers padrão em itens da barra de ferramentas ou do menu de sugestões:
<script setup lang="ts">
import type { EditorToolbarItem } from '@nitro/ui'
const value = ref('<h1>Hello World</h1>\n')
const items: EditorToolbarItem[] = [
{ kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
{ kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
{ kind: 'heading', level: 1, icon: 'i-lucide-heading-1' },
{ kind: 'heading', level: 2, icon: 'i-lucide-heading-2' },
{ kind: 'textAlign', align: 'left', icon: 'i-lucide-align-left' },
{ kind: 'textAlign', align: 'center', icon: 'i-lucide-align-center' },
{ kind: 'bulletList', icon: 'i-lucide-list' },
{ kind: 'orderedList', icon: 'i-lucide-list-ordered' },
{ kind: 'blockquote', icon: 'i-lucide-quote' },
{ kind: 'link', icon: 'i-lucide-link' }
]
</script>
<template>
<NEditor v-slot="{ editor }" v-model="value">
<NEditorToolbar :editor="editor" :items="items" />
</NEditor>
</template>
Use a prop handlers para estender ou sobrescrever os handlers padrão. Os handlers personalizados são mesclados com os padrão, permitindo adicionar novas ações ou modificar o comportamento existente.
Cada handler implementa a interface EditorHandler:
interface EditorHandler {
/* Checks if the command can be executed in the current editor state */
canExecute: (editor: Editor, item?: any) => boolean
/* Executes the command and returns a Tiptap chain */
execute: (editor: Editor, item?: any) => any
/* Determines if the item should appear active (used for toggle states) */
isActive: (editor: Editor, item?: any) => boolean
/* Optional additional check to disable the item (combined with `canExecute`) */
isDisabled?: (editor: Editor, item?: any) => boolean
}
Aqui está um exemplo de criação de handlers personalizados:
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'
import type { EditorCustomHandlers, EditorToolbarItem } from '@nitro/ui'
const value = ref('<h1>Hello World</h1>\n')
const customHandlers = {
highlight: {
canExecute: (editor: Editor) => editor.can().toggleHighlight(),
execute: (editor: Editor) => editor.chain().focus().toggleHighlight(),
isActive: (editor: Editor) => editor.isActive('highlight'),
isDisabled: (editor: Editor) => !editor.isEditable
}
} satisfies EditorCustomHandlers
const items = [
// Built-in handler
{ kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
// Custom handler
{ kind: 'highlight', icon: 'i-lucide-highlighter' }
] satisfies EditorToolbarItem<typeof customHandlers>[]
</script>
<template>
<NEditor v-slot="{ editor }" v-model="value" :handlers="customHandlers">
<NEditorToolbar :editor="editor" :items="items" />
</NEditor>
</template>
Você pode usar o componente EditorToolbar para adicionar uma barra de ferramentas fixed, bubble ou floating ao Editor com ações de formatação comuns.
Você pode usar o componente EditorDragHandle para adicionar uma alça arrastável para reordenar blocos.
Você pode usar o componente EditorSuggestionMenu para adicionar comandos de barra para formatação e inserções rápidas.
Você pode usar o componente EditorMentionMenu para adicionar menções com @ para marcar usuários ou entidades.
Você pode usar o componente EditorEmojiMenu para adicionar suporte ao seletor de emojis.
Este exemplo demonstra como criar um recurso de upload de imagem usando a prop extensions para registrar um node personalizado do TipTap e a prop handlers para definir como o botão da barra de ferramentas dispara o fluxo de upload.
Este exemplo demonstra como adicionar recursos com IA ao Editor usando o Vercel AI SDK, especificamente o composable useCompletion para completar texto em streaming, combinado com o Vercel AI Gateway para acessar modelos de IA por meio de um endpoint centralizado. Ele inclui autocompletar com texto fantasma e ações de transformação de texto (corrigir gramática, estender, reduzir, simplificar, traduzir, etc.).
pnpm add ai @ai-sdk/gateway @ai-sdk/vue
yarn add ai @ai-sdk/gateway @ai-sdk/vue
npm install ai @ai-sdk/gateway @ai-sdk/vue
bun add ai @ai-sdk/gateway @ai-sdk/vue
streamText:import { streamText, createTextStreamResponse } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { prompt, mode, language } = await readBody(event)
if (!prompt) {
throw createError({ statusCode: 400, message: 'Prompt is required' })
}
let instructions: string
let maxOutputTokens: number
const preserveMarkdown = 'IMPORTANT: Preserve all markdown formatting (bold, italic, links, etc.) exactly as in the original.'
switch (mode) {
case 'fix':
instructions = `You are a writing assistant. Fix all spelling and grammar errors in the given text. ${preserveMarkdown} Only output the corrected text, nothing else.`
maxOutputTokens = 500
break
case 'extend':
instructions = `You are a writing assistant. Extend the given text with more details, examples, and explanations while maintaining the same style. ${preserveMarkdown} Only output the extended text, nothing else.`
maxOutputTokens = 500
break
case 'reduce':
instructions = `You are a writing assistant. Make the given text more concise by removing unnecessary words while keeping the meaning. ${preserveMarkdown} Only output the reduced text, nothing else.`
maxOutputTokens = 300
break
case 'simplify':
instructions = `You are a writing assistant. Simplify the given text to make it easier to understand, using simpler words and shorter sentences. ${preserveMarkdown} Only output the simplified text, nothing else.`
maxOutputTokens = 400
break
case 'summarize':
instructions = 'You are a writing assistant. Summarize the given text concisely while keeping the key points. Only output the summary, nothing else.'
maxOutputTokens = 200
break
case 'translate':
instructions = `You are a writing assistant. Translate the given text to ${language || 'English'}. ${preserveMarkdown} Only output the translated text, nothing else.`
maxOutputTokens = 500
break
case 'continue':
default:
instructions = `You are a writing assistant providing inline autocompletions.
CRITICAL RULES:
- Output ONLY the NEW text that comes AFTER the user's input
- NEVER repeat any words from the end of the user's text
- Keep completions short (1 sentence max)
- Match the tone and style of the existing text
- ${preserveMarkdown}`
maxOutputTokens = 25
break
}
const result = streamText({
model: gateway('anthropic/claude-haiku-4.5'),
instructions,
prompt,
maxOutputTokens
})
return createTextStreamResponse({ stream: result.textStream })
})
autoTrigger: true para sugerir conclusões automaticamente enquanto você digita (desabilitado por padrão). Você também pode dispará-la manualmente com j.| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
modelValue | null | string | JSONContent | JSONContent[] | |
contentType | "markdown" | "json" | "html"The content type the content is provided as. When not specified, it's automatically inferred: strings are treated as 'html', objects as 'json'. | |
starterKit | true | boolean | Partial<StarterKitOptions> The starter kit options to configure the editor.
Set to |
placeholder | { showOnlyWhenEditable: false, showOnlyCurrent: true, mode: 'everyLine' } | string | Partial<PlaceholderOptions> & { mode?: "firstLine" | "everyLine" | undefined; }The placeholder text to show in empty paragraphs. Can be a string or PlaceholderOptions from |
markdown | { markedOptions: { gfm: true } } | Partial<MarkdownExtensionOptions>The markdown extension options to configure markdown parsing and serialization. |
image | true | boolean | Partial<ImageOptions> The image extension options to configure image handling. Set to |
mention | true | boolean | Partial<Omit<MentionOptions<any, MentionNodeAttrs>, "suggestion" | "suggestions">> The mention extension options to configure mention handling. Set to |
handlers | HCustom item handlers to override or extend the default handlers. These handlers are provided to all child components (toolbar, suggestion menu, etc.). | |
extensions | ExtensionsThe extensions to use | |
injectCSS | boolean Whether to inject base CSS styles | |
injectNonce | stringA nonce to use for CSP while injecting styles | |
autofocus | null | number | false | true | "start" | "end" | "all"The editor's initial focus position | |
editable | boolean Whether the editor is editable | |
textDirection | "ltr" | "rtl" | "auto"The default text direction for all content in the editor. When set to 'ltr' or 'rtl', all nodes will have the corresponding dir attribute. When set to 'auto', the dir attribute will be set based on content detection. When undefined, no dir attribute will be added. | |
editorProps | EditorProps<any>The editor's props | |
parseOptions | ParseOptions | |
coreExtensionOptions | { clipboardTextSerializer?: { blockSeparator?: string | undefined; } | undefined; tabindex?: { value?: string | undefined; } | undefined; delete?: { async?: boolean | undefined; filterTransaction?: ((transaction: Transaction) => boolean) | undefined; } | undefined; }The editor's core extension options | |
enableInputRules | false | true | (string | AnyExtension)[]Whether to enable input rules behavior | |
enablePasteRules | false | true | (string | AnyExtension)[]Whether to enable paste rules behavior | |
enableCoreExtensions | boolean | Partial<Record<"editable" | "textDirection" | "clipboardTextSerializer" | "commands" | "focusEvents" | "keymap" | "tabindex" | "drop" | "paste" | "delete", false>> Determines whether core extensions are enabled. If set to | |
enableContentCheck | boolean If | |
emitContentError | boolean If | |
onBeforeCreate | (props: { editor: Editor; }): voidCalled before the editor is constructed. | |
onCreate | (props: { editor: Editor; }): voidCalled after the editor is constructed. | |
onMount | (props: { editor: Editor; }): voidCalled when the editor is mounted. | |
onUnmount | (props: { editor: Editor; }): voidCalled when the editor is unmounted. | |
onContentError | (props: { editor: Editor; error: Error; disableCollaboration: () => void; }): voidCalled when the editor encounters an error while parsing the content.
Only enabled if | |
onUpdate | (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): voidCalled when the editor's content is updated. | |
onSelectionUpdate | (props: { editor: Editor; transaction: Transaction; }): voidCalled when the editor's selection is updated. | |
onTransaction | (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): voidCalled after a transaction is applied to the editor. | |
onFocus | (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): voidCalled on focus events. | |
onBlur | (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): voidCalled on blur events. | |
onDestroy | (props: void): voidCalled when the editor is destroyed. | |
onPaste | (e: ClipboardEvent, slice: Slice): voidCalled when content is pasted into the editor. | |
onDrop | (e: DragEvent, slice: Slice, moved: boolean): voidCalled when content is dropped into the editor. | |
onDelete | (props: { editor: Editor; deletedRange: Range; newRange: Range; transaction: Transaction; combinedTransform: Transform; partial: boolean; from: number; to: number; } & ({ ...; } | { ...; })): voidCalled when content is deleted from the editor. | |
enableExtensionDispatchTransaction | boolean Whether to enable extension-level dispatching of transactions.
If | |
ui | { root?: SlotClass; content?: SlotClass; base?: SlotClass; } |
| Slot | Type |
|---|---|
default | { editor: Editor; handlers: EditorHandlers<H>; } |
| Event | Type |
|---|---|
update:modelValue | [value: T] |
Ao acessar o componente por meio de um template ref, você pode usar o seguinte:
| Name | Type |
|---|---|
editor | Ref<Editor | undefined> |
export default defineAppConfig({
ui: {
editor: {
slots: {
root: '',
content: 'relative size-full flex-1',
base: [
'w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-primary/20',
'[&_:is(p,h1,h2,h3,h4).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4).is-empty]:before:text-dimmed [&_:is(p,h1,h2,h3,h4).is-empty]:before:float-left [&_:is(p,h1,h2,h3,h4).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4).is-empty]:before:pointer-events-none',
'[&_li_.is-empty]:before:content-none',
'[&_p]:leading-7',
'[&_a]:text-primary [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-primary [&_a]:font-medium',
'[&_a]:transition-colors',
'[&_.mention]:text-primary [&_.mention]:font-medium',
'[&_:is(h1,h2,h3,h4)]:text-highlighted [&_:is(h1,h2,h3,h4)]:font-bold',
'[&_h1]:text-3xl',
'[&_h2]:text-2xl',
'[&_h3]:text-xl',
'[&_h4]:text-lg',
'[&_:is(h1,h2,h3,h4)>code]:border-dashed [&_:is(h1,h2,h3,h4)>code]:font-bold',
'[&_h2>code]:text-xl/6',
'[&_h3>code]:text-lg/5',
'[&_blockquote]:border-s-4 [&_blockquote]:border-accented [&_blockquote]:ps-4 [&_blockquote]:italic',
'[&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2',
'[&_hr]:border-t [&_hr]:border-default',
'[&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-muted [&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-x-auto',
'[&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent',
'[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono [&_code]:font-medium [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-muted [&_code]:text-highlighted [&_code]:bg-muted',
'[&_:is(ul,ol)]:ps-6',
'[&_ul]:list-disc [&_ul]:marker:text-(--ui-border-accented)',
'[&_ol]:list-decimal [&_ol]:marker:text-muted',
'[&_li]:my-1.5 [&_li]:ps-1.5',
'[&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-primary',
'[&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-primary/20'
]
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nitro/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
editor: {
slots: {
root: '',
content: 'relative size-full flex-1',
base: [
'w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-primary/20',
'[&_:is(p,h1,h2,h3,h4).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4).is-empty]:before:text-dimmed [&_:is(p,h1,h2,h3,h4).is-empty]:before:float-left [&_:is(p,h1,h2,h3,h4).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4).is-empty]:before:pointer-events-none',
'[&_li_.is-empty]:before:content-none',
'[&_p]:leading-7',
'[&_a]:text-primary [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-primary [&_a]:font-medium',
'[&_a]:transition-colors',
'[&_.mention]:text-primary [&_.mention]:font-medium',
'[&_:is(h1,h2,h3,h4)]:text-highlighted [&_:is(h1,h2,h3,h4)]:font-bold',
'[&_h1]:text-3xl',
'[&_h2]:text-2xl',
'[&_h3]:text-xl',
'[&_h4]:text-lg',
'[&_:is(h1,h2,h3,h4)>code]:border-dashed [&_:is(h1,h2,h3,h4)>code]:font-bold',
'[&_h2>code]:text-xl/6',
'[&_h3>code]:text-lg/5',
'[&_blockquote]:border-s-4 [&_blockquote]:border-accented [&_blockquote]:ps-4 [&_blockquote]:italic',
'[&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2',
'[&_hr]:border-t [&_hr]:border-default',
'[&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-muted [&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-x-auto',
'[&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent',
'[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono [&_code]:font-medium [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-muted [&_code]:text-highlighted [&_code]:bg-muted',
'[&_:is(ul,ol)]:ps-6',
'[&_ul]:list-disc [&_ul]:marker:text-(--ui-border-accented)',
'[&_ol]:list-decimal [&_ol]:marker:text-muted',
'[&_li]:my-1.5 [&_li]:ps-1.5',
'[&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-primary',
'[&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-primary/20'
]
}
}
}
})
]
})