All notable changes to Etcher are documented here. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
[0.2.7] — 2026-05-15
Documentation + comment cleanup release. No runtime behavior changes — every existing call site behaves exactly as in 0.2.6. The goal is to make Etcher visibly decoupled from any specific consumer so that a third-party Phoenix dev reading the source or docs sees a clean, drop-in library rather than an obvious satellite.
Changed
Etcher.Storagemoduledoc: replaced the "PhoenixKit, for example" paragraph with a generic "consumer that pairs every annotation with a comment thread" example. The behaviour itself is unchanged — only the explanatory prose.lib/etcher.exmoduledoc: corrected the install snippet ({:fresco, "~> 0.1"}, {:etcher, "~> 0.2"}— the old version pins pointed at non-existent fresco 0.2 / outdated etcher 0.1) and expanded the shape list to includecallout, text, dimensionrather than only the original four.Etcher.Layermoduledoc: tools example and "Tools" section now include:eraser, matching the actual default. Added one line explaining how to opt out.priv/static/etcher.js: four inline comments that named PhoenixKit / PhoenixKitComments now describe the generic contract instead (any element withdata-annotation-uuidfor cross-component highlight; "consumer's annotation-creation UI" / "host apps" for extension-point comments). The contracts themselves were already generic — only the prose changed.README.md: corrected the Installation version pins, expanded the bottom-toolbar ASCII diagram to show all eight buttons, replaced "the four drawing tools" with "seven drawing tools (rectangle, circle, polygon, freehand, callout, text, dimension) plus an eraser," documented theEtcherLayerhook name for explicit hook-map wiring, and rewrote the Out-of-scope section to drop a stale "v0.1 is draw-and-commit" claim (editing has been supported for several releases) and the "four built-ins" reference.CHANGELOG.md: reworded the 0.2.6 entry to drop the two PhoenixKit name-drops; the same fix applies to any consumer that opens its own composer popup onetcher:created.
[0.2.6] — 2026-05-15
Single fix to the dimension-creation flow so consumer apps that open
their own composer popup on etcher:created can attach a comment to
a freshly-drawn dimension without fighting an auto-opened inline
editor.
Fixed
- Dimension creation no longer auto-opens the inline label editor.
0.2.5 fired
_startTextEditin the_finalizeShapeafterCreate for dimensions (mirroring the callout flow), which stacked a foreignObject input over the label position. Consumers that pop a separate composer popup onetcher:created(for setting an annotation title + comment in one flow) lost the composer behind the inline editor — users would dismiss the composer they hadn't noticed and lose the comment, sometimes the whole shape (such composers typically treat cancel as "discard the annotation"). Dimensions now spawn empty; the consumer's UI sets the label via whatever path it normally uses for non-text shapes (typically theetcher:created→ composer-popup →etcher:updatedchain). Re- editing the label after creation still works via double-click on the dimension — that path was wired in 0.2.5 and is unchanged.
[0.2.5] — 2026-05-15
New annotation kind — dimension — for measurement-style labeling.
A horizontal-or-angled shaft with V-arrows on both ends and a
black, slidable label. Arrow color follows the active swatch; label
stays black with a white halo so it's legible on any color.
Added
- Dimension tool (
kind: "dimension"). Two endpoints (geometry: {a: [x,y], b: [x,y]}); label text + position along the shaft live inmetadata.titleandmetadata.title_offset(0–1, default 0.5 = midpoint). Drawn either by click-drag (commit on release) or by two-click rubberband (first click locks endpoint A, the line follows the cursor, second click commits endpoint B). After commit, drops straight into inline-edit mode for the label. - Slidable label — in cursor mode, click and drag the label to
slide it along the shaft. Persists as
metadata.title_offset. - Endpoint corner handles + body-drag translate, double-click to re-edit the label, eraser supports the new kind.
Etcher.Annotation's@kindswidened to include"callout","text", and"dimension"(the schema docs were also out of date for callout/text — fixed in passing).
Changed
- The bundled
Etcher.Annotationschema now accepts the same kinds the JS toolbar exposes. Consumers using the bundled storage need no migration if their CHECK constraint was already widened for callout/text — add'dimension'to the same allow-list. - Inline text editor input pinned to black so the typed text stays readable on the white-ish input background regardless of the shape's stroke color (light pastels were nearly invisible).
[0.2.4] — 2026-05-15
Cross-browser fixes + a callout stability sweep — mostly fallout from the 0.2.3 title fix not being symmetric across the rendering paths.
Fixed
- Title / callout / text-shape labels render at the same vertical
position in Firefox as in Safari. The text elements were created
with
dominant-baseline: hanging, which Safari and Firefox interpret differently for Latin text — Safari renders glyphs ABOVE the hanging baseline while Firefox renders them BELOW per spec. Combined with the wrap helper'sdy="1em"on the first tspan, callout text rendered correctly in Safari but floated below the rect in Firefox. All three render paths (_renderTitleSibling, thecase "text"branch, and the callout branch in_renderShape) now overridedominant-baselinetoalphabeticon the text element each render. Both browsers honor alphabetic identically: the alphabetic baseline lands attext.y + 1em, putting the text cleanly inside the rect with the underline / leader attaching at the rect's bottom edge. - Callouts no longer grow exponentially when text overflows. The
width-fit font cap that 0.2.3 added to
_renderTitleSiblingwas missing from the callout render path, so a long callout label triggered the same multi-line wrap → grow → wider font → more wraps feedback loop the title fix originally addressed. Same cap applied to callouts. - Click on a title or callout/text-shape handle no longer triggers
a phantom resize.
_startTitleHandleDragand_startHandleDragunconditionally firedetcher:updatedand snapped geometry to the rendered (shrunk) box onpointerup, even when the user just clicked without dragging. Both now use a 3-px screen-space dead zone (matching the body-drag and title-drag handlers) so a bare click is a no-op. - Callout corner drags no longer shrink the box on every
interaction. Drag math used
_renderedBox(shrink-fit visual) as the start reference, so each drag computed a new geometry of(visual + delta)instead of(geometry + delta). With shrink-fit on, that baked the shrink offset back into storage every drag — the callout visibly shrunk a bit each time, then converged. Drag math now uses pointer DELTA (pt - startPt) against the fullgeometry.text_box, so dragging a handle by Δpx grows or shrinks geometry by exactly Δpx; the visual continues to shrink-fit independently. Anchor drag (idx 0) still uses absolute pt — no visual/storage offset there. - The
_startHandleDragonUpsnap-to-_renderedBoxis skipped for callouts (delta math already keeps geometry consistent with the visible drag). Text shapes still snap on release — same 0.2.x behavior, no regression.
[0.2.3] — 2026-05-14
Single bug fix — stops the runaway growth of shape titles on drag / click. No API change, no behavior change for code that doesn't hit the bug.
Fixed
- Shape title text no longer balloons on every interaction. When
a title's content overflowed the default box width at the
height-derived font-size,
_fillTextWithWrappedTspanswrapped it onto multiple lines.actualH = measured.height + pad·2then exceeded the inputth, that taller height got persisted back intometadata.title_boxon release, the next render derived an even larger font, more lines wrapped, and the title grew exponentially per interaction (title_box.hgoing 22 → 54 → 273 → … in three drags)._renderTitleSiblingnow caps the font-size so the title fits the box width on a single line (floor of 10 px), boundingactualHto one line of text. The shrink-to-text rendering + handle-drag commit are unchanged; with the cap in place the system has a fixed point instead of a feedback loop.
0.2.2 — 2026-05-14
Follow-up patch to 0.2.1: restore body-grab on the editing shape,
keep the tooltip from blocking the satellite title label, and make
edit-mode survive a click on a sibling shape now that shapes are
pointer-events: none.
Fixed
- Body-grab restored on the editing shape. With 0.2.1 flipping
every shape to
pointer-events: none, the click-drag-the-body- to-move-the-shape gesture stopped firing because the shape's own pointerdown listener no longer saw any events. The currently-edit-mode shape now re-enablespointer-events: visiblePaintedvia a.etcher-shape.is-editingrule — only THAT shape catches its own pointerdown; the rest of the shapes stay invisible to events so pan/zoom still passes through them. - Tooltip no longer covers the title satellite. When the cursor was over a shape's movable title label, the hover tooltip rendered above the parent shape — directly on top of the title the user was trying to grab. The doc-level hover hit-test now detects when the cursor is inside a title's bbox and suppresses the tooltip for that hit; hover styling on the parent shape stays applied so it's still clear which annotation is targeted.
- Edit-mode and tooltip-pin survive a click on a sibling shape.
Both outside-click handlers (the edit-mode tear-down and the
tooltip-pin tear-down) used
e.target.closest(".etcher-shape")to detect "is this click on a shape?" — but since 0.2.1 shapes arepointer-events: none, the click's DOM target is OSD's canvas, not the shape. The handlers now fall back to an image-px hit-test via_shapeAt(pt)so clicking a different shape switches edit mode or the pin instead of tearing down to empty.
Internal
_setHoveredShape/2(was /1) gains anonTitleflag.- New helper
_pointOnTitleOf/2for the title-bbox hit-test.
0.2.1 — 2026-05-14
Patch release: pan / zoom now work over shapes.
Fixed
- Scroll-wheel zoom and click-drag pan on the underlying viewer
stopped working whenever the cursor was over an annotation
(rectangle, circle, polygon, freehand, callout, text). Root
cause: shapes had
pointer-events: visiblePaintedso they caught wheel + pointerdown before OSD's MouseTracker on the canvas sibling could see them — pointer events bubble UP the DOM, not sideways. Shapes are nowpointer-events: none, and hover + click are re-detected at the document level via image-px hit-testing (reuses the eraser's per-kind point-in- shape check). Hover styling, tooltips, click-to-pin, click-to- edit, and dblclick-inline-edit all continue to work; pan and zoom now pass through every annotation cleanly.
Internal
- Renamed
_eraserHit/2to a shared_shapeContainsPoint/2helper. The eraser keeps a thin alias for readability at its call sites. - New helpers:
_shapeAt/1(topmost-shape lookup),_onShapeTap/1(shared tap-handling entry),_wireGlobalShapeListeners/0+_unwireGlobalShapeListeners/0,_setHoveredShape/1. - Tap-vs-drag disambiguation with a 5px dead-zone keeps a quick click-without-drag firing the shape's selection / pin / edit flow, while any drag-with-movement passes through to OSD's pan unchanged.
0.2.0 — 2026-05-14
A backwards-compatible second release: two new shape kinds, an eraser tool, undo/redo with full history, satellite titles, edge-resize grabbers, polygon midpoint insertion, a visibility toggle, and a complete programmatic API so consumers can drive the layer without rendering its built-in toolbar.
Added
- Callout tool (
kind: "callout") — blueprint-style leader-line annotation: an anchor dot pointing at the image, a thin line to a resizable text bbox (with a horizontal underline spanning the bbox bottom). Text inside scales to fit the bbox. - Text tool (
kind: "text") — freestanding text label drawn as a click-drag bbox. Inline editor (<foreignObject>+<input>) opens on commit and on double-click for re-edit. Font scales with bbox height; bbox shrink-wraps to the text on release. - Eraser tool — press-and-drag wipes shapes by sweep. Each shape
the cursor crosses dims (
.is-erasing) for preview; release flushes them all as a single compound delete. Idle hover (eraser selected, no button held) previews the single shape under the cursor. - Optional title field per annotation — every kind can carry a
short label (
title varchar(200)). On rect/circle/polygon/freehand the title renders as a movable, resizable satellite group with a dashed leader line back to the parent's nearest perimeter point (leader auto-hides when the title is inside the parent). On callouts the title is the in-bbox content. Drag to move (persisted asmetadata.title_box), 4 corner handles to resize, double-click to inline-edit. - Edge-midpoint resize grabbers on rectangles — small rounded
rect handles on each side; drag a side to slide one edge while
the opposite edge stays anchored. Distinct visual +
ns-resize/ew-resizecursors so they don't conflate with polygon midpoints. - Polygon midpoint vertex insertion — every polygon edge now carries a "ghost" midpoint handle that lights up when the cursor is near. Grab it to insert a new vertex at that midpoint and place it via a vertex-style drag.
- Undo / Redo — toolbar buttons + ⌘Z / ⌘⇧Z / Ctrl+Y keyboard
shortcuts. 50-op session history covers geometry, style, metadata,
title text, and deletes (including bulk-delete from the eraser).
Delete recreation honors a new
restore: trueflag so the consumer can suppress its create-time UI (e.g. comment composer) on undo. - Visibility toggle — eye / eye-slash button above the pencil in Fresco's nav column. Hides/shows the entire SVG overlay with one click.
- Color picker — bottom-toolbar swatches; persisted as
style.coloron each annotation; vertex + title handles inherit the shape's color viacurrentColor(no more always-orange dots). Override the palette viawindow.Etcher.colorSwatches; initial color viawindow.Etcher.defaultColor. - Tooltip slot extension API —
window.Etcher.tooltipSlots = { header, body, footer }lets consumers replace the tooltip content per-slot while keeping the wrapper (trash button, pin/unpin, hover bridge) under Etcher's control. Default slots read genericmetadata.{title,body,subtitle}keys. - Complete programmatic control surface on
window.Etcher.layerFor(frescoId)so every built-in button is callable from outside. Methods grouped by mode, visibility, tool, color, history, and shape selection/edit. Consumers can render their own toolbar and drive the layer headlessly. - CustomEvents for state changes:
etcher:mode-changed,etcher:tool-changed,etcher:color-changed,etcher:visibility-changed,etcher:history-changed,etcher:tooltip-show / -hide / -pin / -unpin. - Restored comment threads on undo-of-delete — the etcher:created
payload for a restore carries
restore_from_uuidso consumers can re-link soft-deleted child rows (e.g. comments) to the new uuid the server assigns to the recreated annotation. appendNavButtonmutable handle (Fresco 0.1.2+) — Etcher's nav buttons can now update their icon / title in place (used by the visibility toggle to flip eye ↔ eye-slash).
Changed
- Default
:toolslist onEtcher.Layer.layer/1is now[:rectangle, :circle, :polygon, :freehand, :callout, :text, :eraser](all seven). Pass an explicit list to subset. - Text + title + callout bboxes shrink-wrap to the rendered text on every render; the stored geometry is rewritten to the shrunk dimensions on release so storage always matches what's visible.
- Vertex handles now inherit the shape's color (
currentColor) instead of hard-coded orange. CSS hover / drag fills usefill-opacityso they tint correctly with whichever color the shape carries. - Single-shape deletes now flow through the same compound
bulk_deleteundo op the eraser uses, so the tooltip trash button also gets redo support.
Fixed
- Tooltip stops hijacking hover state on a different shape while another shape's tooltip is pinned.
- Pinned shape keeps its
.is-selectedoutline when the cursor leaves it (was getting stuck visually deselected). - Tooltip
.is-hoveredno longer sticks after the cursor leaves a pinned shape.
0.1.0 — 2026-05-06
Initial release.
Added
Etcher.LayerPhoenix LiveView function component — attaches an annotation overlay to a named Fresco viewer and adds a pencil button to its nav column.Etcher.Storagebehaviour — pluggable storage adapter contract with four callbacks (create/1,list_for/2,update/2,delete/1).Etcher.Storage.Default— bundled implementation backed by theetcher_annotationstable. Reads the consumer's Repo fromconfig :etcher, repo: ….Etcher.AnnotationEcto schema for the bundled table (UUIDv7 primary key,target_type/target_uuid, four geometry kinds: rectangle, circle, polygon, freehand).mix etcher.gen.migration— generates theetcher_annotationstable migration into the consumer'spriv/repo/migrations/.- JS engine at
priv/static/etcher.js— registers theEtcherLayerLiveView hook, draws shapes as SVG overlays anchored to image coordinates, emitsetcher:created/:updated/:deleted/:selectedevents. - Bottom drawing toolbar with rectangle / circle / polygon / freehand
tools; pencil-button toggle integrated with Fresco's nav column via
handle.appendNavButton/3(Fresco 0.2+).