Skip to content

Image Frames ​

An image frame is an image slot that more than one dialog writes. A location image that can be a generated map, a satellite view or a picked photo, with a crop or an annotation on top, is one frame.

Declare the slot once with $uhuu.editFrame. The editor shows one chooser for it and saves every dialog into the same value, so:

  • the image saved last is the one that shows;
  • each source reopens with its own saved state, for example the map with its places;
  • crops and annotations belong to the source they were made on: a photo picked over an annotated map does not carry the map's annotation, and the map keeps it.

Use a frame when two or more source dialogs write the same slot, such as a map and a picked photo. An image that only the image dialog writes, with or without an annotation, does not need one.

What the User Sees ​

Clicking the slot opens one short chooser. It shows no preview: the page behind it is the preview.

  • Show: one row per source, named by the editor in the user's language (Map, Satellite image, Photo…). A radio marks the source that shows; choosing another row shows it at once, without opening a dialog. Edit on a row opens that source's dialog on its own saved state. A source not made yet shows a plus and Create (Choose for a photo).
  • Edit the image: the transforms and layers that work on what shows: crop, AI enhance, annotate. Under Annotate, a note appears when the image changed after the annotation was drawn, with Review, Keep and Remove.
  • Undo and Done. Choices apply as they are made, so Done only closes.

Inside a source dialog, Save can continue straight to a transform or a layer ("Save and continue: Annotate"), so a map can be made, cropped and annotated without returning to the document.

Parameters ​

$uhuu.editFrame(declaration) takes one object:

  • path (string, required): Payload path of the slot.
  • sources (array, required): The dialogs that create the image, in the order the chooser lists them. Each entry has a type (map, satellite, cadastral, search-image or image) and the options that dialog takes, such as its config, or imageGalleryPath and galleryUpload for image. Leave out path, state and value: the frame sets them. An image source may name an imagePath: frames ignore it, and an editor without frames then writes a pick to that key (default contentUrl), so set it when the page reads another key, such as url. title is optional: the editor names the dialogs it knows in the user's language, and uses title only for another type and in the type list of an editor without frame support.
  • layers (array, optional): Overlays the page draws over the image, never merged into it: { type: 'annotation', config }. Default: none.
  • transforms (array or false, optional): Edits applied to the shown image: 'image-editor' (crop and edit) and 'ai-marketing-studio' (AI enhance, where the team has it). Default: every transform the editor offers. false offers none.
  • ratio (number, optional): The slot's proportions (width / height), taken from the authored frame box as for the image dialog. The chooser previews the slot at this ratio, the crop keeps it, and a map, satellite or cadastral source exports at it unless its config sets its own.

A new template needs nothing more. The next three parameters only matter for documents that were saved before the slot became a frame; see Values saved before frames and skip them otherwise:

  • legacyPrecedence ('generated' or 'pick', optional): Which image an old value shows when it holds both a generated image and a picked one. See Values saved before frames.
  • legacyDataImage ('show' or 'ignore', optional): Whether an old map, satellite or cadastral value whose upload failed shows its unsaved render ('show', the default) or nothing, as a template that showed a placeholder did ('ignore').
  • annotationPath (string, optional): Where the template saved its annotation before it used a frame. See Values saved before frames.

Example Usage ​

The smallest frame: a location image that is a generated map or a picked photo. The editor offers its crop on top; nothing else is needed.

js
$uhuu.editFrame({
  path: 'location.image',
  sources: [
    { type: 'map', config: { lat: 47.3769, lng: 8.5417, design: true } },
    { type: 'image', imageGalleryPath: 'gallery' }
  ],
  ratio: 4 / 3
});

A fuller slot that offers a map, a satellite view and a picked photo, with crop, AI enhance and annotation on top, in a template whose documents predate the frame:

js
// Opens the location slot. `location` is payload.location.
const editLocationFrame = (location = {}) => {
  const center = { lat: location.lat ?? 47.3769, lng: location.lng ?? 8.5417 };
  const ratio = 1280 / 1468; // the authored frame box, width / height

  $uhuu.editFrame({
    path: 'location.image',
    sources: [
      {
        type: 'map',
        // `design: true`: the map dialog exports an image only in design mode.
        config: { ...center, design: true, types: ['train_station', 'school', 'supermarket'] }
      },
      { type: 'satellite', config: { ...center, zoom: 18 } },
      { type: 'image', imageGalleryPath: 'gallery', galleryUpload: true }
    ],
    layers: [
      { type: 'annotation', config: { visualGallery: ['https://cdn.example.com/icons/school.svg'] } }
    ],
    transforms: ['image-editor', 'ai-marketing-studio'],
    ratio,                                // also the map's and satellite's export ratio
    legacyPrecedence: 'pick',             // old documents: a picked photo wins over a saved map
    annotationPath: 'locationAnnotation'  // where the annotation was saved before the frame
  });
};

An entry carries only its config and that dialog's own options. The editor hands each source its saved state, and hands crops and annotations the image that shows. In a frame, the image source's gallery also lists the images the document's frames already hold, such as a map render or a crop, under "From this document".

