# Widgets konfigurieren

Der Widget-Katalog liefert zu jedem Widget ein vollständiges JSON-Schema seiner Konfiguration: jedes Feld mit Überschrift und Erklärung, die Pflichtfelder benannt und mindestens eine fertige Beispiel-Konfiguration. Wer eine Szene ohne den Editor baut, liest daraus die Regeln, statt sie zu raten — und prüft seine Konfiguration dagegen, bevor er sie schreibt.

> **Typ:** anleitung
> **Für wen:** Wer Szenen über die Schnittstelle anlegt oder ändert — ein eigenes Werkzeug, ein Assistent, ein Import.
> **Voraussetzung:** ein Ausweis Ihrer Organisation für `GET /v1/widget-catalog`; ein JSON-Schema-Prüfer, der Draft 2020-12 versteht.

## Den Katalog abrufen

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

Die Antwort trägt unter `entries` je Widget einen Eintrag:

| Feld | Bedeutung |
|---|---|
| `appId` | Die App, zu der das Widget gehört — oder `core` bei den sechs eingebauten Bausteinen. |
| `version` | Die Fassung der App, aus der die Definition stammt; bei den eingebauten Bausteinen fest `0.0.0`. |
| `definition.id` | Der Widget-Typ. Genau dieser Wert steht später in `widgetType` der Widget-Instanz. |
| `definition.name`, `definition.description` | Name und Kurztext, wie sie auch die Auswahl im Editor zeigt. |
| `definition.configSchema` | Das JSON-Schema der Konfiguration — der Gegenstand dieser Seite. |
| `definition.size.min` | Der kleinste Rahmen in Bezugspixeln einer 1920 × 1080-Szene, in dem das Widget noch etwas Sinnvolles zeigt. |
| `definition.preview.defaultConfig` | Die Konfiguration, mit der eine frisch eingefügte Instanz startet. |
| `definition.preview.variants` | Die Darstellungen, die das Widget beherrscht, je mit einer Teil-Konfiguration und, als Vorlage, mit einer Startgröße (`size`). |
| `definition.preview.states` | Benannte Beispiel-Nutzlasten für die Vorschau im Editor (Regen, Nacht, keine Daten). Für eine Konfiguration ohne Bedeutung. |

Der Katalog spiegelt den jeweils neuesten freigegebenen Stand einer App. Eine Organisation, die eine ältere Fassung installiert hat, kann geringfügig davon abweichen; maßgeblich ist dann die Fassung, die in `appVersionPin` der Instanz steht.

## Was im `configSchema` steht

Das `configSchema` ist ein JSON-Schema-Dokument nach Draft 2020-12. Es beschreibt genau das Objekt, das später als `config` in der Widget-Instanz steht.

| Schlüssel | Bedeutung |
|---|---|
| `$schema` | Die Fassung der Spezifikation: `https://json-schema.org/draft/2020-12/schema`. |
| `type` | Immer `object` — eine Widget-Konfiguration ist nie eine Liste und nie ein blanker Wert. |
| `title`, `description` | Name und Kurztext des Widgets, damit das Dokument auch für sich allein verständlich bleibt. |
| `properties` | Die Felder der Konfiguration, je Feld ein eigener Knoten. |
| `required` | Die Feldnamen, ohne die die Konfiguration unvollständig ist. Häufig leer: Felder mit Vorgabe sind nicht Pflicht. |
| `examples` | Mindestens eine **vollständige** Konfiguration, die Sie unverändert übernehmen können. |

Jeder Knoten unter `properties` trägt Klartext und, wo es sie gibt, die Grenzen des Werts:

| Schlüssel | Bedeutung |
|---|---|
| `title` | Kurze Überschrift des Feldes, wie sie auch im Editor über dem Bedienelement steht. |
| `description` | Ein Satz dazu, was das Feld bewirkt. |
| `type` | `string`, `number`, `integer`, `boolean`, `array` oder `object`. |
| `enum` | Die zulässigen Werte eines Auswahlfeldes — andere weist die Prüfung ab. |
| `default` | Der Wert, der gilt, wenn das Feld fehlt. |
| `minimum`, `maximum`, `minLength`, `maxLength`, `pattern` | Die Grenzen des Werts; `pattern` steht meist bei Farben (`#rrggbb`) und Kennungen. |
| `properties`, `items` | Bei verschachtelten Objekten und Listen: dieselben Regeln eine Ebene tiefer. |

