# SDUI

**`sonic-sdui`** builds an interface from a JSON descriptor (server-driven UI). Pass the descriptor through `props`, or add `fetch` and an endpoint to load it like [Fetch](/core/components/functional/fetch). It renders in the light DOM with `display: contents`, so an empty instance does not affect layout.

## Basic example

Each entry of `nodes` becomes an element: `tagName` (default `div`) · `attributes` · `textContent` · nested `nodes`.

<sonic-code>
  <template>
    <sonic-sdui
      dataProvider="sdui-basic-example"
      props='{
        "nodes":[{
          "tagName":"div",
          "attributes":{ "class":"flex items-center gap-2" },
          "nodes":[
            { "tagName":"strong", "textContent":"CN 001 · CDG → JFK" },
            { "tagName":"sonic-badge", "attributes":{ "type":"warning" }, "textContent":"Delayed" }
          ]
        }]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Fetching a descriptor

Fetching is off by default: add the boolean `fetch` attribute, then configure `serviceURL` / `endPoint` / `dataProvider` as on [Fetch](/core/components/functional/fetch). Invalidating the publisher or changing the endpoint reloads the interface.

<sonic-code>
  <template>
    <sonic-sdui fetch dataProvider="sdui-fetch-example" endPoint="/src/core/components/functional/sdui/example.json"></sonic-sdui>
  </template>
</sonic-code>

## HTML content

`markup` builds the node from an HTML string, `innerHTML` appends HTML inside the element, and `prefix` / `suffix` wrap it. Prefer `textContent` for anything coming from user data.

<sonic-code>
  <template>
    <sonic-sdui
      dataProvider="sdui-html-example"
      props='{
        "nodes":[
          { "markup":"Cruising at &lt;sonic-badge&gt;Mach 2.04&lt;/sonic-badge&gt;" },
          { "tagName":"p", "prefix":"&lt;hr&gt;", "innerHTML":"Altitude: &lt;b&gt;60,000 ft&lt;/b&gt;" }
        ]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Library

A node with `libraryKey` uses a library entry as its model, then adds its own attributes and children. In the model, `contentElementSelector` sets where those children go. The descriptor `library` is merged over a default library of form fields (`textfield` · `email` · `password` · `date` · `select` · `checkbox` · `radio` · `textarea` · `button` · `submit` · `fieldset`…, see `default-library.json`). The `library` attribute replaces it with a JSON file loaded from a URL.

<sonic-code>
  <template>
    <sonic-sdui
      dataProvider="sdui-library-example"
      props='{
        "library":{
          "flightCard":{
            "tagName":"sonic-card",
            "attributes":{ "class":"block max-w-sm" },
            "nodes":[{ "tagName":"div", "attributes":{ "class":"text-sm" } }],
            "contentElementSelector":"div"
          }
        },
        "nodes":[
          { "libraryKey":"flightCard", "nodes":[{ "tagName":"span", "textContent":"CN 001 · Gate C4" }] },
          { "libraryKey":"email", "attributes":{ "label":"Passenger email", "name":"email", "class":"block mt-4 max-w-sm" } }
        ]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Child placement

A child with `parentElementSelector` is appended to the first element matching that selector inside its parent, instead of the parent itself. It has no effect on top-level nodes.

<sonic-code>
  <template>
    <sonic-sdui
      dataProvider="sdui-parent-selector-example"
      props='{
        "nodes":[{
          "nodes":[
            { "tagName":"strong", "textContent":"Departures" },
            { "tagName":"ul" },
            { "tagName":"li", "parentElementSelector":"ul", "textContent":"CN 001 · CDG → JFK" },
            { "tagName":"li", "parentElementSelector":"ul", "textContent":"CN 003 · LHR → JFK" }
          ]
        }]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Actions

A node with `action: { name, context? }` makes a click on its element dispatch an `sdui-action` event from `sonic-sdui`. `detail` is `{ name, context, sourceNodeId? }`, where `sourceNodeId` is the node `nodeId`. The event bubbles and is composed, so a page shell can listen once for every node. Form posting stays with [Submit](/core/components/functional/submit).

<sonic-code>
  <template>
    <sonic-sdui
      id="sdui-action-example"
      dataProvider="sdui-action-example"
      props='{
        "nodes":[{
          "tagName":"sonic-button",
          "nodeId":"check-in",
          "textContent":"Check in",
          "action":{ "name":"check-in", "context":{ "flight":"CN 001" } }
        }]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

