# MarkdownEditor

WYSIWYG markdown editor based on Tiptap. Renders markdown visually (headings, lists, blockquotes) while editing, serializes to markdown string.

## Features

- Visual headings (H1, H2, H3) with proper sizing
- Bold, italic, strikethrough, inline code
- Bullet and ordered lists
- Blockquotes with left border
- Horizontal rules
- Toolbar with icon buttons
- `@`-mentions with auto-flipping popup (`@floating-ui/dom`)
- Customizable markdown serialization for mentions (six built-in presets)
- Markdown input/output (stored as plain markdown string)
- SSR-safe (`immediatelyRender: false`)

## Quick Start

```tsx
import { MarkdownEditor } from '@djangocfg/ui-tools';

function MyComponent() {
  const [bio, setBio] = useState('# Hello\n\nThis is **markdown**.');

  return (
    <MarkdownEditor
      value={bio}
      onChange={setBio}
      placeholder="Write something..."
    />
  );
}
```

## CSS

Two ways to load styles, depending on your build:

```ts
// Tailwind-based apps — pulls in source utilities so Tailwind's JIT picks them up.
import '@djangocfg/ui-tools/styles';

// Plain Vite / webpack / CRA — pre-compiled CSS, no Tailwind required.
import '@djangocfg/ui-tools/dist.css';
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `string` | — | Markdown string |
| `onChange` | `(value: string) => void` | — | Called on every change |
| `placeholder` | `string` | `'Write markdown...'` | Placeholder text |
| `minHeight` | `number` | `120` | Min height in px |
| `className` | `string` | — | Additional CSS class |
| `disabled` | `boolean` | `false` | Read-only mode |
| `showToolbar` | `boolean` | `true` | Show formatting toolbar |
| `mentions` | `MentionConfig` | — | `@`-mention autocomplete config |
| `onMentionIdsChange` | `(ids: string[]) => void` | — | Called when mentioned IDs change |

### `MentionConfig` fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `items` | `MentionItem[]` | required | Available mention items (`id`, `label`, optional `description`, `thumbnail`) |
| `trigger` | `string` | `'@'` | Trigger character |
| `maxItems` | `number` | `5` | Max items shown in dropdown |
| `renderMarkdown` | `MentionMarkdownRenderer` | `mentionPresets.plainAt` | Serializes a mention to markdown — see below |

## Mentions

```tsx
<MarkdownEditor
  value={text}
  onChange={setText}
  mentions={{
    items: [
      { id: '1', label: 'Alice', thumbnail: '/alice.jpg' },
      { id: '2', label: 'Bob', description: 'Antagonist' },
    ],
  }}
  onMentionIdsChange={(ids) => console.log('Mentioned:', ids)}
/>
```

Type `@` to trigger autocomplete. Mentions render as inline chips. The popup auto-flips above the trigger when there isn't enough room below, and tracks scroll/resize via `autoUpdate` — viewport-aware out of the box.

> Heads-up: Tiptap initialises the editor exactly once. If `mentions` is `undefined` on first render and becomes truthy later, the extension is never installed. Pass `{ items: [] }` from the start and mutate `.items` in place if items load async.

## Mention serialization

By default Tiptap's `Mention` extension emits a useless shortcode like `[@ id="..." label="..."]` into markdown. `MarkdownEditor` overrides that with the renderer from `MentionConfig.renderMarkdown` (defaults to `mentionPresets.plainAt`, which yields `@<label>`).

Six presets are exported from `@djangocfg/ui-tools`:

```tsx
import { MarkdownEditor, mentionPresets } from '@djangocfg/ui-tools';

<MarkdownEditor
  value={text}
  onChange={setText}
  mentions={{
    items,
    renderMarkdown: mentionPresets.customUri('cmdop', 'machine'),
  }}
/>
```

| Preset | Output for `{ label: 'Vps-audi', id: 'uuid' }` |
|--------|-------------------------------------------------|
| `plainAt` *(default)* | `@Vps-audi` |
| `plainLabel` | `Vps-audi` |
| `markdownLink(baseUrl)` | `[@Vps-audi](baseUrl/uuid)` |
| `customUri(scheme, kind)` | `@[Vps-audi](scheme://kind/uuid)` |
| `slackStyle` | `<@uuid>` |
| `htmlSpan(className?)` | `<span class="mention" data-mention-id="uuid">@Vps-audi</span>` |

Pick by use case:

- **`plainAt`** — chat composers feeding an LLM that reads `@<label>` natively.
- **`customUri`** — chat that also wants machine-readable IDs in `href` for downstream parsing (e.g. `@[Vps-audi](cmdop://machine/uuid)`).
- **`markdownLink`** — clickable mentions linking to a profile / detail page.
- **`slackStyle`** — interop with Slack-like backends that resolve `<@id>` server-side.
- **`htmlSpan`** — markdown consumers that allow inline HTML and want chip styling baked in.
- **`plainLabel`** — bare display string (no `@`), e.g. for `/`-commands or non-mention triggers.

### Custom renderer

The signature is just `(attrs: MentionAttrs) => string`:

```tsx
import type { MentionMarkdownRenderer } from '@djangocfg/ui-tools';

const renderMention: MentionMarkdownRenderer = ({ id, label }) =>
  `{{user:${id}|${label}}}`;

<MarkdownEditor mentions={{ items, renderMarkdown: renderMention }} ... />
```

Either `id` or `label` may be empty strings if upstream config didn't populate them — fall back accordingly. Returning `''` drops the mention from the output.

> Mentions are write-only: the markdown isn't parsed back into mention nodes on `setContent`. After submit/reset, the editor receives a plain string — fine for chat composers.

## Dependencies

All Tiptap packages and `@floating-ui/dom` are direct dependencies — no extra installs needed.
