# RichTextEditor

> Tiptap-based rich text editor with a Nasaq toolbar, per-paragraph auto direction, HTML or JSON output and read-only mode.

Source: https://docs.nasaqui.com/components/rich-text-editor

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/rich-text-editor.json
```

A WYSIWYG editor on Tiptap v3 (ProseMirror). The toolbar offers bold, italic, underline, strike, inline code,
headings 1-3, bulleted and numbered lists, quote, link, undo and redo, each with a tooltip that shows its keyboard
shortcut. Every block gets `dir="auto"`, so an Arabic paragraph and an English paragraph in the same document each
align from their own side.

## Bundle cost

Tiptap plus ProseMirror is roughly 150-200 KB minified (about 50-60 KB gzip) before Nasaq code. The component is
self-contained (no other Nasaq component imports it) and the packages are ES modules with `sideEffects` limited to
CSS, so an app that never renders it does not ship it. Load it lazily where it appears:

```tsx
import { lazy, Suspense } from "react";

const RichTextEditor = lazy(() => import("@nasaq/web").then((m) => ({ default: m.RichTextEditor })));

export function Body(props: { value: string; onChange: (v: string) => void }) {
  return (
    <Suspense fallback={<div className="h-40 rounded-control border border-border" />}>
      <RichTextEditor aria-label="Body" value={props.value} onValueChange={props.onChange} />
    </Suspense>
  );
}
```

## When to use

- Comments, descriptions, notes and articles that need formatting.
- Read-only rendering of content that was authored here (`readOnly`).

## When not to use

- Plain multi-line text: use `Textarea`. Mentions in plain text: [mention-textarea](https://docs.nasaqui.com/components/mention-textarea).
- Displaying Markdown: use [markdown](https://docs.nasaqui.com/components/markdown).
- Code editing: this is not a code editor.

## Import

```tsx
import { RichTextEditor } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { RichTextEditor } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Notes() {
  const [html, setHtml] = useState("<p>Hello</p>");
  return <RichTextEditor aria-label="Notes" value={html} onValueChange={setHtml} />;
}
```

## Anatomy

```
RichTextEditor                  data-slot="rich-text-editor"
  toolbar                       data-slot="rich-text-editor-toolbar" (role="toolbar")
    ToggleGroup (marks) / ToggleGroup (blocks) / link Popover / undo / redo
  content                       data-slot="rich-text-editor-content" (role="textbox", aria-multiline)
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `format` | `"html" \| "json"` | `"html"` | Type of `value`, `defaultValue` and `onValueChange`. |
| `value` | `string \| JSONContent` | | Controlled document. Changes from outside replace the content without echoing. |
| `defaultValue` | `string \| JSONContent` | `""` | Uncontrolled initial document. |
| `onValueChange` | `(value: string) => void` (`JSONContent` for `format="json"`) | | Fires on every edit. |
| `placeholder` | `string` | localised "Write something…" | Shown while empty. |
| `readOnly` | `boolean` | `false` | No toolbar, no editing, no border. |
| `toolbar` | `RichTextToolbarItem[]` | all | Items and their order (`bold italic underline strike code h1 h2 h3 bulletList orderedList blockquote link undo redo`). `[]` hides the toolbar. |
| `minHeight` | `string` | `"10rem"` | Minimum height of the writing area. |
| `aria-labelledby` | `string` | | `id` of the label naming the editor. Without it the editor is named "Rich text editor". |
| `onBlur` | `() => void` | | Focus left the editor. |
| `className` | `string` | | Classes for the outer frame. |

Install `@tiptap/pm` alongside (peer of `@tiptap/react`). Also exported: `AutoDirection` (the Tiptap extension that adds `dir`), `isSafeLink(url)` (http(s), mailto, tel,
relative and `#` links only) and the type `RichTextJson`.

## Examples

Field label and Arabic:

```tsx
import { Field, FieldLabel, RichTextEditor } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function ArabicBody() {
  const [html, setHtml] = useState("<p>مرحبًا</p>");
  return (
    <div lang="ar" dir="rtl">
      <Field>
        <FieldLabel id="body-label">المحتوى</FieldLabel>
        <RichTextEditor aria-labelledby="body-label" value={html} onValueChange={setHtml} />
      </Field>
    </div>
  );
}
```

JSON output:

