# Configure widgets

The widget catalog delivers a complete JSON Schema of its configuration for every widget: each field with a heading and an explanation, the required fields named, and at least one ready-made example configuration. Anyone who builds a scene without the editor reads the rules from it instead of guessing them — and checks their configuration against it before writing it.

> **Type:** guide
> **For:** Anyone who creates or changes scenes through the API — a tool of your own, an assistant, an import.
> **Prerequisite:** a credential from your organization for `GET /v1/widget-catalog` and a JSON Schema validator that understands Draft 2020-12.

## Fetch the catalog

```bash
curl -sS "$SZENERA_API/v1/widget-catalog" \
  -H "Authorization: Bearer $TOKEN" | jq '.entries | length'
```

The response carries one entry per widget under `entries`:

| Field | Meaning |
|---|---|
| `appId` | The app that the widget belongs to, or `core` for the six built-in building blocks. |
| `version` | The version of the app that the definition comes from. Fixed at `0.0.0` for the built-in building blocks. |
| `definition.id` | The widget type. This exact value later appears in `widgetType` of the widget instance. |
| `definition.name`, `definition.description` | Name and short text, as the selection in the editor also shows them. |
| `definition.configSchema` | The JSON Schema of the configuration — the subject of this page. |
| `definition.size.min` | The smallest frame, in reference pixels of a 1920 × 1080 scene, in which the widget still shows something useful. |
| `definition.preview.defaultConfig` | The configuration with which a freshly inserted instance starts. |
| `definition.preview.variants` | The presentations that the widget supports, each with a partial configuration and, as a template, a starting size (`size`). |
| `definition.preview.states` | Named sample payloads for the preview in the editor (rain, night, no data). Of no significance for a configuration. |

The catalog mirrors the latest released state of an app. An organization that has installed an older version may differ slightly from it, and the version that counts is then the one in the instance’s `appVersionPin`.

## What is in the `configSchema`

The `configSchema` is a JSON Schema document according to Draft 2020-12. It describes exactly the object that later appears as `config` in the widget instance.

| Key | Meaning |
|---|---|
| `$schema` | The version of the specification: `https://json-schema.org/draft/2020-12/schema`. |
| `type` | Always `object` — a widget configuration is never a list and never a bare value. |
| `title`, `description` | Name and short text of the widget, so that the document stays understandable on its own. |
| `properties` | The fields of the configuration, one node per field. |
| `required` | The field names without which the configuration is incomplete. Often empty: fields with a default are not required. |
| `examples` | At least one **complete** configuration that you can adopt unchanged. |

Each node under `properties` carries plain text and, where they exist, the limits of the value:

| Key | Meaning |
|---|---|
| `title` | Short heading of the field, as it also appears above the control in the editor. |
| `description` | One sentence on what the field does. |
| `type` | `string`, `number`, `integer`, `boolean`, `array`, or `object`. |
| `enum` | The permitted values of a selection field — the check rejects any others. |
| `default` | The value that applies if the field is missing. |
| `minimum`, `maximum`, `minLength`, `maxLength`, `pattern` | The limits of the value. `pattern` usually appears for colors (`#rrggbb`) and identifiers. |
| `properties`, `items` | For nested objects and lists: the same rules one level deeper. |