<sonic-code language="typescript">
  <template>
document.querySelector("#sdui-action-example")?.addEventListener("sdui-action", (e) => {
  const { name, context, sourceNodeId } = (e as CustomEvent).detail;
});
  </template>
</sonic-code>

## Safe profile

With `profile="safe"`, the descriptor cannot inject HTML or scripts: `js` URLs are not loaded, and nodes using `markup` / `innerHTML` / `prefix` / `suffix` (directly or through their library model) are skipped with a `console.warn`. `textContent`, attributes and actions still work. Without the attribute, everything is rendered.

<sonic-code>
  <template>
    <sonic-sdui
      profile="safe"
      dataProvider="sdui-safe-example"
      props='{
        "nodes":[
          { "tagName":"p", "textContent":"Gate C4 · Boarding at 10:40" },
          { "innerHTML":"&lt;b&gt;Skipped: HTML is refused&lt;/b&gt;" }
        ]
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

To check a descriptor before rendering it, `validateSafeDescriptor(descriptor)` from `@supersoniks/concorde/sdui-safe` returns the violations as `{ path, field, message }[]` without side effects.

## Partial rendering

Declare nodes once in `nodeById` and reference them with `childNodeIds`. With the boolean `partial`, a missing id renders an empty placeholder (`data-sdui-placeholder` · `aria-hidden` · `display: contents`) so the node can arrive later. Without it, the missing child is skipped with a `console.warn`.

<sonic-code>
  <template>
    <sonic-sdui
      partial
      dataProvider="sdui-partial-example"
      props='{
        "nodes":[{ "tagName":"div", "childNodeIds":["flight", "gate"] }],
        "nodeById":{
          "flight":{ "tagName":"strong", "textContent":"CN 001 · CDG → JFK" }
        }
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Incremental updates

`upsertNode` · `removeNode` · `setData` patch the rendered interface without a full rebuild, so sibling nodes keep their state (focus, input values). A node needs a `nodeId` to be updated. The first operation switches the component to incremental mode, where `props` changes are ignored until `endIncremental()`. To drive it from data, set `opsDataProvider`: each value assigned to that publisher (`SDUIOp` or `SDUIOp[]`) goes through `applyOp`.

<sonic-code>
  <template>
    <sonic-sdui
      id="sdui-ops-example"
      dataProvider="sdui-ops-example"
      props='{
        "nodes":[
          { "nodeId":"cn001", "tagName":"p", "textContent":"CN 001 · CDG → JFK · On time" },
          { "nodeId":"cn003", "tagName":"p", "textContent":"CN 003 · LHR → JFK · Boarding" }
        ]
      }'
    ></sonic-sdui>
    <sonic-button size="sm" onclick="document.getElementById('sdui-ops-example').upsertNode({ nodeId: 'cn001', tagName: 'p', textContent: 'CN 001 · CDG → JFK · Delayed 20 min' })">Delay CN 001</sonic-button>
    <sonic-button size="sm" variant="outline" onclick="document.getElementById('sdui-ops-example').removeNode('cn003')">Remove CN 003</sonic-button>
  </template>
</sonic-code>

## Transformations

The `transformation` attribute points to a JSON file applied to the descriptor before rendering. Its `library` is merged into the descriptor library; each entry of `transforms` runs an action on the nodes matching `patterns`.

<sonic-code language="typescript">
  <template>
{
  "transforms": [
    { "action": "remap", "patterns": [{ "libraryKey": "textfield" }], "ui": { "tagName": "sonic-input", "attributes": { "size": "lg" } } },
    { "action": "insert", "after": { "libraryKey": "email" }, "ui": { "tagName": "p", "textContent": "Boarding pass sent by email" } }
  ]
}
  </template>
</sonic-code>

| Action | Effect |
|--------|--------|
| `remap` | Replaces each matched node with `ui`, keeping its attributes, content and children unless `ui` redefines them |
| `unwrap` | Replaces each matched node with its children |
| `wrap` | Groups the matched nodes into the `ui` node |
| `delete` | Removes the matched nodes |
| `insert` | Inserts `ui` relative to the node matched by `before` · `after` · `in` |
| `move` | Moves the matched nodes next to the node matched by `before` or `after` |

## Wrapped descriptor