```tsx
import { RichTextEditor, type RichTextJson } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function JsonNotes() {
  const [doc, setDoc] = useState<RichTextJson>({ type: "doc", content: [{ type: "paragraph" }] });
  return <RichTextEditor format="json" aria-label="Notes" value={doc} onValueChange={setDoc} />;
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Ctrl/Cmd+B, I, U | Bold, italic, underline. |
| Ctrl/Cmd+Shift+S | Strikethrough. |
| Ctrl/Cmd+E | Inline code. |
| Ctrl/Cmd+Alt+1-3 | Headings. |
| Ctrl/Cmd+Shift+7 / 8 | Numbered / bulleted list. |
| Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z | Undo, redo. |
| Arrow keys in the toolbar | Move between toggles (follow the reading direction). |

The content is `role="textbox"` with `aria-multiline`. Toolbar buttons have `aria-label`s (en/ar built in) and
toggles expose `aria-pressed`. Point `aria-labelledby` at the visible label, or give `aria-label`. Tooltips carry the
shortcut but are not the only name.

## RTL & i18n

- Each paragraph, heading, quote and list has `dir="auto"`; with `text-start` the block aligns to its own start.
- The `dir` attribute is stored in the document, so HTML and JSON output keep it and `Markdown`-style renderers or emails orient correctly.
- Toolbar labels, placeholder and link dialog are Arabic when the Nasaq locale is `ar`. The link address input is forced LTR.
- Undo and redo icons mirror in RTL.

## Styling & tokens

Content typography reuses the `Markdown` roles: `text-h1..h3`, `text-body`, `text-nq-fg-body`,
`border-nq-line-strong`, `bg-secondary` for inline code. The frame uses `border-input`, `bg-card` and the focus outline
token. Target `data-slot` values and `data-readonly`. Extend with `className`.

## Do / Don't

- Do lazy-load it.
- Do sanitise the HTML on the server before storing or rendering it elsewhere; the editor only filters link schemes.
- Don't put raw hex colours in pasted content styling.
- Don't use it for short single-line input.

## Related

- [markdown](https://docs.nasaqui.com/components/markdown)
- [field](https://docs.nasaqui.com/components/field)
- [toggle-group](https://docs.nasaqui.com/components/toggle-group)

## Lab

https://docs.nasaqui.com/?path=/docs/components-editors-richtexteditor--docs

## Code

### React

```tsx
import { RichTextEditor } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Notes() {
  const [html, setHtml] = useState("<p>Hello</p>");
  return <RichTextEditor aria-label="Notes" value={html} onValueChange={setHtml} />;
}
```

### shadcn

```tsx
import { RichTextEditor } from "@/components/ui/rich-text-editor";
import { useState } from "react";

export function Notes() {
  const [html, setHtml] = useState("<p>Hello</p>");
  return <RichTextEditor aria-label="Notes" value={html} onValueChange={setHtml} />;
}
```

### Vue

```vue
<script setup lang="ts">
import { NqRichTextEditor } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

// Tiptap is not bundled. Install @tiptap/core, @tiptap/pm, @tiptap/starter-kit and @tiptap/extensions, re-export
// { Editor, Extension, StarterKit, Placeholder } from one module (./tiptap.ts) and pass
// :load="() => import('./tiptap')" so it is fetched on first mount. Until then the toolbar is disabled.
const html = ref("<p>Hello</p>");
</script>

<template>
  <NqRichTextEditor v-model="html" aria-label="Notes" class="w-[32rem]" />
