0.2.13

  • Add hybrid mode — Obsidian-style live preview that renders formatting inline (bold, italic, strike, code, spoiler, headings, horizontal rule, ordered/unordered lists) while keeping the source markers editable. Markers (**, *, ~~, ||, `, #######) appear as faded characters when the cursor is inside their wrapper and fade out as soon as the cursor leaves; arrowing into a wrapper from either side reveals them again. Hybrid is now the first tab in the mode switcher.
  • Hybrid auto-format as you type: **word**, *word*, ~~word~~, ||word||, `word`, and ***word*** wrap on the closing delimiter; # through ###### retag the current paragraph as a heading and live-retag when the user adds or removes leading #s; --- on its own line becomes a real <hr>; - / * / + becomes a <ul>; 1. (or any \d+.) becomes an <ol> (with start="N" when N ≠ 1). Consecutive list paragraphs merge into one wrapper rather than producing one wrapper per item.
  • Hybrid horizontal rule is cursor-aware: clicking the rule, or arrowing onto it from above / below, swaps it for an editable <p data-leaf-hr-source>---</p> so the dashes can be adjusted or deleted; moving the cursor away renders the rule again. Arrow detection uses bounding-rect line measurement so it fires from the first/last visual line of any block (empty, multi-line, or with a trailing <br> filler), not just from an empty paragraph.
  • Hybrid handles nested formatting (***bold-italic***, ~~**within strike**~~, etc.) by decorating the full chain of ancestors at once, deferring auto-format inside an unclosed outer delimiter, then recursively wrapping the inner pattern when the outer closes. Editing or deleting a delimiter span unwraps the formatting back to plain text.
  • Hybrid keeps typing past the closing delimiter working reliably across browsers — the keystroke is intercepted in keydown and inserted outside the wrapper as a sibling, even when Chrome's caret affinity would otherwise pull the cursor back inside. Typing inside an already-formatted paragraph now lands at the cursor position instead of being silently redirected to the end of the line.
  • After hybrid auto-formats a wrapper, the caret rests just past the closing delimiter (**bold**|, not **bold|**) while still anchored inside the wrapper so the markers stay visible without needing a click. Decoration markers also appear immediately for second / third / nested wrappers in the same paragraph (previously a click-in was needed).
  • Stop the hybrid-only ** / * / ~~ / # decoration markers from leaking into pure visual mode: the deferred toolbar refresh and the heading-decoration listener are now mode-gated, and switching from hybrid to visual strips every existing decoration span (and the cursor-anchoring zero-width spaces heading decoration leaves behind) from the contenteditable.
  • Replace the visual-mode HR toolbar handler. document.execCommand("insertHorizontalRule") produced inconsistent DOM that didn't round-trip through htmlToMarkdown, so the rule vanished on the next re-render and never appeared in markdown mode. The handler now builds the <hr> and trailing <p> manually (same shape hybrid mode uses), and the duplicate <hr> CSS that the JS hook was injecting on top of the inline <style> rule is gone — the line renders once, vertically centered inside an 18px hover-friendly hit area.
  • Center the drag handle against blocks shorter than the handle itself (in particular hybrid-mode <hr>), so the grab icon aligns with the rule's line instead of hovering above the block.
  • Fix markdown → hybrid and html → hybrid mode switches losing the latest edits. _syncModes only matched to === "visual" when copying content out of the markdown / html textareas, so switching to hybrid left the visual contenteditable showing its previous DOM. Both branches now fire for hybrid too (hybrid reuses the same contenteditable as visual).
  • Wire hybrid mode into the footer word / char counter. Counts read 0 / 0 in hybrid before because _updateCounts had no branch for it; the new branch reads the contenteditable's text after stripping decoration spans and ZWSPs, so the numbers track user-perceived content and don't jitter when the cursor moves in / out of formatted runs.

0.2.12

  • Add a vertical resize grip to the visual editor's bottom-right corner so users can drag to grow or shrink the editing area, mirroring the native grip the markdown and html textareas already had. Per-mode resize state (each mode keeps its own height — resizing in visual doesn't change markdown's height and vice versa).
  • Double-click the resize grip to auto-fit the editor height to its content. Works for visual, markdown, and html modes. The auto-fit clamps to the configured :height as a floor — shorter content still respects the minimum.
  • Show a small tooltip ("Drag to resize · Double-click to fit content") when the mouse hovers the resize-grip area, so the double-click gesture is discoverable.
  • Fix the bold toolbar button lighting up for plain heading text. document.queryCommandState("bold") returns true for any text whose computed font-weight is bold, and the editor's own CSS sets font-weight: 700 on h1h4, so a heading without an explicit <b>/<strong> was misreported as bold. The button now probes for an actual <b>/<strong> ancestor; explicit bold inside a heading still lights up correctly.
  • Add inline spoilers (Discord-style ||hidden text|| markdown) with a Spoiler entry in the More-formatting dropdown. Renders as a censored block (dark background, hidden text) anywhere on the page; click anywhere on the page to reveal. Inside the editor itself the spoiler text is always shown (with a subtle background hint) so writers can see what they're typing.
  • Quality-of-life cursor escapes for any inline formatting (<b>, <strong>, <i>, <em>, <s>, <del>, <code>, <u>, <sub>, <sup>, <mark>, <a>, and the spoiler span). Pressing Enter inside any of them breaks out into a fresh paragraph instead of carrying the formatting into the new <p>. ArrowLeft at the start or ArrowRight at the end exits the wrapper on a single press; if there's no content on the target side, a non-breaking space is inserted so the cursor has a typeable home.
  • Preserve the contenteditable's selection when the user miss-clicks anywhere on the editor's chrome (toolbar gaps, dividers, mode tabs, footer, border, background). mousedown is intercepted on the editor wrapper and preventDefault keeps focus in the contenteditable; clicks still register so buttons and dropdown triggers behave normally. Clicks on form controls and inside the contenteditable itself are unaffected.
  • Make the main image toolbar button fall back to the URL dialog when the consumer hasn't configured an upload_handler. The wrapper's data-has-upload attribute now correctly reflects whether upload_handler is set (it previously misreported truthy whenever :image was in the toolbar list). Result: with :image in the toolbar but no upload handler, clicking the main image button opens the URL dialog directly instead of silently no-opping.
  • Stop the LiveView from crashing on media_ui_opened / media_ui_closed events that the image-URL dialog (and other media popovers) push. Added no-op handlers; the events are kept on the wire so future server-side reactions to media UI being active can hook in without breaking existing consumers.
  • Align toolbar icons on a consistent vertical centerline. SVG icons no longer drift from inline-baseline (svg { display: block }), text-glyph buttons (B/I/S/H) get a tighter line-height: 1, and the dropdown wrappers for heading, more-formatting, table, and more-inserts now use inline-flex so their buttons sit at the same height as direct flex-child buttons instead of being pushed up by the wrapper's line-height. Rules ship in the inline <style> block emitted server-side so the alignment is correct on first paint, not just after mounted() runs.
  • Force Shift+Enter to always insert a soft break (<br>) inside the current block. Some browsers — notably Chromium contenteditables that have defaultParagraphSeparator set to "p" — otherwise treat Shift+Enter the same as plain Enter and start a new <p>. The editor now intercepts the key and inserts a <br> explicitly, so a single Shift+Enter always continues the current paragraph on a new line and any following <p> stays separate.
  • Preserve <br> soft breaks across visual↔markdown↔html round-trips. Earmark's breaks: true HTML output puts a literal \n after every <br> as pretty-print whitespace, which htmlToMarkdown was reading and combining with the <br>'s own \n into \n\n — a markdown paragraph break — causing a single paragraph with internal soft breaks to split into multiple paragraphs after a round-trip. The walker now strips leading newlines from text nodes that follow a <br>. Same root cause for the cursor-visibility filler <br> that the Shift+Enter handler appends at end of block — it's now marked data-leaf-filler and skipped by the markdown walker so a Shift+Enter at end of paragraph doesn't get serialized as a paragraph break.

0.2.11

  • Treat single newlines in markdown as line breaks when rendering to HTML for visual mode (breaks: true passed to Earmark). Content like emoji-prefixed lists or any line-by-line text without blank lines now renders line-by-line in visual mode, matching how the markdown source visually appears in markdown mode and how editors like GitHub, Slack, and Notion handle the same input. Round-trips through visual→markdown still preserve the original \n-separated source.

0.2.10

  • Fix the editor expanding horizontally on mount and stealing space from sibling flex items. The outer wrapper and the toolbar now have min-width: 0 so the editor's intrinsic min-content width can no longer push past its parent's allocated width. Pages with a flex-[2] / flex-1 split (or similar) no longer redistribute when the editor finishes mounting.

0.2.9

  • Loading placeholder now picks a random label per page load by default (loading_preset defaults to :random, drawing from the bundled set of :unpuzzling, :brewing, :polishing, :composing, :crafting, :tidying). Use loading_preset={:default} for the plain "Loading…" label, or loading_text="…" to fully customize.
  • Fix layout jump on loading→ready: the toolbar, mode tabs, border wrapper, and footer now render as a real skeleton during loading, so the page no longer shifts when the editor finishes mounting.
  • Pin data-leaf-mount-state to "ready" in the JS hook's updated() callback so a parent re-render can't briefly flicker the editor back through the loading state.

0.2.8

  • Add a styled loading placeholder shown until the editor JS mounts, replacing the brief flash of unstyled content on cold page loads. Configurable via two new leaf_editor/1 attrs:
    • loading_preset (:default | :unpuzzling | :brewing | :polishing | :composing | :crafting | :tidying) — pick a bundled label

    • loading_text — fully custom string, wins over the preset

0.2.7

  • Fix excessive blank lines in markdown output when pressing Enter in visual mode
  • Fix mode toggle reverting to visual and in-progress keystrokes being lost while typing in markdown or html mode
  • Fix table column widths shifting while typing into cells

0.2.6

  • Add edit image URL button (pencil icon) to image floating island
  • Add simple/advanced toolbar presets for lightweight vs full editing
  • Prevent bold toggling inside headings to keep visual-markdown sync
  • Fix drag handle jumping to wrong block on large/resized images
  • Fix image popover persistence through LiveView re-renders

0.2.5

  • Add image insert by URL with split button toolbar (upload + by URL options)
  • Add inline URL dialog with alt text support for both visual and markdown modes
  • Bump sticky toolbar z-index for better stacking with fixed navbars

0.2.4

  • Remove blue focus outline from contenteditable editor area

0.2.3

  • Fix footer word/char counts resetting to zero on component re-render

0.2.2

  • Add leaf_editor/1 function component wrapper for cleaner <.leaf_editor /> syntax

0.2.1

  • Add editor footer with live word and character count

0.2.0

  • Add drag-and-drop block reordering for images and any block element
  • Add drag handles for easier block manipulation with margin hover activation
  • Add image resize handles with persistent dimensions through save
  • Add table support with insert, add/remove row and column operations
  • Add More Inserts dropdown to toolbar for organized insert options
  • Add superscript and subscript toolbar buttons
  • Add indent/outdent toolbar buttons
  • Add emoji picker toolbar button (keeps open for multiple inserts)
  • Update toolbar icons to Heroicons
  • Fix italic/bold/strikethrough lost on save due to whitespace in markers
  • Add sticky toolbar navbar offset detection and morphdom resilience

0.1.0

  • Initial release
  • Dual-mode editor: visual (WYSIWYG) and markdown
  • Toolbar with formatting, headings, lists, links, code blocks
  • Content syncs between modes via Earmark
  • Optional gettext support for i18n
  • No npm dependencies — vendored JS bundle