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>(withstart="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
keydownand 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 throughhtmlToMarkdown, 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 → hybridandhtml → hybridmode switches losing the latest edits._syncModesonly matchedto === "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
_updateCountshad 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
:heightas 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 computedfont-weightis bold, and the editor's own CSS setsfont-weight: 700onh1–h4, 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).
mousedownis intercepted on the editor wrapper andpreventDefaultkeeps 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'sdata-has-uploadattribute now correctly reflects whetherupload_handleris set (it previously misreported truthy whenever:imagewas in the toolbar list). Result: with:imagein 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_closedevents 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 tighterline-height: 1, and the dropdown wrappers for heading, more-formatting, table, and more-inserts now useinline-flexso 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 aftermounted()runs. - Force Shift+Enter to always insert a soft break (
<br>) inside the current block. Some browsers — notably Chromium contenteditables that havedefaultParagraphSeparatorset 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'sbreaks: trueHTML output puts a literal\nafter every<br>as pretty-print whitespace, whichhtmlToMarkdownwas reading and combining with the<br>'s own\ninto\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 markeddata-leaf-fillerand 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: truepassed 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: 0so the editor's intrinsic min-content width can no longer push past its parent's allocated width. Pages with aflex-[2] / flex-1split (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_presetdefaults to:random, drawing from the bundled set of:unpuzzling,:brewing,:polishing,:composing,:crafting,:tidying). Useloading_preset={:default}for the plain "Loading…" label, orloading_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-stateto"ready"in the JS hook'supdated()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/1attrs:loading_preset(:default | :unpuzzling | :brewing | :polishing | :composing | :crafting | :tidying) — pick a bundled labelloading_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/1function 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