</template>
```

### Blade

```blade
{{-- Tiptap is not bundled: pass load="() => import('/assets/tiptap.js')" (a module re-exporting Editor, Extension, StarterKit and Placeholder) or set window.NasaqRichText = { load }. --}}
<x-nq::rich-text-editor class="w-[32rem]" value="<p>Hello</p>" aria-label="Notes" />
```

### HTML + Alpine

```html
<div data-slot="rich-text-editor" x-data="nqRichText('\u003Cp\u003EHello\u003C\/p\u003E', JSON.parse('{\u0022ariaLabel\u0022:\u0022Notes\u0022}'))" x-modelable="value"     class="flex min-w-0 flex-col overflow-hidden rounded-control border border-input bg-card text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus w-[32rem]">
            <div role="toolbar" aria-label="Formatting" data-slot="rich-text-editor-toolbar" class="flex flex-wrap items-center gap-1 border-b border-border bg-card p-1.5">
                                            <div role="group" data-slot="toggle-group" data-multiple class="flex w-fit max-w-full rounded-control bg-transparent p-0">
                                            <button type="button" data-slot="toggle" aria-label="Bold" title="Bold" x-on:mousedown.prevent x-on:click="run('bold')"
                            :aria-pressed="on('bold') ? 'true' : 'false'" :data-pressed="on('bold') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Italic" title="Italic" x-on:mousedown.prevent x-on:click="run('italic')"
                            :aria-pressed="on('italic') ? 'true' : 'false'" :data-pressed="on('italic') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <line x1="19" x2="10" y1="4" y2="4"/>
  <line x1="14" x2="5" y1="20" y2="20"/>
  <line x1="15" x2="9" y1="4" y2="20"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Underline" title="Underline" x-on:mousedown.prevent x-on:click="run('underline')"
                            :aria-pressed="on('underline') ? 'true' : 'false'" :data-pressed="on('underline') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M6 4v6a6 6 0 0 0 12 0V4"/>
  <line x1="4" x2="20" y1="20" y2="20"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Strikethrough" title="Strikethrough" x-on:mousedown.prevent x-on:click="run('strike')"
                            :aria-pressed="on('strike') ? 'true' : 'false'" :data-pressed="on('strike') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M16 4H9a3 3 0 0 0-2.83 4"/>
  <path d="M14 12a4 4 0 0 1 0 8H6"/>
  <line x1="4" x2="20" y1="12" y2="12"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Code" title="Code" x-on:mousedown.prevent x-on:click="run('code')"
                            :aria-pressed="on('code') ? 'true' : 'false'" :data-pressed="on('code') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m16 18 6-6-6-6"/>
  <path d="m8 6-6 6 6 6"/>
</svg>                        </button>
                                    </div>
                            <div data-slot="separator" role="separator" data-orientation="vertical"
     aria-orientation="vertical"     class="shrink-0 bg-border h-4 w-px self-center"></div>
                <div role="group" data-slot="toggle-group" data-multiple class="flex w-fit max-w-full rounded-control bg-transparent p-0">
                                            <button type="button" data-slot="toggle" aria-label="Heading 1" title="Heading 1" x-on:mousedown.prevent x-on:click="run('h1')"
                            :aria-pressed="on('h1') ? 'true' : 'false'" :data-pressed="on('h1') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M4 12h8"/>
  <path d="M4 18V6"/>
  <path d="M12 18V6"/>
  <path d="m17 12 3-2v8"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Heading 2" title="Heading 2" x-on:mousedown.prevent x-on:click="run('h2')"
                            :aria-pressed="on('h2') ? 'true' : 'false'" :data-pressed="on('h2') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M4 12h8"/>
  <path d="M4 18V6"/>
  <path d="M12 18V6"/>
  <path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Heading 3" title="Heading 3" x-on:mousedown.prevent x-on:click="run('h3')"
                            :aria-pressed="on('h3') ? 'true' : 'false'" :data-pressed="on('h3') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M4 12h8"/>
  <path d="M4 18V6"/>
  <path d="M12 18V6"/>
  <path d="M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2"/>
  <path d="M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Bulleted list" title="Bulleted list" x-on:mousedown.prevent x-on:click="run('bulletList')"
                            :aria-pressed="on('bulletList') ? 'true' : 'false'" :data-pressed="on('bulletList') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M3 5h.01"/>
  <path d="M3 12h.01"/>
  <path d="M3 19h.01"/>
  <path d="M8 5h13"/>
  <path d="M8 12h13"/>
  <path d="M8 19h13"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Numbered list" title="Numbered list" x-on:mousedown.prevent x-on:click="run('orderedList')"
                            :aria-pressed="on('orderedList') ? 'true' : 'false'" :data-pressed="on('orderedList') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11 5h10"/>
  <path d="M11 12h10"/>
  <path d="M11 19h10"/>
  <path d="M4 4h1v5"/>
  <path d="M4 9h2"/>
  <path d="M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02"/>
</svg>                        </button>
                                            <button type="button" data-slot="toggle" aria-label="Quote" title="Quote" x-on:mousedown.prevent x-on:click="run('blockquote')"
                            :aria-pressed="on('blockquote') ? 'true' : 'false'" :data-pressed="on('blockquote') ? '' : undefined" :disabled="!ready" class="inline-flex size-control-sm shrink-0 items-center justify-center whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] px-0 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">
                            <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/>
  <path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/>