An excerpt from the “Text” building block:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "title": "Text",
  "description": "Statischer Text-Block mit voller Kontrolle über Schrift, Ausrichtung und Farben.",
  "properties": {
    "content": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500,
      "title": "Text",
      "description": "Der angezeigte Text. Zeilenumbrüche bleiben erhalten; Markup gibt es hier nicht."
    },
    "textAlign": {
      "type": "string",
      "enum": ["left", "center", "right"],
      "default": "center",
      "title": "Ausrichtung",
      "description": "Waagerechte Ausrichtung des Textes."
    }
  },
  "required": ["content"],
  "examples": [{ "content": "Ihr Text", "textAlign": "center", "fontSizeRem": 2 }]
}
```

### Four rules you cannot see in the schema

- **Required means: there is no default.** `required` lists only what the platform cannot fill in itself, usually the content. Everything else carries a `default`, and an omitted field takes exactly that value.
- **Unknown fields are discarded, not rejected.** A field that the schema does not know does not make a configuration invalid, and it disappears on saving. That keeps scenes created before a field restructuring readable, but do not count on a typo being noticed.
- **The examples are complete and verified.** Every entry in `examples` is a configuration that satisfies the schema: the starting configuration and one more per preview variant. An app build aborts if an example breaks its own rule.
- **A configuration survives new versions.** An app adds fields only with a default, and none disappears or changes its type. A stored configuration therefore stays valid when the organization updates the app, and an older version that a display still plays also reads a configuration with fields from a newer one.

### Two values with a fixed shape: place and surface

Two field kinds of the editor forms write values whose shape is the same everywhere. Anyone who builds a configuration themselves finds them in the `configSchema` as an object or a string, respectively:

| Value | Shape | Meaning |
|---|---|---|
| Place (`placeSelection()`) | `{ "mode": "display" \| "fixed", "place": Place \| null, "fallback": Place \| null }`, a place is `{ "displayName", "countryCode", "latitude", "longitude" }` | `display` takes the location of the display that plays the scene, and `fixed` takes the place under `place`. `fallback` is the substitute place for displays whose location has no known position. Default: `display`, both places `null`. |
| Surface | an identifier such as `light`, `dark`, `glass`, `transparent`, or one of the app’s own | The background of the widget. The permitted values are named by the field’s schema. |

How the editor shows these and all other fields, organizes them, and places them on the widget toolbar is described by the app’s form description (`config-ui.ts`, type `WidgetConfigUI`). It belongs to the editor, not to the catalog: the `configSchema` remains the single rule for everyone who writes a configuration.

## The six built-in building blocks

Text, image, video, shape, graphic, and document are part of the core setup: they are not installed, need no consent, and are open to every organization. In the catalog they appear like the widgets of an app, with the reserved identifier `core` and the fixed version pin `0.0.0`.

| `widgetType` | Name | Purpose |
|---|---|---|
| `text` | Text | A text from the configuration, without markup and without network access. |
| `image` | Image | An image from the media library, optionally with slow image motion. |
| `video` | Video | A video from the media library, muted and looping. |
| `shape` | Shape | Rectangles, circles, stars, arrows, and lines, drawn from numbers. |
| `graphic` | Graphic | A motif from the built-in library, tintable and flippable. |
| `document` | Document | A PDF from the media library as page images: one page, in sequence, or two side by side. |

Image, video, and document refer to the media library instead of an address: `assetRef` carries the identifier of the file and the file variants that the display software needs. Without `assetRef`, the widget shows a designed placeholder — that is a valid state and not an error.

For the document, `assetRef.variantSet` stays empty: its page images always travel along, one row `page-<n>` per page in the snapshot’s media list. Which pages the widget shows is stated in `pages`: `numbers` are the chosen pages, and `of` is the file’s page count at the time of choosing. If `pages` is missing, the widget shows all pages except empty ones. If the file later has a different page count than `of`, the scene check reports `DocumentPagesChanged` (a notice, not a blocker). The document declares three templates (`page`, `spread`, `full`) and the minimum size 200 × 200.

## A ready-made widget instance per building block

This is what an entry in a scene’s `document.widgets` looks like. `id` is a new identifier in the format `wgi_<ULID>`, `frame` is the box in pixels of the scene surface, and `zIndex` is the stacking order.

**Text**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "appId": "core",
  "widgetType": "text",
  "appVersionPin": "0.0.0",
  "frame": { "x": 120, "y": 120, "w": 1680, "h": 400 },
  "zIndex": 10,
  "config": {
    "content": "Willkommen in Ihrer Apotheke",
    "fontFamily": "inter",
    "fontStyle": "sans",
    "fontWeight": "bold",
    "fontSizeRem": 3,
    "lineHeight": "tight",
    "textTransform": "none",
    "textAlign": "center",
    "verticalAlign": "middle",
    "padding": "md",
    "textColor": "#0f172a",
    "backgroundStyle": "transparent",
    "backgroundColor": "#ffffff"
  }
}
```