When the descriptor is one key of a larger payload, set `sduiKey` to that key. Set `messagesKey` to render the toast messages found at that key with `sonic-toast-message-subscriber` (`{ content, status, type }` items).

<sonic-code>
  <template>
    <sonic-sdui
      sduiKey="ui"
      dataProvider="sdui-key-example"
      props='{
        "flight":"CN 001",
        "ui":{ "nodes":[{ "tagName":"p", "textContent":"Weather warning: turbulence expected over the Atlantic" }] }
      }'
    ></sonic-sdui>
  </template>
</sonic-code>

## Component catalog

The package ships a JSON catalog of every custom element (`tag`; `props` with attribute / type / allowed values / default; slots and events), generated at build time for descriptor authors and tools. It has no runtime cost. Elements with thin annotations are listed in `component-catalog.gaps.json`.

<sonic-code language="typescript">
  <template>
import catalog from "@supersoniks/concorde/sdui/component-catalog.json";
  </template>
</sonic-code>

## Playground

Edit the descriptor and leave the field to render it.

<sonic-code>
  <template>
    <sonic-textarea
      rows="8"
      onChange="SonicPublisherManager.get('sdui-playground').set(JSON.parse(this.value))"
      value='{"nodes":[{"tagName":"sonic-badge", "attributes":{"type":"success"}, "textContent":"CN 001 landed"}]}'
    ></sonic-textarea>
    <sonic-sdui dataProvider="sdui-playground" props='{"nodes":[{"tagName":"sonic-badge", "attributes":{"type":"success"}, "textContent":"CN 001 landed"}]}'></sonic-sdui>
  </template>
</sonic-code>

## API reference

| Attribute | Values | Description |
|-----------|--------|-------------|
| `dataProvider` | string | Publisher holding the descriptor. Required |
| `props` | `SDUIDescriptor` (JSON) | Descriptor to render |
| `fetch` | boolean | Loads the descriptor from `serviceURL` + `endPoint` (or `dataProvider`). Other request attributes as on [Fetch](/core/components/functional/fetch) |
| `sduiKey` | string | Reads the descriptor from `props[sduiKey]` |
| `messagesKey` | string | Path in `props` of the toast messages to display |
| `profile` | `safe` | Refuses `js` / `markup` / `innerHTML` / `prefix` / `suffix`. Unset by default |
| `partial` | boolean | Renders a placeholder for missing `childNodeIds` |
| `library` | URL | JSON library that replaces the descriptor library |
| `transformation` | URL | JSON transform description applied before rendering |
| `opsDataProvider` | string | Publisher whose assigned values are passed to `applyOp` |

| Descriptor key | Description |
|----------------|-------------|
| `nodes` | Root nodes |
| `nodeById` | Flat registry of nodes for `childNodeIds` |
| `library` | Node models by key, merged over the default library |
| `js` / `css` | Script and stylesheet URLs to load (`js` refused in safe profile) |

| Node field | Description |
|------------|-------------|
| `tagName` | Element tag (default `div`) |
| `attributes` | Attribute name → value (objects are serialized to JSON) |
| `nodes` | Child nodes |
| `childNodeIds` | Child `nodeId`s resolved from `nodeById` or the tree |
| `textContent` | Plain text. Ignored when the node has children |
| `markup` · `innerHTML` · `prefix` · `suffix` | HTML strings (refused in safe profile) |
| `libraryKey` | Library entry used as the model |
| `contentElementSelector` | In a library model: where children are appended |
| `parentElementSelector` | Appends the node to a matching element of its parent |
| `nodeId` | Logical id for actions, `childNodeIds` and incremental updates (rendered as `data-sdui-node-id`) |
| `action` | `{ name, context? }` dispatched as `sdui-action` on click |

| Method | Description |
|--------|-------------|
| `upsertNode(node, parentId?, index?)` | Adds or replaces a node by `nodeId` |
| `removeNode(nodeId)` | Removes a node |
| `setData(path, value)` | Sets a publisher path (`form.email`), absolute or relative to `dataProvider` |
| `applyOp(op)` | Runs an `SDUIOp`: `upsertNode` · `removeNode` · `setData` |
| `endIncremental()` | Leaves incremental mode; the next `props` change rebuilds everything |

| Event | Detail |
|-------|--------|
| `sdui-action` | `{ name, context, sourceNodeId? }` |

No slots.