</svg>                        </button>
                                    </div>
                                        <div data-slot="separator" role="separator" data-orientation="vertical"
     aria-orientation="vertical"     class="shrink-0 bg-border h-4 w-px self-center"></div>
                <div class="relative flex items-center gap-0.5">
                                            <button data-slot="button"
     type="button"                         aria-label="Link" title="Link" :aria-pressed="on(&#039;link&#039;) ? &#039;true&#039; : &#039;false&#039;" :data-pressed="on(&#039;link&#039;) ? &#039;&#039; : undefined" :aria-expanded="linkOpen ? &#039;true&#039; : &#039;false&#039;" :disabled="!ready" x-on:mousedown.prevent="x-on:mousedown.prevent" x-on:click="toggleLink()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 data-pressed:bg-nq-selected">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M9 17H7A5 5 0 0 1 7 7h2"/>
  <path d="M15 7h2a5 5 0 1 1 0 10h-2"/>
  <line x1="8" x2="16" y1="12" y2="12"/>
</svg></button>
                                                                <button data-slot="button"
     type="button"                         aria-label="Undo" title="Undo" :disabled="!canUndo()" x-on:mousedown.prevent="x-on:mousedown.prevent" x-on:click="undo()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M9 14 4 9l5-5"/>
  <path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>
</svg></button>
                                                                <button data-slot="button"
     type="button"                         aria-label="Redo" title="Redo" :disabled="!canRedo()" x-on:mousedown.prevent="x-on:mousedown.prevent" x-on:click="redo()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m15 14 5-5-5-5"/>
  <path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13"/>
</svg></button>
                                                                <form x-show="linkOpen" x-cloak style="display: none" x-on:submit.prevent="applyLink()" x-on:keydown.escape="linkOpen = false" data-slot="rich-text-editor-link"
                            class="absolute start-0 top-full z-50 mt-1 flex w-72 flex-col gap-2 rounded-card border border-border bg-card p-3 shadow-floating">
                            <input data-slot="input" type="text"
     dir="ltr"                 x-model="linkUrl" inputmode="url" placeholder="https://" aria-label="Link address" x-on:input="linkError = false" :aria-invalid="linkError ? &#039;true&#039; : undefined" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
                            <div class="flex justify-end gap-2">
                                <button data-slot="button"
     type="button"                         x-show="on(&#039;link&#039;)" x-on:click="removeLink()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Remove link</button>
                                <button data-slot="button"
     type="submit"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control-sm px-2.5">
        Apply</button>
                            </div>
                        </form>
                                    </div>
                    </div>
        <div x-ref="host" style="min-height: 10rem" class="min-w-0 flex-1 cursor-text px-3 py-2.5 [&amp;_.tiptap]:min-h-[inherit] [&amp;_.tiptap]:outline-none [&amp;_.tiptap]:flex [&amp;_.tiptap]:flex-col [&amp;_.tiptap]:gap-3 [&amp;_.tiptap]:text-body [&amp;_.tiptap]:text-nq-fg-body [&amp;_.tiptap_p]:text-start [&amp;_.tiptap_p]:min-h-[1lh] [&amp;_.tiptap_h1]:text-h1 [&amp;_.tiptap_h1]:text-foreground [&amp;_.tiptap_h1]:text-start [&amp;_.tiptap_h2]:text-h2 [&amp;_.tiptap_h2]:text-foreground [&amp;_.tiptap_h2]:text-start [&amp;_.tiptap_h3]:text-h3 [&amp;_.tiptap_h3]:text-foreground [&amp;_.tiptap_h3]:text-start [&amp;_.tiptap_ul]:list-disc [&amp;_.tiptap_ol]:list-decimal [&amp;_.tiptap_ul]:ps-6 [&amp;_.tiptap_ol]:ps-6 [&amp;_.tiptap_ul]:space-y-1 [&amp;_.tiptap_ol]:space-y-1 [&amp;_.tiptap_li]:text-start [&amp;_.tiptap_li]:marker:text-muted-foreground [&amp;_.tiptap_blockquote]:border-s-2 [&amp;_.tiptap_blockquote]:border-nq-line-strong [&amp;_.tiptap_blockquote]:ps-4 [&amp;_.tiptap_blockquote]:text-muted-foreground [&amp;_.tiptap_blockquote]:text-start [&amp;_.tiptap_a]:rounded-[2px] [&amp;_.tiptap_a]:text-foreground [&amp;_.tiptap_a]:underline [&amp;_.tiptap_a]:decoration-nq-line-strong [&amp;_.tiptap_a]:underline-offset-4 [&amp;_.tiptap_strong]:font-semibold [&amp;_.tiptap_strong]:text-foreground [&amp;_.tiptap_code]:rounded-[4px] [&amp;_.tiptap_code]:bg-secondary [&amp;_.tiptap_code]:px-1 [&amp;_.tiptap_code]:font-mono [&amp;_.tiptap_code]:text-code"></div>
</div>
```