Opening a Frame ​

Pick the one way that matches how the slot is bound:

The slot isOpen the frame with
Your own element (<div data-uhuu onClick={…}>)$uhuu.editFrame(declaration) in the click handler, with event.stopPropagation() as for any dialog.
An ImageBlockdialog={{ path: declaration.path, frame: declaration }}. The SDK completes that message, so the block needs nothing else.

Both work in every editor: one without frame support gets the sources as a classic type list (see Compatibility), and the template carries no fallback of its own. A FrameBlock component that renders and opens a frame in one tag ships with the next uhuu-components release (0.5.0).

What the Editor Stores ​

The value at path is a uhuu.frame.v1 object. Its top level mirrors the source that shows, in the shape that source saved before frames; uhuuFrame is the editor's record of every source (abridged):

json
{
  "image": "https://cdn.uhuu.io/…/map-cropped.png",
  "url": "https://cdn.uhuu.io/…/map-cropped.png",
  "list": [],
  "home": {},
  "mapbox": {},
  "uhuuFrame": {
    "schema": "uhuu.frame.v1",
    "active": "map",
    "sources": {
      "map": {
        "url": "https://cdn.uhuu.io/…/map.png",
        "urlKey": "image",
        "state": { "list": [], "home": {}, "mapbox": {} },
        "steps": [
          { "type": "image-editor", "url": "https://cdn.uhuu.io/…/map-cropped.png" }
        ],
        "layers": {
          "annotation": {
            "state": { "schema": "uhuu.annotation.v1", "annotations": [], "annotationSvg": "<svg>…</svg>" },
            "baseUrl": "https://cdn.uhuu.io/…/map-cropped.png"
          }
        }
      },
      "image": { "url": "https://cdn.uhuu.io/…/photo.jpg", "urlKey": "contentUrl" }
    },
    "previous": { "active": "map", "type": "map", "before": {} }
  }
}
  • active: The source that shows.
  • sources: One entry per source that has been saved: its image (url, and urlKey, the key its dialog used), the state it reopens with, its steps and its layers.
  • steps: Transforms applied to that source's image, oldest first. The last step is what shows. Saving the source again with a new image clears them.
  • layers: Overlays on that source, each with the baseUrl it was drawn on. A layer gets stale: true when the image under it changes.
  • previous: One level of undo.

The shown image is the last step of the active source, or that source's own image. The editor rebuilds the top level from the record on every save. Dialogs can only save uploaded images into a frame, never data: URLs.

Which keys a page may read:

KeyHoldsOn the page
urlThe image to showRead freely. The editor sets it whenever the frame shows an image.
contentUrlThe same URL, only while a picked image showsRead it before url (an editor without frames patches it when it picks an image). A map never has one, so "a string contentUrl means a pick" stays true.
imageThe same URL, only while a map, satellite or cadastral source showsDo not depend on it. It keeps pages written before frames working.
width, heightThat image's size, when knownRead freely.
annotationSvgThe overlay to draw over the image, while the shown source has an annotationRead freely: inline it over the image like any annotation SVG, or hand it to ImageBlock's overlaySvg. Absent when there is nothing to draw; the cadastral dialog's parcel outline is baked into its image and never appears here.
Other top-level keysThe active source's own state, such as the map's list and homeRead them for that source only. They change when another source shows.
uhuuFrameThe editor's recordRead it through the helpers, not directly.