**Image**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "appId": "core",
  "widgetType": "image",
  "appVersionPin": "0.0.0",
  "frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
  "zIndex": 0,
  "config": {
    "assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": ["webp", "original"] },
    "fit": "cover",
    "position": { "x": "center", "y": "center" },
    "borderRadius": "none",
    "backgroundMode": "color",
    "backgroundColor": "#0f172a",
    "motionEffect": "kenburns",
    "kenBurns": { "zoom": 1.15, "durationSec": 24, "direction": "in" }
  }
}
```

**Video**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAX",
  "appId": "core",
  "widgetType": "video",
  "appVersionPin": "0.0.0",
  "frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
  "zIndex": 0,
  "config": {
    "assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": ["original"] },
    "loop": true,
    "muted": true,
    "autoplay": true,
    "fit": "cover",
    "position": { "x": "center", "y": "center" },
    "borderRadius": "none",
    "backgroundMode": "color",
    "backgroundColor": "#000000",
    "startAtSeconds": 0
  }
}
```

**Shape**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAY",
  "appId": "core",
  "widgetType": "shape",
  "appVersionPin": "0.0.0",
  "frame": { "x": 120, "y": 560, "w": 480, "h": 80 },
  "zIndex": 5,
  "config": {
    "variant": "rectangle",
    "fillColor": "#0f766e",
    "fillTransparent": false,
    "fillOpacity": 1,
    "strokeColor": "#0f172a",
    "strokeWidth": 0,
    "cornerRadius": 0,
    "innerRadius": 60,
    "pointDepth": 60,
    "headSize": 36,
    "shaftWidth": 28,
    "lineCap": "butt",
    "dash": "none",
    "rotationDeg": 0
  }
}
```

**Graphic**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAZ",
  "appId": "core",
  "widgetType": "graphic",
  "appVersionPin": "0.0.0",
  "frame": { "x": 1500, "y": 760, "w": 320, "h": 320 },
  "zIndex": 20,
  "config": {
    "graphicId": "pfeil-rechts",
    "tintColor": "#0f172a",
    "accentColor": "#0f766e",
    "flipH": false,
    "flipV": false,
    "opacity": 1
  }
}
```

**Document**

```json
{
  "id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FB0",
  "appId": "core",
  "widgetType": "document",
  "appVersionPin": "0.0.0",
  "frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
  "zIndex": 0,
  "config": {
    "assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": [] },
    "pages": { "numbers": [1, 2, 3], "of": 3 },
    "display": "sequence",
    "fit": "page",
    "interval": 10,
    "corners": "round",
    "surface": "dark"
  }
}
```

A shorter form works too: because every field except `content` carries a default, `{ "content": "Willkommen" }` is already a valid text configuration. The spelled-out form above is the one that the editor saves.

## Check a configuration before you write it

Take the `configSchema` of the widget you want and give it to a validator for Draft 2020-12. The result is a field-level message instead of a rejected request.

```js
import Ajv2020 from 'ajv/dist/2020.js';

const katalog = await (await fetch(`${api}/v1/widget-catalog`, { headers })).json();
const eintrag = katalog.entries.find(
  (e) => e.appId === 'core' && e.definition.id === 'text',
);

const pruefe = new Ajv2020({ allErrors: true }).compile(eintrag.definition.configSchema);
const config = { content: 'Willkommen in Ihrer Apotheke', fontSizeRem: 3 };

if (!pruefe(config)) {
  for (const fehler of pruefe.errors) {
    console.error(`${fehler.instancePath || '(Wurzel)'}: ${fehler.message}`);
  }
}
```

The validator does not cover three things:

- **Whether the file that `assetRef` points to exists.** The media library answers that.
- **Whether the widget type belongs to an installed app.** The platform rejects widgets of an app that is not installed when publishing, and the built-in building blocks are always open.
- **Whether the configuration makes sense.** A QR code with an empty address is schema-valid and still shows only a placeholder.

