Leaf Editor Demo

Hybrid / WYSIWYG / Markdown / HTML editor. Current mode: hybrid saved

Leaf 0.5.1 β€” expects JS bundle 0.5.1, compared against window.LeafHooks.version on mount (console warning on a mismatch, console error if the hook never attached at all). New in 0.5.1: atomic blocks that typeset known attributes as a preview β€” eyebrow, title, supporting text, banner, call to action β€” with unrecognised ones falling through to a faint source line, and double-click to edit the block's raw source in place; an awaitable flush with a correlation ref; deny: [:visual_mode, :hybrid_mode] to complete the mode deny list; toolbar_extra buttons that refuse to collapse into "More"; hashtag styling and boundary: :not_line_start for the # trigger; a warning for undeclared custom tags; and bundle presence/staleness checks. Plus: inline suggestions, GFM task lists & callouts, custom-tag preservation, code blocks with language tags, smart typography, RTL, spellcheck, and lifecycle events.

Tidying…

Welcome to Leaf

A Phoenix LiveView editor with four modes β€” Hybrid, Visual, Markdown, and HTML β€” and one source of truth: the markdown panel below updates as you type.

Hybrid live preview

Put your cursor anywhere in this paragraph and watch the markers fade in. Move it away β€” they fade out. That's hybrid mode: ~~no more~~ guessing whether you're in WYSIWYG or markdown, and no more accidental formatting.

