All notable changes to Fresco are documented here. The format is based on Keep a Changelog and the project adheres to Semantic Versioning.
0.2.0 — 2026-05-15
Official, documented escape hatch to the underlying OpenSeadragon Viewer
instance. No breaking changes — handle.viewer (the original undocumented
name) remains supported as a back-compat alias. Bumped to a minor version
because the new field carries a public stability obligation (Fresco can no
longer freely rename or reshape the OSD viewer reference), not because the
code change itself is large — it's a one-line aliasing.
Added
handle.openSeadragon— official, documented access to the underlying OpenSeadragon Viewer for advanced consumers and layered packages. Use it for OSD APIs Fresco doesn't expose first-class: custom pan/zoom constraints (panHorizontal,minZoomImageRatio, …), raw OSD event handlers (canvas-double-click,canvas-key, …), OSD plugin registration, gesture rebinding. See the new "Advanced: OSD escape hatch" section inREADME.mdfor the stability contract — in particular, the rule that consumers reaching for it routinely should file an issue so common patterns can graduate to first-class Fresco APIs.
Notes
handle.viewer(the existing back-compat alias foropenSeadragon) remains supported indefinitely. Etcher already depends on this field across five call sites for image-space coordinate math — that's the in-tree consumer whose usage pattern motivated formalizing the contract. New code should preferhandle.openSeadragon; Etcher can migrate at its own pace.- No breaking changes; no behavior changes for existing consumers.
0.1.6 — 2026-05-15
Documentation + test polish patch. No changes to the rendered output
of Fresco.viewer/1 — every existing call site behaves exactly as
in 0.1.5. The goal is to make Fresco's genericity (works for any
Phoenix app, not just daisyUI consumers) more visible to a new
reader, and to backfill render-assertion tests for the attributes
added in 0.1.4 / 0.1.5.
Changed
- README: clarified that the daisyUI mapping for
theme={:inherit}is one example among many — any CSS custom properties or fixed colors work. Added a second bare-color example so readers don't infer that daisyUI is required. - README: surfaced the
theme={:system}dark-mode default that landed in 0.1.4 with a "Heads up" callout in the Theming section, so consumers upgrading from 0.1.3 aren't caught off guard by viewers rendering dark on dark-OS machines. - README: documented the
FrescoViewerhook name explicitly so consumers maintaining an explicit hooks map (rather than spreadingwindow.FrescoHooks) know what key to register. - README: promoted the first-source-only caveat for
handle.imageToScreen/screenToImageto a visible⚠️ Caveatcallout in the multi-image section, with one extra sentence on what extension authors should do until multi-image disambiguation ships. - README + viewer attr doc: rotation section now says "fifth button" (the row of four built-in buttons + a fifth opt-in rotation button) instead of "fifth icon".
priv/static/fresco.js: documented the rationale for pinning OpenSeadragon to4.1.0so future maintainers know the bump contract.
Tests
- Added render-assertion coverage for
:theme(all four values),:sources(multi-image JSON payload),:infinite_canvas(modifier class + data attribute),:rotate(data attribute), and theArgumentErrorguard that fires when neither:srcnor:sourcesis provided.
0.1.5 — 2026-05-15
One additive feature — a fourth :theme value, :inherit, that lets
the parent app drive Fresco's palette via the existing --fresco-*
CSS custom properties. Use it to wire the viewer to a parent theme
system (daisyUI, Tailwind, custom palettes) so background, dot grid,
and nav buttons follow the parent theme as it changes. Fully
backwards compatible — existing :system/:light/:dark viewers
behave exactly as in 0.1.4.
Added
- New
:inheritvalue onFresco.viewer's:themeattribute. When set, Fresco emitsdata-fresco-theme="inherit"on the host div and skips its own var declarations for that viewer — the six--fresco-*properties stay unset until the parent app's CSS defines them. Pair with a CSS rule on.fresco-viewer[data-fresco-theme="inherit"]mapping the variables to the parent's theme tokens. The structural styles (background-color + dot grid pattern) still apply; only the color values come from the parent.
Changed
- The base
.fresco-viewer { --fresco-bg: …; … }rule is now scoped to.fresco-viewer:not([data-fresco-theme="inherit"])so it doesn't fight the parent's vars. The@media (prefers-color-scheme: dark)branch picks up the same:not()exclusion. Visible only to consumers who passtheme={:inherit}; everything else stays the same.
0.1.4 — 2026-05-14
Three additive features — opt-in 90° rotation, multi-image canvas
layout, and light/dark/system theming. The API surface stays
backwards-compatible (all existing attrs unchanged, all new attrs
have defaults), but the new :theme defaults to :system, which
means viewers on dark-OS machines will now follow
prefers-color-scheme and render dark by default. Pass
theme={:light} to lock to the old always-light look.
Added
- New
:rotateattribute onFresco.viewer(defaults tofalse). Whentrue, appends a 90°-clockwise rotation button between the Fullscreen and Zoom-in icons. Rotation is tracked independently of zoom/pan — "Reset view" deliberately doesn't undo it. - New
:sourcesattribute for laying multiple images out on one canvas. Each entry is a%{src, x, y, width}map in viewport units; the first image conventionally anchors the layout atwidth: 1, sox: 1.1puts the next image just to the right. Heights derive from each image's natural aspect ratio. Each entry'ssrcruns through the same source-provider chain as:src, so plain images and DZI tile pyramids (via Tessera) can be mixed on a single viewer. Live re-renders that change the list re-open the viewer while preserving the current zoom/pan. :srcis now optional. At least one of:srcor:sourcesmust be given; the component raises otherwise. Existing single-image callers keep working unchanged.- New
:themeattribute —:system(default),:light, or:dark. Plumbed todata-fresco-themeon the host div.:systemfollows the OS viaprefers-color-scheme; the other two force a fixed palette regardless of OS preference. - Six CSS custom properties on
.fresco-viewerexpose the entire palette surface:--fresco-bg,--fresco-grid-dot,--fresco-nav-bg,--fresco-nav-bg-hover,--fresco-nav-fg,--fresco-nav-focus. Override them in user CSS to wire fresco to a parent theme system (daisyUI, Tailwind, custom palettes) — README has a daisyUI mapping example.
Changed
- Default viewer rendering follows
prefers-color-scheme(:themedefaults to:system). Viewers on dark-OS machines that previously rendered light will now render dark unless explicitly pinned viatheme={:light}or an inherited explicit theme. handle.imageToScreen/handle.screenToImagecontinue to operate on the first source when multiple are present. Multi-image coordinate disambiguation is planned but not yet implemented.
0.1.3 — 2026-05-14
Opt-in infinite-canvas mode + a default dot-grid background. No
breaking changes — every existing viewer keeps the stock clamped
behavior unless infinite_canvas is explicitly set, and the
grid is invisible by default (OSD's canvas paints over it).
Added
- New
:infinite_canvasattribute onFresco.viewer(defaults tofalse). Whentrue:visibilityRatiodrops to0andconstrainDuringPanflips tofalse, so the user can pan freely beyond the image edges.minZoomImageRatiolowers to0.05so the image can shrink to a thumbnail in the middle of a vast canvas.- The void around the image lights up with the dot-grid
background (see below); the host also picks up a
.fresco-viewer--infinitemodifier class for any infinite-only styling consumers want to add.
- Subtle 24×24px dot-grid background on every Fresco viewer (via
the new
.fresco-viewerbase class on the host div). Hidden by default because OSD's canvas paints over it; visible in the void wheninfinite_canvasis on, or behind transparent / padded images. Override.fresco-viewerin your own CSS for dark mode or a different accent. - Documented future API: a planned
:sourcesattribute will accept a list of[%{src: "...", offset: {x, y}}]for multiple images on the same canvas. The current:srcstays as the single-image shortcut — no migration when:sourcesships.
0.1.2 — 2026-05-14
Small UX + extension-API patch release. No breaking changes for existing consumers; the click-to-zoom default flip is documented below because it's user-visible.
Added
handle.appendNavButton(...)'s returned remover now carries.setIcon(svgString),.setTitle(text), and.el(the underlying<button>element). Extensions can mutate a button after creation without re-adding it (which would reshuffle its position in the nav column). Used by Etcher 0.2's visibility toggle to flip eye ↔ eye-slash.
Changed
- Mouse single-click no longer zooms.
gestureSettingsMouse.clickToZoomdefaults tofalse;dblClickToZoom, scroll-to-zoom, and pinch-to-zoom on touch are unchanged. Single clicks now reliably pass through to overlays that want them (e.g. annotation selection) instead of fighting OSD's built-in click-to-zoom.
0.1.1 — 2026-05-12
Small additive release for layered libraries. No breaking changes.
Added
handle.appendNavButton(svg, title, onClick)— extensions append a button to the same.fresco-navflexbox column that holds the built-in zoom-in / zoom-out / reset / fullscreen. Returns an unsubscribe function that removes the button on cleanup. Used by Etcher to add a pencil button that toggles annotation mode.animationandupdate-viewportevents bridged on the viewer handle (handle.on("animation", fn)). The existingzoom/panevents only fire on the intent of an input; the new ones fire on every spring-interpolated frame so overlays glide with the image instead of jumping at endpoints.
Changed
<Fresco.viewer>now setsphx-update="ignore"on its host div. Without it, LiveView morphdom patches walk the viewer's children on every render and wipe OSD's runtime-added canvas + extension overlays. The hook still receivesupdatedcallbacks for attribute changes (e.g.data-srcswaps continue to work) —phx-updateprotects children only.- Nav column reordered top-to-bottom: fullscreen → zoom-in → zoom-out
→ reset. Extensions appending via
handle.appendNavButtonland at the bottom of the column.
0.1.0 — 2026-05-12
Initial release. Polished pan-zoom image viewer for Phoenix apps, with a deliberate extension surface for layered libraries.
Built-in viewer
<Fresco.viewer id src class>LiveView function component- Pan: click-drag, touch-drag, keyboard arrows
- Zoom: mouse wheel, pinch, double-click, dedicated buttons,
+/-keys - Fit-to-view initial state regardless of image / container aspect ratio
- Heroicons nav overlay at top-left: zoom-in, zoom-out, reset, fullscreen
- Viewport clamped so the image can't be panned off-screen
(
visibilityRatio: 1.0,constrainDuringPan: true) - Smooth animations tuned for snappy responsiveness
(
animationTime: 0.3,springStiffness: 10) - Browser fullscreen mode
Extension surface
window.Fresco.viewerFor(domId)— synchronous lookup of a live viewer handlewindow.Fresco.onViewerReady(domId, callback)— async-safe lookup that fires the callback as soon as the viewer is ready (handles mount-order races when an extension hook mounts before its host viewer)window.Fresco.registerSourceProvider(predicate, factory)— registers a predicate-matched URL transformer; first registered provider that matches wins, falling back to a default plain-image provider- Viewer handle exposes:
imageToScreen/screenToImage,getViewportBounds,fitBounds,setSource,swapSourcePreservingBounds, andon(event, handler)forzoom/pan/open/resizeevents
JS engine
- OpenSeadragon ~> 4.1 lazy-loaded from jsDelivr on first mount
- One bundled JS file (
priv/static/fresco.js); no npm dep, no build step in consumer apps - Heroicons SVGs inlined; no PNG sprite dance against a CDN
Requirements
phoenix_live_view ~> 1.1,phoenix_html ~> 4.0,jason ~> 1.4