Ein Ausschnitt aus dem Baustein „Text“:

```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 }]
}
```

### Vier Regeln, die man dem Schema nicht ansieht

- **Pflicht heißt: es gibt keine Vorgabe.** In `required` steht nur, was die Plattform nicht selbst einsetzen kann — meistens der Inhalt. Alles andere trägt ein `default`, und ein weggelassenes Feld nimmt genau diesen Wert an.
- **Unbekannte Felder werden verworfen, nicht abgelehnt.** Ein Feld, das das Schema nicht kennt, macht eine Konfiguration nicht ungültig; es verschwindet beim Speichern. Das hält Szenen lesbar, die vor einem Feld-Umbau entstanden sind — verlässt sich aber niemand darauf, dass ein Tippfehler auffällt.
- **Die Beispiele sind vollständig und geprüft.** Jeder Eintrag in `examples` ist eine Konfiguration, die das Schema erfüllt: die Start-Konfiguration und je Vorschau-Variante eine weitere. Der Bau einer App bricht ab, wenn ein Beispiel seine eigene Regel bricht.
- **Eine Konfiguration überlebt neue Fassungen.** Eine App fügt Felder nur mit Vorgabe hinzu, und keines verschwindet oder ändert seinen Typ. Eine gespeicherte Konfiguration bleibt deshalb gültig, wenn die Organisation die App aktualisiert, und eine ältere Fassung, die ein Display noch spielt, liest auch eine Konfiguration mit Feldern einer neueren.

### Zwei Werte mit fester Form: Ort und Fläche

Zwei Feldarten der Editor-Formulare schreiben Werte, deren Form überall gleich ist. Wer eine Konfiguration selbst baut, findet sie im `configSchema` als Objekt bzw. Zeichenkette wieder:

| Wert | Form | Bedeutung |
|---|---|---|
| Ort (`placeSelection()`) | `{ "mode": "display" \| "fixed", "place": Ort \| null, "fallback": Ort \| null }`, ein Ort ist `{ "displayName", "countryCode", "latitude", "longitude" }` | `display` nimmt den Standort des Displays, das die Szene spielt, `fixed` den Ort unter `place`. `fallback` ist der Ersatzort für Displays, deren Standort keine bekannte Lage hat. Vorgabe: `display`, beide Orte `null`. |
| Fläche | eine Kennung wie `light`, `dark`, `glass`, `transparent` oder eine eigene der App | Der Hintergrund des Widgets. Die zulässigen Werte nennt das Schema des Felds. |

Wie der Editor diese und alle anderen Felder zeigt, gliedert und an die Leiste am Widget stellt, beschreibt die Formular-Beschreibung der App (`config-ui.ts`, Typ `WidgetConfigUI`). Sie gehört zum Editor, nicht zum Katalog: Das `configSchema` bleibt die eine Regel für jeden, der eine Konfiguration schreibt.

## Die sechs eingebauten Bausteine

Text, Bild, Video, Form, Grafik und Dokument gehören zur Grundausstattung: Sie werden nicht installiert, brauchen keine Zustimmung und stehen jeder Organisation offen. Im Katalog erscheinen sie wie die Widgets einer App — mit der reservierten Kennung `core` und dem festen Versions-Pin `0.0.0`.

| `widgetType` | Name | Wofür |
|---|---|---|
| `text` | Text | Ein Text aus der Konfiguration, ohne Markup und ohne Netzwerkzugriff. |
| `image` | Bild | Ein Bild aus der Medienbibliothek, wahlweise mit langsamer Bildbewegung. |
| `video` | Video | Ein Video aus der Medienbibliothek, stumm und in Schleife. |
| `shape` | Form | Rechtecke, Kreise, Sterne, Pfeile und Linien — gezeichnet aus Zahlen. |
| `graphic` | Grafik | Ein Motiv aus der eingebauten Bibliothek, einfärbbar und spiegelbar. |
| `document` | Dokument | Ein PDF aus der Medienbibliothek als Seitenbilder: eine Seite, nacheinander oder zwei nebeneinander. |