In a schema-bound template, type the slot as ImageFrame from the common schema (https://schemas.uhuu.io/standard/common/0.2.0/schema.json#/$defs/ImageFrame). It reads like an ImageObject and allows the shown source's own keys and uhuuFrame, which ImageObject's additionalProperties: false rejects.

Templates never write a frame. Do not build or patch the value in the template, for example with emitPayload: the editor rebuilds it from uhuuFrame on every save, so a template change is overwritten, or read as a new pick when it changes contentUrl. A plain image in sample data is fine; it reads as a picked image.

Reading a Frame ​

A page needs no helper. Read two keys from the value at path:

  • The image: value?.contentUrl || value?.url, in that order (imageUrl(value) from any uhuu-components version reads the same). The editor sets url whenever the frame shows an image; an editor without frame support patches only contentUrl when it picks an image.
  • The overlay: value?.annotationSvg, when there is one. Pass it as ImageBlock's overlaySvg, or inline it over the image.
jsx
import { ImageBlock, imageUrl } from 'uhuu-components';

const locationFrame = {
  path: 'location.image',
  sources: [{ type: 'map', config: mapConfig }, { type: 'image', imageGalleryPath: 'gallery' }],
  layers: [{ type: 'annotation' }],
  ratio: 314 / 156
};

export default ({ payload }) => {
  const value = payload.location?.image;
  return (
    <ImageBlock
      src={imageUrl(value)}
      overlaySvg={value?.annotationSvg}
      dialog={{ path: locationFrame.path, frame: locationFrame }}
      className="absolute left-[18mm] top-[207mm] h-[156mm] w-[314mm]"
    />
  );
};

The annotation SVG is exported at the image's pixel size. Inlined by hand, size the <svg> to the box (width="100%", height="100%") and, when the image uses object-fit: cover, set preserveAspectRatio="xMidYMid slice", so pins stay where they were placed. ImageBlock does this for overlaySvg.

A template that declared annotationPath can keep drawing the overlay from that path instead: the editor writes the shown source's annotation back there (see below).

Frame helpers (uhuu-components 0.5.0) ​

The next uhuu-components release adds helpers for pages that need more than the shown image, such as which source shows or a legacy value's precedence: frameImage, frameOverlays, frameActive, frameSourceView and frameOptionsOf(declaration, payload), plus FrameBlock. Until it is published, the two keys above are all a page needs.

Values Saved Before Frames ​

Documents saved before a template used a frame keep rendering without a migration. The editor and the helpers read an old value as a frame in memory, and the first save in the slot stores it as one:

Saved valueRead as
A URL string, or { contentUrl }, { url }, { src }the image source; other keys become its state
The map's { list, home, mapbox, image }the map source
{ image, satellite }the satellite source
{ image, cadastral, annotationSvg }the cadastral source; annotationSvg stays in its state (the outline is already in the image)
A map, satellite or cadastral value whose upload failed (dataImage, a data: URL)that source, showing the unsaved render; legacyDataImage: 'ignore' shows nothing instead, for templates that showed a placeholder
One of the three above plus a picked contentUrlboth sources; legacyPrecedence decides which one shows
A bare { image }the first map, satellite or cadastral entry in sources (map when there is none)
An annotation at its own paththe shown source's annotation, when annotationPath names that path

legacyPrecedence: 'generated' (the default) shows the map, satellite or cadastral image; 'pick' shows the picked contentUrl. Match what the template rendered before: a helper that read contentUrl first needs 'pick', one that read image first keeps the default. It only affects values the frame has not saved yet.

annotationPath: The path where the template saved its annotation before. The editor reads that annotation into the frame and, after each save, writes the shown source's annotation back to the path ({} when it has none), so pages that still read the path keep working.

Migrating a types Chooser ​

A slot wired by hand opens a types chooser in which every dialog writes the same path in its own shape. The template then has to decide what shows and keep the dialogs from undoing each other:

js
// Before: the template owns the slot.
const mapImageSrc = (value) => value?.contentUrl || value?.image || value?.dataImage || '';

const withoutPick = (value) => {
  if (!value || typeof value !== 'object') return value;
  const { contentUrl, ...rest } = value;
  return rest;
};

const mapDialog = (saved, annotation) => {
  const image = mapImageSrc(saved);
  $uhuu.editDialog({
    types: [
      { path: 'location.image', type: 'map', title: 'Create Map', state: withoutPick(saved), config: mapConfig },
      {
        path: 'location.image', type: 'image', title: 'Choose Image', imagePath: 'contentUrl',
        imageGalleryPath: 'gallery', galleryUpload: true, value: image
      },
      ...(image
        ? [{ path: 'locationAnnotation', type: 'annotation', title: 'Annotate', image, config: { visualGallery }, value: annotation }]
        : [])
    ]
  });
};
  • mapImageSrc decides which key wins: a picked contentUrl or the map's image.
  • withoutPick strips the pick before the map opens, because a stale contentUrl would otherwise outlive a new map.
  • The annotation lives at a second path, and the template passes it the image itself.

One editFrame call replaces all of it:

js
// After: the editor owns the slot.
const editLocationFrame = () => $uhuu.editFrame({
  path: 'location.image',
  sources: [
    { type: 'map', config: mapConfig },
    { type: 'image', imageGalleryPath: 'gallery', galleryUpload: true }
  ],
  layers: [{ type: 'annotation', config: { visualGallery } }],
  legacyPrecedence: 'pick',            // mapImageSrc read contentUrl first
  annotationPath: 'locationAnnotation' // where the annotation was saved
});

Then:

  1. Keep mapImageSrc if it reads contentUrl before image, or replace it with value.contentUrl || value.url. Draw value.annotationSvg (or keep reading annotationPath) instead of your own overlay wiring.
  2. Delete withoutPick. Each source opens on its own saved entry only.
  3. Stop passing path, state, value, image and imagePath to the entries. The editor fills them from the frame. title can go too: the editor names known dialogs itself.
  4. Keep each source's config (coordinates, styles, zoom). It is all an entry still needs.

Compatibility ​

editFrame also sends the sources as a classic types list. An editor without frame support ignores the frame and shows that list as its type chooser, so the template keeps working there:

  • Each source saves as it always has. The map, satellite and cadastral dialogs replace the value at path. The image source is sent with imagePath: 'contentUrl', so a string entry or a conversation upload patches only contentUrl, and an object entry replaces the value.
  • The helpers read those saves: a replaced value reads as that source, and a contentUrl that differs from the frame's own image reads as the newest pick.
  • Such an editor cannot keep the other sources. A save that replaces the value there, such as a new map or an object from the gallery, leaves only that source. The next save in an editor with frame support stores a frame again.
  • Layers are not part of the types list, so only editors with frame support offer the annotation.

Public developer documentation for Uhuu.