Type > on a new line to start a blockquote. Type - for a bullet list, 1. for a numbered one, or - [ ] for a task. Type --- on its own line for a horizontal rule. Type [label](https://hexdocs.pm/leaf) and arrow into it to edit the URL.

GFM task lists

  • Round-trip - [ ] / - [x] as clickable checkboxes
  • Toggle on tap (mousedown) β€” no caret hijack
  • Try it: click a box, or add a new task with - [ ]
  • Loose checklists (blank line between items) still round-trip

GFM callouts

Note

Callouts derive their title from the marker β€” NOTE, TIP, IMPORTANT, WARNING, CAUTION β€” and round-trip via data-callout without touching the markdown source.

Tip

Put your cursor inside the callout body and edit normally. The title block stays non-editable.

Warning

Switch to Markdown mode (top-right) to see the raw > [!WARNING] syntax.

Fenced code with a language tag

defmodule Leaf.Demo do
  @moduledoc "Round-trips the language tag and shows a copy button."

  def greet(name), do: "Hello, " <> name <> "!"
end

Inline suggestions

Type @ for a person, or / at the start of an empty line for a component. Type : plus two letters for emoji, and # mid-line for a tag β€” the first column is reserved, so # there is still a heading.

Because a # trigger is configured, Leaf knows # means "tag" here and paints one: #elixir and #liveview read as tokens rather than prose, while the markdown stays #elixir.

The editor knows none of those lists. It spots the trigger, asks this LiveView over {:leaf_suggest, ...}, and inserts whatever you pick; the Lifecycle Events panel below logs every request. Four triggers are configured at once β€” the one nearest the caret wins.

Shortcuts that matter

  • Cmd/Ctrl + B β€” bold the selection (or the current word)
  • Cmd/Ctrl + I β€” italic
  • Cmd/Ctrl + K β€” wrap selection in a link
  • Cmd/Ctrl + Shift + V β€” paste as plain text
  • Enter inside an empty list item or blockquote β€” exit it
  • Shift + Enter β€” soft line break (<br>) inside the current block

Custom tag preservation

Tags listed in preserve_tags render as atomic blocks and serialize back to source byte-for-byte. A block is no longer an opaque ⧉ Hero chip: known attribute names are mapped to typographic roles and typeset in the editor's own prose voice β€” kicker as an eyebrow, title as a title, subtitle as supporting text, cover as a banner:

HeroDouble-click to editReleaseLeaf 0.5.1Atomic blocks that read like a document, awaitable flush, deniable modes.

Children render as formatted text, so bold and links inside a wrapping tag stay visible. An attribute with no role β€” variant below β€” falls through to a small, faint source line, because there is nothing better to say about it:

ShowcaseDouble-click to editAtomic blocks you can readDouble-click this block to edit its raw source in place β€” ⌘/Ctrl+Enter or Save commits, Escape cancels. The round trip stays byte-for-byte identical either way.variant="wide"

label plus href reads as a call to action with its destination:

CTADouble-click to editRead the docshttps://hexdocs.pm/leaf

The scale stays close to prose on purpose β€” this is a placeholder that reads like a document, not an imitation of the published component. Click around a block: it stays intact and survives round-trips through every mode. Undeclared <Foo /> tags now log a warning naming the declaration to paste, instead of being silently flattened on the first keystroke.


Landscape
Saved
Raw Markdown Output
# Welcome to Leaf

A **Phoenix LiveView** editor with four modes β€” *Hybrid*, *Visual*, *Markdown*, and *HTML* β€” and one source of truth: the markdown panel below updates as you type.

## Hybrid live preview

Put your cursor anywhere in this paragraph and ***watch the markers fade in***. Move it away β€” they fade out. That's hybrid mode: ~~no more~~ guessing whether you're in WYSIWYG or markdown, and ||no more accidental formatting||.

Type `> ` on a new line to start a blockquote. Type `- ` for a bullet list, `1. ` for a numbered one, or `- [ ] ` for a task. Type `---` on its own line for a horizontal rule. Type `[label](https://hexdocs.pm/leaf)` and arrow into it to edit the URL.

## GFM task lists

- [x] Round-trip `- [ ] ` / `- [x] ` as clickable checkboxes
- [x] Toggle on tap (mousedown) β€” no caret hijack
- [ ] Try it: click a box, or add a new task with `- [ ] `
- [ ] Loose checklists (blank line between items) still round-trip

## GFM callouts

> [!NOTE]
> Callouts derive their title from the marker β€” `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION` β€” and round-trip via `data-callout` without touching the markdown source.

> [!TIP]
> Put your cursor inside the callout body and edit normally. The title block stays non-editable.

> [!WARNING]
> Switch to **Markdown** mode (top-right) to see the raw `> [!WARNING]` syntax.

## Fenced code with a language tag

```elixir
defmodule Leaf.Demo do
  @moduledoc "Round-trips the language tag and shows a copy button."

  def greet(name), do: "Hello, " <> name <> "!"
end
```

## Inline suggestions

Type `@` for a person, or `/` at the start of an empty line for a component. Type `:` plus two letters for emoji, and `#` mid-line for a tag β€” the first column is reserved, so `# ` there is still a heading.

Because a `#` trigger is configured, Leaf knows `#` means "tag" here and paints one: #elixir and #liveview read as tokens rather than prose, while the markdown stays `#elixir`.

The editor knows none of those lists. It spots the trigger, asks this LiveView over `{:leaf_suggest, ...}`, and inserts whatever you pick; the Lifecycle Events panel below logs every request. Four triggers are configured at once β€” the one nearest the caret wins.

## Shortcuts that matter

- **Cmd/Ctrl + B** β€” bold the selection (or the current word)
- **Cmd/Ctrl + I** β€” italic
- **Cmd/Ctrl + K** β€” wrap selection in a link
- **Cmd/Ctrl + Shift + V** β€” paste as plain text
- **Enter** inside an empty list item or blockquote β€” exit it
- **Shift + Enter** β€” soft line break (`<br>`) inside the current block

## Custom tag preservation

Tags listed in `preserve_tags` render as atomic blocks and serialize back to source byte-for-byte. A block is no longer an opaque `⧉ Hero` chip: known attribute names are mapped to typographic roles and typeset in the editor's own prose voice β€” `kicker` as an eyebrow, `title` as a title, `subtitle` as supporting text, `cover` as a banner:

<Hero kicker="Release" title="Leaf 0.5.1" subtitle="Atomic blocks that read like a document, awaitable flush, deniable modes." image="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwallpaperaccess.com%2Ffull%2F168092.jpg&f=1&nofb=1&ipt=92fbdbaac52b7179875b3ae036db9466b8b20e30b1d4987d35993a0e14850e9c" />

Children render as formatted text, so bold and links inside a wrapping tag stay visible. An attribute with no role β€” `variant` below β€” falls through to a small, faint source line, because there is nothing better to say about it:

<Showcase title="Atomic blocks you can read" variant="wide" cover="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwallpaperaccess.com%2Ffull%2F168092.jpg&f=1&nofb=1&ipt=92fbdbaac52b7179875b3ae036db9466b8b20e30b1d4987d35993a0e14850e9c">
**Double-click** this block to edit its raw source in place β€” ⌘/Ctrl+Enter or Save commits, Escape cancels. The [round trip](https://hexdocs.pm/leaf) stays byte-for-byte identical either way.
</Showcase>

`label` plus `href` reads as a call to action with its destination:

<CTA label="Read the docs" href="https://hexdocs.pm/leaf" />

The scale stays close to prose on purpose β€” this is a placeholder that reads like a document, not an imitation of the published component. Click around a block: it stays intact and survives round-trips through every mode. Undeclared `<Foo />` tags now log a warning naming the declaration to paste, instead of being silently flattened on the first keystroke.

---

<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwallpaperaccess.com%2Ffull%2F168092.jpg&f=1&nofb=1&ipt=92fbdbaac52b7179875b3ae036db9466b8b20e30b1d4987d35993a0e14850e9c" alt="Landscape" width="475" height="299" />
Lifecycle Events 0

Gated behind emit_events=true: focus, blur, selection_changed, paste_image. Ungated: suggest, toolbar_action, and the flushed reply to a flush carrying a ref.

  • No events yet β€” focus the editor.