## The form in the editor

This section is aimed at app authors: it describes how the scene editor builds the settings window and the widget toolbar from an app’s form description (`config-ui.ts`). It changes nothing for a configuration written through the API.

### The order of precedence

The settings window takes the app’s **form description** if it has one registered (`registry.registerConfigUI(widgetType, ui)`). Only without it does an app’s own form apply (`registerConfigForm`), and without both, one derived from the schema. Declarative comes first so that shared improvements (structure, collapsed groups, hints, place field) reach every app without it having to update its form. An app in the new standard therefore no longer registers a whole form, only individual fields at most.

### Custom fields: `registerConfigField`

What no field kind covers (a city search, a selection from its own data), the app brings along as an individual control:

```ts
import type { AuthoringRegistry, ConfigFieldProps } from '@szenera/widget-editor/registry';
import type { GeoPlace } from '@szenera/apps-sdk/config';

function CityPicker({ value, onChange, apiFetch, context }: ConfigFieldProps<GeoPlace | null>) {
  // searches the app's cities through apiFetch, context.locale for the spelling
}

export function registerAuthoring(registry: AuthoringRegistry): void {
  registry.registerConfigUI('weather', weatherConfigUI);
  registry.registerConfigField('weather', 'location.place', CityPicker as never);
}
```

The key is the dot path of the field (`city`) or of a part of it (`location.place` is the place selection of the place field `location`). The form renders label, help text, required asterisk, and error around the control. It receives:

| Prop | Content |
|---|---|
| `value`, `onChange` | The value at this key and the way to write it. |
| `field` | The declared description of the field. |
| `config` | The whole config of the draft, read-only. |
| `error` | The message if the draft violates the schema here. |
| `context` | Frame of the widget in stage pixels, its shape and size class (`band`, `wide`, `square`, `tall`, the same classification as `useWidgetFormat`), language and time zone of the organization, and its displays (name, location name, whether the location has coordinates). |
| `apiFetch` | The app’s transport for its own routes, as with a form of its own. |

### Field kinds with a control of their own

| Field kind | In the window | On the toolbar |
|---|---|---|
| `location` | Choice “Display location” or “Fixed place” (without a display reference using `allowDisplay: false`, only the fixed place). With the display reference, a sentence on which displays have no location with an address, and with `fallback: true`, a substitute place. The place selection is the app’s own (`<field>.place`), otherwise a simple input of name, country, latitude, and longitude. | not compact, window only |
| `surface` | Tiles with a color swatch and label, one active. | Selection with swatch |
| Group with `collapsed: true` | Accordion, closed on opening, and open as long as an error is inside. | – |
| `required: true` | Required asterisk on the label. The schema decides whether a value is missing. | – |

### Quick fields: `quickFields`

`quickFields` names at most two fields of a compact kind (`color`, `segmented`, `select`, `slider`, `toggle`, `surface`), in the order of the toolbar. The toolbar leaves out other kinds and more than two fields, and `defineWidgetConfigUI()` reports this at build time. Besides the quick fields, the toolbar shows the template selection for a widget with several templates ([Widget, configuration, and preview](/apps-sdk/widget-and-preview#templates-in-the-editor)).

### The fit report: keys are field names

If the runtime hides enabled content because the frame is too small, it reports it with `useFitReport().reportHidden(parts)`. The parts are named like the **config fields** that turn the content on, for example `['showDaily']`. The editor then shows “Hidden in this frame: Next days.” beneath the preview, using the field’s label from the form description, and beneath the toggle a sentence on why it has no effect. A part without a matching field appears with its key.

## Related

- [Widget, configuration, and preview](/apps-sdk/widget-and-preview) — how an app writes its widget definition, from which this schema arises.
- [Signing and the path into the catalog](/apps-sdk/signing-and-catalog#the-two-catalogs-across-all-apps) — the two catalogs across all apps and their queries.
- [Data access, permissions, and terms](/apps-sdk/data-access) — what an app discloses before installation.