Bild, Video und Dokument verweisen auf die Medienbibliothek statt auf eine Adresse: `assetRef` trägt die Kennung der Datei und die Fassungen, die die Anzeige braucht. Ohne `assetRef` zeigt das Widget einen gestalteten Platzhalter — das ist ein gültiger Zustand und kein Fehler.

Beim Dokument bleibt `assetRef.variantSet` leer: Seine Seitenbilder reisen immer mit, je Seite eine Zeile `page-<n>` in der Medien-Liste des Snapshots. Welche Seiten das Widget zeigt, steht in `pages`: `numbers` sind die gewählten Seiten, `of` die Seitenzahl der Datei beim Wählen. Fehlt `pages`, zeigt das Widget alle Seiten außer leeren. Hat die Datei später eine andere Seitenzahl als `of`, meldet die Prüfung der Szene `DocumentPagesChanged` (ein Hinweis, kein Halt). Das Dokument erklärt drei Vorlagen (`page`, `spread`, `full`) und die Mindestgröße 200 × 200.

## Je Baustein eine fertige Widget-Instanz

So sieht ein Eintrag in `document.widgets` einer Szene aus. `id` ist eine neue Kennung im Format `wgi_<ULID>`, `frame` die Box in Pixeln der Szenenfläche, `zIndex` die Stapelreihenfolge.

**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"
  }
}
```

**Bild**

```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
  }
}
```

**Form**

```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
  }
}
```

**Grafik**

```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
  }
}
```

**Dokument**

```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"
  }
}
```

Kürzer geht es auch: Weil jedes Feld außer `content` eine Vorgabe trägt, ist `{ "content": "Willkommen" }` bereits eine gültige Text-Konfiguration. Die ausgeschriebene Fassung oben ist die, die der Editor speichert.

## Eine Konfiguration prüfen, bevor Sie sie schreiben

Nehmen Sie das `configSchema` des gewünschten Widgets und geben Sie es einem Prüfer für Draft 2020-12. Der Ertrag ist eine feldgenaue Meldung, statt einer abgelehnten Anfrage.

```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}`);
  }
}
```

Drei Dinge nimmt Ihnen der Prüfer nicht ab:

- **Ob es die Datei gibt, auf die `assetRef` zeigt.** Das beantwortet die Medienbibliothek.
- **Ob der Widget-Typ zu einer installierten App gehört.** Widgets einer nicht installierten App weist die Plattform beim Veröffentlichen ab; die eingebauten Bausteine stehen immer offen.
- **Ob die Konfiguration Sinn ergibt.** Ein QR-Code mit leerer Adresse ist schema-gültig und zeigt trotzdem nur einen Platzhalter.

## Das Formular im Editor

Dieser Abschnitt richtet sich an App-Autoren: Er beschreibt, wie der Szenen-Editor aus der Formular-Beschreibung einer App (`config-ui.ts`) das Einstellungs-Fenster und die Leiste am Widget baut. Für eine Konfiguration über die Schnittstelle ändert er nichts.

### Die Rangfolge

Das Einstellungs-Fenster nimmt die **Formular-Beschreibung** der App, wenn sie eine hinterlegt (`registry.registerConfigUI(widgetType, ui)`). Nur ohne sie greift ein eigenes Formular der App (`registerConfigForm`), ohne beides ein aus dem Schema abgeleitetes. Deklarativ zuerst, damit gemeinsame Verbesserungen (Gliederung, eingeklappte Gruppen, Hinweise, Ort-Feld) jede App erreichen, ohne dass sie ihr Formular nachzieht. Eine App im neuen Standard registriert deshalb kein ganzes Formular mehr, sondern höchstens einzelne Felder.

### Eigene Felder: `registerConfigField`

Was keine Feldart abbildet (eine Stadtsuche, eine Auswahl aus eigenen Daten), bringt die App als einzelnes Bedienelement mit:

