Editor

TipTapGitLab
Um componente de editor de texto rico baseado no TipTap com suporte aos tipos de conteúdo markdown, HTML e JSON.

Uso

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.

Este exemplo demonstra um componente Editor pronto para produção. Confira o código-fonte no GitHub.
Se você encontrar erros relacionados ao prosemirror, como 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.
nuxt.config.ts
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'
      ]
    }
  }
})

Conteúdo

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>

Tipo de conteúdo

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>

Extensões

O Editor inclui as seguintes extensões por padrão:

  • StarterKit - Core editing features (bold, italic, headings, lists, etc.)
  • Placeholder - Show placeholder text (when placeholder prop is provided)
  • Image - Insert and display images
  • Mention - Add @ mentions support
  • Markdown - Parse and serialize markdown (when content type is markdown)
Cada extensão embutida pode ser configurada usando sua prop correspondente (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>
Confira o exemplo de upload de imagem para criar extensões personalizadas do TipTap.

Placeholder

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>
A prop 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>
Por padrão, os placeholders só aparecem em nodes vazios de nível superior. Para exibir placeholders em elementos aninhados, como itens de lista, defina includeChildren como true:
<template>
  <NEditor :placeholder="{ placeholder: 'Start writing...', includeChildren: true }" />
</template>
Saiba mais sobre a extensão Placeholder na documentação do TipTap.

Kit inicial

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>
Defina 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.
Saiba mais sobre a extensão StarterKit na documentação do TipTap.

Manipuladores

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.).

Manipuladores padrão

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:

HandlerDescriptionUsage
markToggle text marks (bold, italic, strike, code, underline)Requires mark property in item
textAlignSet text alignment (left, center, right, justify)Requires align property in item
headingToggle heading levels (1-6)Requires level property in item
linkAdd, edit, or remove linksPrompts for URL if not provided
imageInsert imagesPrompts for URL if not provided
blockquoteToggle blockquotes
bulletListToggle bullet listsHandles list conversions
orderedListToggle ordered listsHandles list conversions
taskListToggle task listsHandles list conversions
codeBlockToggle code blocks
horizontalRuleInsert horizontal rules
paragraphSet paragraph format
undoUndo last change
redoRedo last undone change
clearFormattingRemove all formattingWorks with selection or position
duplicateDuplicate a nodeRequires pos property in item
deleteDelete a nodeRequires pos property in item
moveUpMove a node upRequires pos property in item
moveDownMove a node downRequires pos property in item
suggestionTrigger suggestion menuInserts / character
mentionTrigger mention menuInserts @ character
emojiTrigger emoji pickerInserts : character
Os handlers 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>

Manipuladores personalizados

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>
Confira o exemplo de upload de imagem para uma implementação completa com handlers personalizados.

Com barra de ferramentas

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.

Com alça de arrastar

Você pode usar o componente EditorDragHandle para adicionar uma alça arrastável para reordenar blocos.

Com menu de sugestões

Você pode usar o componente EditorSuggestionMenu para adicionar comandos de barra para formatação e inserções rápidas.

Com menu de menções

Você pode usar o componente EditorMentionMenu para adicionar menções com @ para marcar usuários ou entidades.

Com menu de emojis

Você pode usar o componente EditorEmojiMenu para adicionar suporte ao seletor de emojis.

Com upload de imagem

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.

  1. Crie um componente Vue que usa o componente FileUpload:
undefined.vue
  1. Crie uma extensão personalizada do TipTap para registrar o node:
undefined.ts
  1. Use a extensão personalizada no Editor:
Saiba mais sobre a criação de extensões personalizadas na documentação do TipTap.

Com completar por IA

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.).

Você precisa instalar estas dependências primeiro para usar este exemplo:
pnpm add ai @ai-sdk/gateway @ai-sdk/vue
  1. Crie uma extensão personalizada do TipTap que trata as sugestões de texto fantasma inline:
undefined.ts
  1. Crie um composable que gerencia o estado e os handlers da conclusão por IA:
useEditorCompletion.ts
  1. Crie um endpoint de API no servidor para tratar as requisições de conclusão usando streamText:
server/api/completion.post.ts
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 })
})
  1. Use o composable no Editor:
