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.
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.
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.
- [ ] / - [x] as clickable checkboxes- [ ] Note
Callouts derive their title from the marker β
NOTE,TIP,IMPORTANT,WARNING,CAUTIONβ and round-trip viadata-calloutwithout 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.
defmodule Leaf.Demo do
@moduledoc "Round-trips the language tag and shows a copy button."
def greet(name), do: "Hello, " <> name <> "!"
end
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.
<br>) inside the current blockTags 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 edit
ReleaseLeaf 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 edit
Atomic 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.
# 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" />
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.