```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>) {
  // sucht über apiFetch die Städte der App, context.locale für die Schreibweise
}

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

Der Schlüssel ist der Punkt-Pfad des Felds (`city`) oder eines Teils davon (`location.place` ist die Ortsauswahl des Ort-Felds `location`). Das Formular rendert Beschriftung, Hilfetext, Pflicht-Stern und Fehler um das Bedienelement herum. Es bekommt:

| Prop | Inhalt |
|---|---|
| `value`, `onChange` | Der Wert an diesem Schlüssel und der Weg, ihn zu schreiben. |
| `field` | Die deklarierte Beschreibung des Felds. |
| `config` | Die ganze Config des Entwurfs, nur zum Lesen. |
| `error` | Die Meldung, wenn der Entwurf hier gegen das Schema verstößt. |
| `context` | Rahmen des Widgets in Bühnenpixeln, seine Form und Größenklasse (`band`, `wide`, `square`, `tall`, dieselbe Einordnung wie `useWidgetFormat`), Sprache und Zeitzone der Organisation und ihre Displays (Name, Standortname, ob der Standort Koordinaten hat). |
| `apiFetch` | Der Transport der App für ihre eigenen Routen, wie bei einem eigenen Formular. |

### Feldarten mit eigenem Bedienelement

| Feldart | Im Fenster | An der Leiste |
|---|---|---|
| `location` | Wahl „Standort des Displays“ oder „Fester Ort“ (ohne Display-Bezug mit `allowDisplay: false` nur der feste Ort). Beim Display-Bezug ein Satz, welche Displays keinen Standort mit Anschrift haben, mit `fallback: true` ein Ersatzort. Die Ortsauswahl ist die der App (`<feld>.place`), sonst eine einfache Eingabe aus Name, Land, Breite und Länge. | nicht kompakt, nur im Fenster |
| `surface` | Kacheln mit Farbmuster und Beschriftung, eine aktiv. | Auswahl mit Muster |
| Gruppe mit `collapsed: true` | Aufklapper, zu beim Öffnen, offen, solange darin ein Fehler steht. | – |
| `required: true` | Pflicht-Stern an der Beschriftung. Ob ein Wert fehlt, entscheidet das Schema. | – |

### Schnellfelder: `quickFields`

Höchstens zwei Felder kompakter Art (`color`, `segmented`, `select`, `slider`, `toggle`, `surface`) nennt `quickFields`, in der Reihenfolge der Leiste. Andere Arten und mehr als zwei Felder lässt die Leiste weg, `defineWidgetConfigUI()` meldet das schon beim Bau. Neben den Schnellfeldern zeigt die Leiste bei einem Widget mit mehreren Vorlagen die Vorlagen-Auswahl ([Widget, Konfiguration und Vorschau](/de/apps-sdk/widget-and-preview#vorlagen-im-editor)).

### Der Passt-Bericht: Schlüssel sind Feldnamen

Blendet die Laufzeit einen eingeschalteten Inhalt aus, weil der Rahmen zu klein ist, meldet sie ihn mit `useFitReport().reportHidden(parts)`. Die Teile heißen wie die **Felder der Config**, die den Inhalt einschalten, etwa `['showDaily']`. Der Editor zeigt dann unter der Vorschau „In diesem Rahmen ausgeblendet: Nächste Tage.“ mit der Beschriftung des Felds aus der Formular-Beschreibung und unter dem Schalter einen Satz, warum er nichts bewirkt. Ein Teil ohne passendes Feld erscheint mit seinem Schlüssel.

## Weiterführendes

- [Widget, Konfiguration und Vorschau](/de/apps-sdk/widget-and-preview) — wie eine App ihre Widget-Definition schreibt, aus der dieses Schema entsteht.
- [Signieren und der Weg in den Katalog](/de/apps-sdk/signing-and-catalog#die-beiden-kataloge-über-alle-apps) — die beiden Kataloge über alle Apps und ihre Abfragen.
- [Datenzugriff, Rechte und Bedingungen](/de/apps-sdk/data-access) — was eine App vor der Installation offenlegt.