A extensão de conclusão pode ser configurada com autoTrigger: true para sugerir conclusões automaticamente enquanto você digita (desabilitado por padrão). Você também pode dispará-la manualmente com j.
Saiba mais sobre o Vercel AI SDK e os provedores disponíveis.

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

modelValuenull | 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'.

starterKittrueboolean | Partial<StarterKitOptions>

The starter kit options to configure the editor. Set to false for a plain-text editor: keeps the essential nodes (paragraph, text, history) and disables all rich-text formatting.

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 @tiptap/extension-placeholder.

markdown{ markedOptions: { gfm: true } } Partial<MarkdownExtensionOptions>

The markdown extension options to configure markdown parsing and serialization.

imagetrueboolean | Partial<ImageOptions>

The image extension options to configure image handling. Set to false to disable the extension.

mentiontrueboolean | Partial<Omit<MentionOptions<any, MentionNodeAttrs>, "suggestion" | "suggestions">>

The mention extension options to configure mention handling. Set to false to disable the extension. The suggestion and suggestions options are omitted as they are managed by the EditorMentionMenu component.

handlers H

Custom item handlers to override or extend the default handlers. These handlers are provided to all child components (toolbar, suggestion menu, etc.).

extensions Extensions

The extensions to use

injectCSSboolean

Whether to inject base CSS styles

injectNonce string

A nonce to use for CSP while injecting styles

autofocus null | number | false | true | "start" | "end" | "all"

The editor's initial focus position

editableboolean

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

parseOptionsParseOptions
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

enableCoreExtensionsboolean | Partial<Record<"editable" | "textDirection" | "clipboardTextSerializer" | "commands" | "focusEvents" | "keymap" | "tabindex" | "drop" | "paste" | "delete", false>>

Determines whether core extensions are enabled.

If set to false, all core extensions will be disabled. To disable specific core extensions, provide an object where the keys are the extension names and the values are false. Extensions not listed in the object will remain enabled.

enableContentCheckboolean

If true, the editor will check the content for errors on initialization. Emitting the contentError event if the content is invalid. Which can be used to show a warning or error message to the user.

emitContentErrorboolean

If true, the editor will emit the contentError event if invalid content is encountered but enableContentCheck is false. This lets you preserve the invalid editor content while still showing a warning or error message to the user.

onBeforeCreate (props: { editor: Editor; }): void

Called before the editor is constructed.

onCreate (props: { editor: Editor; }): void

Called after the editor is constructed.

onMount (props: { editor: Editor; }): void

Called when the editor is mounted.

onUnmount (props: { editor: Editor; }): void

Called when the editor is unmounted.

onContentError (props: { editor: Editor; error: Error; disableCollaboration: () => void; }): void

Called when the editor encounters an error while parsing the content. Only enabled if enableContentCheck is true.

onUpdate (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called when the editor's content is updated.

onSelectionUpdate (props: { editor: Editor; transaction: Transaction; }): void

Called when the editor's selection is updated.

onTransaction (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called after a transaction is applied to the editor.

onFocus (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on focus events.

onBlur (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on blur events.

onDestroy (props: void): void

Called when the editor is destroyed.

onPaste (e: ClipboardEvent, slice: Slice): void

Called when content is pasted into the editor.

onDrop (e: DragEvent, slice: Slice, moved: boolean): void

Called 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; } & ({ ...; } | { ...; })): void

Called when content is deleted from the editor.

enableExtensionDispatchTransactionboolean

Whether to enable extension-level dispatching of transactions. If false, extensions cannot define their own dispatchTransaction hook.

ui { root?: SlotClass; content?: SlotClass; base?: SlotClass; }

Slots

Slot Type
default{ editor: Editor; handlers: EditorHandlers<H>; }

Emits

Event Type
update:modelValue[value: T]

Expose

Ao acessar o componente por meio de um template ref, você pode usar o seguinte:

NameType
editorRef<Editor | undefined>
A instância do editor exposta é a API do Editor do TipTap. Consulte a documentação do TipTap para todos os métodos e propriedades disponíveis.

Tema

app.config.ts
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'
        ]
      }
    }
  }
})
vite.config.ts
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'
            ]
          }
        }
      }
    })
  ]
})

Changelog

No recent changes