> ## Documentation Index
> Fetch the complete documentation index at: https://widgetic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# UI Components

> Put Widgetic's File Browser, composition editor, widget creator and Workflows in your own page

The UI SDK (`@widgetic/ui-sdk`) puts Widgetic's ready-made pieces in your own web page with a few lines of code. Your customers use them without leaving your product and without a Widgetic account.

> [!NOTE]
>
> **Preview.** The pieces on this page are new. They are built and tested, and you can try them now, but small details can still change while this label is on. Tell us what you find at [support@widgetic.com](mailto:support@widgetic.com).

## The pieces

| Module | What your customer can do | The session needs |
| - | - | - |
| `file-browser` | Browse, upload and pick files, from Widgetic's media library (files kept in your own storage) or from your own | `files` (nothing, with your own [media library](#show-your-own-media-library)) |
| `composition-editor` | Change the texts, pictures, colours and fonts of a widget beside a live preview, and save | `editor` and `files` |
| `widget-creator` | The whole Widgetic canvas: draw, describe a widget to the AI, watch it being built, change it and publish it | `creator`, `editor` and `files` |
| `workflows` | Where a team, or one of your customers, builds workflows: compositions chained together, with filters between them that decide which one a visitor sees next. The list of their workflows and, one click away, the editor of one: blocks and lines on a canvas, a draft that is saved while you work, and versions you publish | `workflows`. A signed-in person of your own team uses their own token instead (see [Workflows](#workflows)) |

The File Browser is covered on this page, and so is [Workflows](#workflows). The composition editor and the widget creator have their own options and events, in [Canvas Embedding](https://widgetic.com/docs/partners/canvas-embedding.md).

Four things hold for all of them:

* **They work only for a partner in good standing, with storage.** A session is made only while your account is active and your own [storage](https://widgetic.com/docs/partners/storage.md) works (see [Every session needs your own storage](https://widgetic.com/docs/partners/editor-sessions.md#every-session-needs-your-own-storage)). While your account is paused, the pieces already on your customers' pages are refused too (see [Account Status](https://widgetic.com/docs/partners/account-status.md)).
* **No API key in the browser.** Your server keeps the key and makes a short-lived [editor session](https://widgetic.com/docs/partners/editor-sessions.md) for the customer who is on the page. The page only ever holds that session's token, and it can reach only that customer's content. Workflows can also be shown to a signed-in person of your own team, with that person's token: it never holds a key either.
* **Your page stays yours.** A piece is drawn inside a shadow root: your CSS cannot restyle it and its CSS cannot touch your page. The SDK writes nothing to `<html>` or `<body>`, sends no `postMessage` and never clears your container.
* **Small.** The SDK is a loader of about 30 KB. A piece is downloaded from Widgetic's CDN when you mount it, and only then.

## How it works

**1. Your server makes an editor session**

When the page opens, your server asks Widgetic for a session for the signed-in customer (`POST /v1/managed-users/{id}/editor-sessions`, with your API key) and returns the token. See [Editor Sessions](https://widgetic.com/docs/partners/editor-sessions.md).

**2. Your page gives the SDK a getToken function**

`getToken` fetches that token from your server. The SDK calls it when the piece starts and again shortly before the token runs out.

**3. The SDK mounts the piece**

It downloads the piece, draws it in your container, and the piece talks to the Widgetic API with the token and nothing else.

## Install

```bash theme={null}
npm install @widgetic/ui-sdk
```

Or with a script tag. Pin the exact version you have tested and do not use `@latest`. Replace `<version>` with it; `npm view @widgetic/ui-sdk version` prints the newest one:

```html theme={null}
<script src="https://unpkg.com/@widgetic/ui-sdk@<version>/dist/umd/ui-sdk.js"></script>
<script>
  WidgeticUISDK.mountModule('file-browser', {
    container: '#media-library',
    getToken: () => fetch('/widgetic-session', { method: 'POST' }).then((response) => response.json()),
    on: { select: ({ files }) => console.log(files) },
  });
</script>
```

## Mount a piece

Give the container a height: the piece fills it.

```javascript theme={null}
import { mountModule } from '@widgetic/ui-sdk';

const fileBrowser = await mountModule('file-browser', {
  container: document.querySelector('#media-library'), // an element, or a CSS selector
  getToken: async () => (await fetch('/widgetic-session', { method: 'POST' })).json(), // { token, expiresAt }
  maxSelections: 1,
  on: {
    select: ({ files }) => useFiles(files), // [{ id, name, url, mimeType?, size?, thumbnail? }]
  },
});

// later:
await fileBrowser.unmount();
```

`/widgetic-session` is the endpoint of yours that makes the session (see [A server endpoint for your page](https://widgetic.com/docs/partners/editor-sessions.md#a-server-endpoint-for-your-page)). It returns `{ token, expiresAt }`.

### Options

| Option | Type | |
| - | - | - |
| `container` | element or CSS selector | **Required.** Where the piece is drawn. |
| `getToken` | `({ reason }) => Promise<string \| { token, expiresAt }>` | Required to show the customer's Widgetic data. Not needed for the File Browser with your own `mediaLibrary`. Workflows takes an editor session with the `workflows` surface, or the token of a signed-in person. See [Tokens](#tokens). |
| `version` | `string` | Pin an exact version of the piece, such as `0.1.0`. Default: the newest. Pin it in production, and read [what changed](https://widgetic.com/docs/partners/module-versions.md) before you move. |
| `theme` | `'light'`, `'dark'`, `'system'`, or `{ preset, mode, colors, radius, font }` | Default `light`. `system` follows the customer's setting. An object gives the piece your colours, corner radius and font. See [Make the pieces look like your product](#make-the-pieces-look-like-your-product). |
| `locale` | `string` | A BCP 47 tag such as `en` or `de`. Every piece uses it for plural forms and for how numbers, dates and file sizes are written, and for the `lang` of its elements; a language written right to left also turns each piece around. See [Show the pieces in your customers' language](#show-the-pieces-in-your-customers-language). |
| `messages` | `{ [key]: string }` | The words you want to change, in the File Browser, the composition editor, the widget creator and Workflows. A key you leave out stays English. See [Show the pieces in your customers' language](#show-the-pieces-in-your-customers-language). |
| `apiUrl` | `string` | The Widgetic API. Default `https://api.widgetic.com`. |
| `moduleBaseUrl` | `string` | Where pieces are served from. Default `https://cdn.widgetic.com/modules`. See [Versions and self-hosting](#versions-and-self-hosting). |
| `on` | `{ [event]: handler }` | Event handlers, connected before the piece starts so you see `ready`. |
| `signal` | `AbortSignal` | Cancels the mount, for components that can go away before the piece is up. |
| `placeholder` | `boolean` | `false` skips the loading spinner shown while the piece downloads. |
| `loadingLabel` | `string` | The words a screen reader says for that spinner. Default `Loading`. |
| anything else | | Handed to the piece. File Browser: `maxSelections`, `initialFilter`, `selectLabel`, `mediaLibrary`. Workflows: `workspaceId` (required) and `workflowId`. |

`mountModule` resolves with a handle once the piece is on screen:

```typescript theme={null}
{
  id: string; type: string; version: string;     // version is the one that was actually loaded
  container: HTMLElement;
  module: UIModule;                              // the piece itself, for its own methods
  on(event, handler): () => void;                // returns a function that unsubscribes
  off(event, handler): void;
  setTheme(theme): void;                         // this piece's look alone
  setLanguage({ locale?, messages? }): void;     // this piece's language alone
  unmount(): Promise<void>;                      // takes the piece and its listeners down
}
```

### Events

| Event | Payload | |
| - | - | - |
| `ready` | | The piece is on screen. |
| `error` | `{ message, code?, fatal }` | `fatal: true`: the piece has stopped. `false`: it carries on, for example after a failed token refresh that will be retried. When your own [media library](#show-your-own-media-library) fails, the File Browser adds `action` (what the customer was doing) and, for an upload, `file` (its name). |
| `select` | `{ files: [{ id, name, url, mimeType?, size?, thumbnail? }] }` | File Browser: the customer confirmed a pick. |
| `openWorkflow` | `{ workflowId }` | Workflows: the person opened a workflow, or went back to the list (`workflowId` is then `null`). Use it to keep your page's address in step. |
| `showEmbedCode` | `{ workflowId }` | Workflows: the person asked for the embed code of a workflow. You show it, because your page knows how its embed tag is built. |

The composition editor and the widget creator add events of their own (`dirty`, `saved`, `canvasChanged`, `widgetPublished`, `limitReached` and more), and say more in `ready` and `error`. They are listed in [Canvas Embedding](https://widgetic.com/docs/partners/canvas-embedding.md).

### Several pieces on one page

`mountModule` creates a `WidgeticUI`, mounts one piece with it and destroys it again when you `unmount()`. To mount several pieces, or to download one ahead of time, make the `WidgeticUI` yourself:

```javascript theme={null}
import { WidgeticUI } from '@widgetic/ui-sdk';

const ui = new WidgeticUI({ getToken, theme: 'system' });      // touches nothing until you mount
await ui.preload('widget-creator');                            // optional: download it ahead of time
const files = await ui.mount('file-browser', { container: '#files' });

ui.updateTheme('dark');                                        // every mounted piece, and later ones (colours, radius and font stay)
await files.unmount();
await ui.destroy();                                            // unmounts whatever is left
```

## Make the pieces look like your product

A piece can wear your colours, the roundness of your corners and your typeface. The components and the layout stay Widgetic's: only the look follows your design system. The words can follow your language too (see [below](#show-the-pieces-in-your-customers-language)). Each piece has a theme of its own, so the File Browser, the composition editor and the widget creator can look the same or different.

```javascript theme={null}
const ui = new WidgeticUI({
  getToken,
  // The look of every piece of this page
  theme: {
    preset: 'ocean',
    radius: 'medium',
    font: { family: 'Poppins', stylesheetUrl: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap' },
  },
});

const files = await ui.mount('file-browser', { container: '#files' });   // takes the page's look
const editor = await ui.mount('composition-editor', {
  container: '#editor',
  compositionId,
  theme: { preset: 'violet', mode: 'dark', colors: { primary: '#e0541b' } },   // a look of its own
});

ui.updateTheme('dark');                 // the mode of every piece; each keeps its colours, radius and font
ui.updateTheme({ preset: 'sunset' });   // new page-wide settings, for the pieces that have none of their own
editor.setTheme({ radius: 'none' });    // one piece alone
```

### The settings

| Setting | |
| - | - |
| `preset` | A built-in look to start from: `'widgetic'` (the default), `'graphite'`, `'ocean'`, `'violet'` or `'sunset'`. Each has a light and a dark look. A preset sets colours only. |
| `mode` | `'light'` or `'dark'`. On the `WidgeticUI` you can also write `'system'`, which follows the customer's own setting. |
| `colors` | Any of `primary`, `primaryForeground` (the text on the primary colour), `background`, `foreground` (the text), `muted` (quiet surfaces), `mutedForeground` (secondary text), `border`, `accent` (the soft highlight) and `destructive` (errors). Name only the ones you want to decide: the rest follows from them. A `primary` on its own changes the brand colours and leaves the greys alone. |
| `radius` | `'none'` (square), `'small'` (4 px), `'medium'` (8 px) or `'large'` (12 px), or a number of pixels from 0 to 32. Without it the corners are as they were. |
| `font` | `{ family, stylesheetUrl? }`. `family` is a CSS font-family value: one name gets system fonts behind it, a list is used as written. It applies to the piece's text, not to your page. |

| Preset | Brand colour, light | Brand colour, dark | |
| - | - | - | - |
| `widgetic` | Deep forest green (unchanged) | A lighter green on near-black | The Widgetic look. |
| `graphite` | `#1f2328` | `#6b7683` | Neutral and quiet. The content carries the colour. |
| `ocean` | `#0b69c7` | `#3b82f0` | Clear blue. |
| `violet` | `#6d3fd9` | `#8b5cf6` | A saturated purple. |
| `sunset` | `#e0541b` | `#e8602a` | Warm orange. |

Your own `colors` win over the preset's. A `primary` of yours without a `primaryForeground` gets the white or the dark ink that reads better on it.

### Writing a colour

`#rrggbb`, `#rgb`, `rgb(11 107 203)` and `hsl(210 90% 41%)` always work. A colour name or `oklch()` works in a browser that understands it. `var(--brand)` and `currentColor` are refused: the piece reads the colour when you give it and cannot look at your page's variables from there, so read the value in your page and pass it in. Transparency is ignored. The piece warns in the console about a combination that is hard to read: your text under 4.5:1 against your background, or your primary colour (or the text on it) under 3:1.

### A font your page does not load yet

A font can only be defined by the page itself, never from inside a piece. Give the `https` address of its stylesheet as `stylesheetUrl` and the piece adds one `<link rel="stylesheet" data-widgetic-ui-font>` to your `<head>`, once however many pieces use it. It stays after `unmount()`, like the font rules every piece adds. A font your page already loads needs no `stylesheetUrl`. Under a strict Content-Security-Policy, `style-src` has to allow the stylesheet's host and `font-src` the font files (for Google Fonts, `https://fonts.googleapis.com` and `https://fonts.gstatic.com`).

### Changing a look later

* A bare mode (`'dark'`, `{ mode: 'dark' }`) changes the mode and keeps the rest of the look. Settings replace the whole look.
* Settings given in a mount call, or to `setTheme` on a piece, are that piece's own. `ui.updateTheme(settings)` does not take them away, and a page that changes a preset does not turn a dark piece light. A call that names a mode reaches every piece.
* `ui.getTheme()` tells you the page's mode and settings. `'system'` stays `'system'`.
* A theme that cannot be used is refused with the code `INVALID_THEME`. The message names the setting and what to write instead, and nothing changes. When only the piece can tell (an unknown preset name, a colour it cannot read), `ui.updateTheme` reports it in the console and the piece keeps its look, while `setTheme` and a mount call throw (the mount fails with `MOUNT_FAILED` and leaves its container empty).

### What a theme cannot do yet

* **The widget creator takes a theme's mode, neutral colours, brand colour and font, but not yet its corners.** A dark `mode`, or colours of your own for `background`, `foreground`, `muted`, `mutedForeground` and `border`, restyle its panels, toolbar, menus, dialogs and chat, and the `font` applies to all of its text. Every text of it stays readable in a dark look, for each built-in preset and for neutral colours of your own. Your `primary` colour is the creator's accent: the selected tool, the sliders, the buttons that act, the chat's Send button and what is on them, darker under the pointer (lighter on a dark surface). The text and icons on an accent are your `primaryForeground` when you give one, else white or a dark ink, whichever reads on your colour, so a light yellow gets dark text. Without a `primary` the accents are Widgetic's own green and blue. The canvas board and the live widget previews stay light on purpose (they show the widget as your customers will see it), and the chat's message bubbles keep their own light colours. The handles the canvas draws on its board, a few accents the creator sets in script and the colours of a status (the green of a published widget, the red of an error) do not take your brand colour, and most of its corners are Widgetic's own, so `radius` changes very little of it. The console says what stays light when a creator gets a dark theme. The File Browser and the composition editor follow a dark theme.
* **No layout or icons.** The components are Widgetic's. The words of the File Browser and of the composition editor can be changed (see the next section); the widget creator is in English.
* **Colours the piece can read as they are.** No CSS variables, gradients or images.
* **The old `ThemeConfig` shape is gone.** `{ mode, colors: { secondary, surface, text }, spacing }` never changed anything in a piece and `theme` no longer takes it. Use the settings above.

## Show the pieces in your customers' language

The File Browser, the composition editor, the widget creator and Workflows can speak your customers' language. Every word they show is a message with a key. You hand over the words you want to change, and every key you leave out stays English, so you can add a language one screen at a time. The examples are the File Browser's; [The composition editor](#the-composition-editor), [The widget creator](#the-widget-creator) and [The Workflows screen](#the-workflows-screen) say what is different.

```javascript theme={null}
const fileBrowser = await mountModule('file-browser', {
  container: document.querySelector('#media-library'),
  getToken,
  locale: 'pl', // plural forms, numbers, dates and file sizes
  messages: {
    'action.cancel': 'Anuluj',
    'action.addToEditor': 'Wybierz', // the button that hands the pick to your page
    'toolbar.deleteMany': '{count, plural, one {Usuń # plik} few {Usuń # pliki} many {Usuń # plików} other {Usuń # pliku}}',
  },
});

// later, while the piece is on screen, without mounting it again:
fileBrowser.setLanguage({ locale: 'de', messages: germanMessages });
```

| Setting | |
| - | - |
| `locale` | A BCP 47 tag (`de`, `pt-BR`). It decides the plural forms and how numbers, dates ("2 days ago") and file sizes are written. For twelve languages it also brings the words (see [Languages that come with the pieces](#languages-that-come-with-the-pieces)); for any other it translates nothing. Blank or left out is English. It is also the `lang` of the piece's elements, so a screen reader says the words in that language, and a language written right to left (`ar`, `he`, `fa`, `ur` and others) turns the whole screen of a piece around: see [Right to left](#right-to-left). `new WidgeticUI({ locale })` hands it to every piece of the page. |
| `messages` | The words you want to change, as `{ 'action.cancel': 'Anuluj', ... }`. |
| `selectLabel` | The text of the button that hands the pick to your page, for a page that only wants to rename that button (default `Select`). `messages['action.addToEditor']` does the same and wins when both are given. |
| `setLanguage({ locale?, messages? })` | Changes the language at once, with no new mount. A value you leave out keeps what it was. The `messages` you give **replace** all the earlier ones: a page that switches from German to French passes the French only, and `setLanguage({ locale: '', messages: {} })` goes back to English. |

### Languages that come with the pieces

Twelve languages are built in: French (`fr`), German (`de`), Spanish (`es`), Brazilian Portuguese (`pt-BR`), Dutch (`nl`), Danish (`da`), Swedish (`sv`), Simplified Chinese (`zh-CN`), Arabic (`ar`), Hebrew (`he`), Persian (`fa`) and Urdu (`ur`). When your `locale` is one of them, or a regional form of one (`fr-CA` reads French, `pt` and `pt-PT` read `pt-BR`, `zh-Hans` reads `zh-CN`, `ar-EG` reads Arabic, and the old tag `iw` reads Hebrew), the piece downloads that language's words as one small file from the same folder of the CDN as the piece itself, and its first paint is already in that language. Any other `locale` (English, `zh-TW`, a tag nobody wrote words for) downloads nothing and the words stay English: plural forms, dates and direction still follow `locale`.

* **Your `messages` win.** The built-in words come first and yours replace them, key by key.
* **They are provisional.** The built-in words are machine translations that no native speaker has read yet. `module.localePackStatus('de')` says `'provisional'` (and `null` for a language with no words), and `module.supportedLocales` lists the twelve. Tell [support@widgetic.com](mailto:support@widgetic.com) about a word that is wrong, with the piece, the `locale` and the message key.
* **A file that cannot be downloaded** is said once in the console, and the piece goes on in English.

### Which keys there are

`fileBrowser.module.defaultMessages` is an object of every key and its English text: a few hundred messages, grouped by where the words appear (`action.`, `upload.`, `dialog.`, `preview.` and so on). It always matches the version of the piece you load, and it is the list to hand to a translator.

```javascript theme={null}
console.log(JSON.stringify(fileBrowser.module.defaultMessages, null, 2));
```

### Writing a message

A message is text. `{name}` is a value the File Browser puts in; a number is written the way the locale writes numbers. `{count, plural, one {# file} other {# files}}` picks the form that fits the number in your customer's language: `other` is required, `=0 {none}` matches exactly 0, `#` is the number, and a language with more forms says so (Polish has `one`, `few`, `many` and `other`). `\{`, `\}`, `\#` and `\\` write the character itself. A translation keeps the `{names}` of the English message, in any order, and cannot ask for a value the message is never given.

### Checking your words

`fileBrowser.module.checkMessages(messages)` says what is wrong without showing anything: one sentence for each key that does not exist (with the nearest real one), value that is not text, message that does not parse, or `{name}` the message is never given. An empty list means every message will be used. Put it in a test of your page.

A message that cannot be used never breaks the screen: the English text of that key is shown and the console says so once. A value of the wrong shape (`messages: 'Anuluj'`, `locale: 5`) is a mistake in your code and is refused where you wrote it: `setLanguage` throws a `TypeError` that names the option and changes nothing, and a mount call fails with `MOUNT_FAILED` and the `TypeError` as its `cause`.

### Pop-ups

The File Browser answers a rename, a delete, a new folder and a pick with a small pop-up in the bottom corner of the box you gave it (the right one, or the left one for a language written right to left), in the piece's theme and in your language. Their words are messages like the rest: the `toast.` keys, with `toast.regionLabel` (the name of the region a screen reader announces them in) and `toast.closeLabel` (the name of the close button). After a pick the piece says "Files selected", not the Widgetic site's "Added 2 files to the editor". The sentence under "Rename failed" or "Failed to delete" is your library's own and is shown as it arrives.

Two habits come with the pop-up library. It moves focus to the newest pop-up when someone presses Alt+T anywhere on the page while one is shown, and a pop-up goes away by itself after about three seconds. You cannot move the pop-ups or switch them off yet.

### The composition editor

The composition editor takes the same `locale`, `messages` and `setLanguage`, and its `module.defaultMessages` and `module.checkMessages(messages)` work as above. One thing is different: **one `messages` object covers the whole editor.** The editor is made of three parts with words of their own, and it splits what you give between them, so you never need to know which part says a word:

* the editor's own words, the `editor.` keys: the Design and Content tabs, every control, the save button, the dates and the failures it explains;
* the file picker that opens from its file fields, which is the File Browser: the keys above (`action.`, `upload.`, `dialog.` and so on), with the same `locale` and `messages`;
* the rich text editor of its text fields, the `shadEditor.` keys. The editor draws its own text toolbar, so these words are not on screen today and a translation can leave them out.

`editor.module.defaultMessages` lists them all, with their English text: more than 500. A key that is in none of the three is said once in the console as `[Editor] ...`, with the nearest real key, and ignored.

```javascript theme={null}
const editor = await mountModule('composition-editor', {
  container: document.querySelector('#editor'),
  compositionId,
  getToken,
  locale: 'de',
  messages: {
    'editor.props.save': 'Speichern', // a word of the editor
    'action.cancel': 'Abbrechen', // a word of its file picker
  },
});

// labels and buttons change at once, and what the customer typed stays
editor.setLanguage({ locale: 'pl', messages: polishMessages });
```

* **Dates follow `locale` without your help.** The names of the months and weekdays in its calendars, and the way a date is written, come from the browser. Only the names of the calendars and of their buttons (what a screen reader says) are messages. The date range control shows dates the way the language writes them: 7 October 2026 reads `10/07/26` with `en` (and with no `locale`), `07/10/26` with `en-GB`, `07.10.26` with `de` and `pl`, and `26/10/07` with `ja`, so give `en-GB` when you want the day first in English. A weekday heading in a calendar is the language's own short name, not cut to two letters. In a language written right to left it is the one-letter name instead (Arabic `ح ن ث ر خ ج س`), because the short names of Arabic are whole words that do not fit a column, and the whole name is the label of the heading for a screen reader. Where the browser has no one-letter names for a language (Urdu), the short names stay. Values saved in the composition stay structured dates, the same in every language.
* **A change of language reaches what is on screen.** Labels and buttons change at once, and so do the banners of a composition that could not be opened and of a save that failed. A sentence the editor said once, such as why there is no preview, keeps its language until the editor says something new.
* **Pop-ups from its file picker.** A rename, a delete, a new folder and a pick raise a small pop-up inside the module's box, in your language (`toast.` keys, including `toast.regionLabel` and `toast.closeLabel`). After a pick the module says "Files selected", not "Added … to the editor".

### The widget creator

The widget creator takes the same `locale`, `messages` and `setLanguage`, and its `module.defaultMessages` and `module.checkMessages(messages)` work as above. Like the composition editor, **one `messages` object covers the whole creator.** It is made of parts with words of their own, and it splits what you give between them, so you never need to know which part says a word:

* the creator's own words, the `creator.` keys: the widget's panel, publishing and versions, the chat's generation options (the language and, in debug mode, custom libraries), the limits it explains, the lines of its status bar; the preset **Libraries** toggle and its add-library control appear only in creator debug mode, which the widget creator module does not turn on, but their messages stay in the catalog for hosts that do;
* the chat's words (`chat.` keys) and the canvas's (`canvas.` keys: the toolbar, the zoom controls, the context menu, the properties panel, the words the shapes draw on the canvas);
* the words of the starting-point picker that a customer with nothing yet is shown, the `presetPicker.` keys (`presetPicker.heading`, `presetPicker.intro`, `presetPicker.startFromScratch` and so on), and the two words of the region its pop-ups are announced in, `toast.regionLabel` and `toast.closeLabel`, which are the File Browser's keys of the same names, so one translation serves both;
* the composition editor with its file picker and its rich text editor (the `editor.` keys, the File Browser's and `shadEditor.`). The creator's composition steps are off in a session, so these are on no screen today and a translation can leave them out.

`creator.module.defaultMessages` lists them all, with their English text: more than 1,300.

```javascript theme={null}
const creator = await mountModule('widget-creator', {
  container: document.querySelector('#creator'),
  getToken,
  locale: 'pl',
  messages: {
    'canvas.tool.select': 'Zaznacz', // a word of the canvas
    'chat.input.placeholder': 'Wpisz wiadomość...', // a word of the chat
    'creator.common.setLive': 'Ustaw jako aktywną', // a word of the creator
    'presetPicker.heading': 'Od czego chcesz zacząć?', // a word of the picker
  },
});

// everything that is open changes at once, and what the customer made stays
creator.setLanguage({ locale: 'de', messages: germanMessages });
```

* **Dates follow `locale` without your help.** The date and time of a version are written the way the language writes them (`10/01/2026 14:05` in English, `01.10.2026 14:05` in Polish, `2026/10/01 14:05` in Japanese). Only the words around them are messages.
* **A change of language reaches what is on screen.** The canvas, the chat, the panels, the menus, the picker and the pop-ups change at once, with no new mount. As for the other pieces, `setLanguage` keeps what you leave out and replaces the messages.
* **`presetLabels` still works.** It is a shortcut for the picker's eight messages and sits under `messages`: when both say a word, `messages` wins, and it stays in force after `setLanguage` for the words the new messages leave out.
* **A mistake never breaks the screen.** A key that none of the parts has is said once in the console as `[Creator] ...`, with the nearest real key. A text that cannot be used is said once by the part that owns the key (`[Creator]`, `[Chat]`, `[Canvas]`, `[Editor]`, `[FileBrowser]`, `[ShadEditor]`, or `[WidgetCreatorModule]` for the picker and the pop-up region), and the English text of that message is shown.

### The Workflows screen

Workflows takes the same `locale`, `messages` and `setLanguage`, and its `module.defaultMessages` and `module.checkMessages(messages)` work as above. It has **one** `messages` object for all of it: a few hundred messages whose keys start with `workflows.`, for the list, the editor and its panels, the names of blocks, operators and units, what is wrong with a workflow that cannot be published, what a failed request means, and the words of the screen that says a part could not be drawn. The words of a block are made of parts the screen puts together (`{field} {value}`, `{count, plural, ...}`), so a language that puts them in another order can say so in its own message. All twelve [built-in languages](#languages-that-come-with-the-pieces) come with these words. A key you leave out stays English.

### Right to left

The File Browser, the composition editor, the widget creator and Workflows are read the way your customer's language is written. For Arabic, Hebrew, Persian, Urdu and the other languages written from right to left, the whole screen turns around and a line starts on the right. `setLanguage({ locale: 'ar' })` turns a piece that is on screen around, and `setLanguage({ locale: 'de' })` turns it back, with no new mount.

```javascript theme={null}
const fileBrowser = await ui.mount('file-browser', { container: '#media', getToken, locale: 'ar' }); // read right to left from the first paint
```

* **The File Browser.** The list starts on the right, the search box, the sort and filter controls, the selection bar, the upload panel and the windows are laid out from the right, the close button of a window is in the corner where a line starts, menus open toward the end of the line, the pop-ups come up in the bottom left corner, and the arrows that mean back and forward are mirrored. In the preview, the arrow key that points toward the end of the line goes on to the next file.
* **The composition editor.** The settings panel is docked at the start of the line, on the right, and the preview on the other side, the same distance from the middle as in English. The Design and Content tabs run from the start of the line, the label of a control is at the start of its row, the calendars start their week at the start of the line, and sliders and switches run from the start. The file picker that opens from a file field is the File Browser above.
* **The widget creator.** The tools of the toolbar run from the start of the line, the properties panel is docked at the end of it, the menus, the list of shapes, the publish history and the starting-point picker are laid out from the start. The composition editor and the File Browser inside it are turned around the same way.
* **Workflows.** The list, the top bar of the editor, the tabs, the side panel, the menus and the dialogs are laid out from the start of the line, as a whole. **The canvas is the exception**: a workflow is read from its first block forward, so it is drawn from left to right in every language, and its lines run from a block on the left to the next one on its right.
* **What a customer types.** A text field, the tags of the editor, the chat and the text fields of the quick edit are read by their own first letter: a sentence in Latin letters is set from the left and one in Arabic from the right, side by side. A field with nothing in it yet takes the direction of the screen. An email, a link and a colour code stay left to right on every screen.
* **Only `locale` decides.** The direction of your page does not: a page written right to left (`<html dir="rtl">`) leaves an English piece alone, and a page written left to right does not stop an Arabic one. Changing `messages` alone changes nothing about it.
* **Names.** File and folder names are read by their own first letter, so a Latin name next to Arabic ones is not put in the wrong order. In a language written right to left, a name inside a sentence ("Failed to delete 2024 report.pdf") sits between two invisible marks (Unicode isolates U+2068 and U+2069), so the words around it cannot turn it around. A screen reader does not say them; leave them out when a test compares the text of a pop-up.
* **The video time line** stays left to right, because time runs that way in every language.
* **Drawings stay left to right.** The drawing board of the widget creator, the live previews of widgets and the code are left to right in every language, and so are the drawings that mean "left" and "right" in the composition editor (the saturation and hue of the colour picker, the grid that places a box, the corners of a rounded box). A drawing is not a line of text, and a widget's own preview must look the way its author made it.
* **Two small things are open.** The weekday names of an Urdu calendar are the short ones, which are whole words because the browser has no one-letter names for Urdu, so that row is tight. And the panel of a widget in the creator opens on the side of its shape that has more room, as it does in English, without knowing where the properties panel is: for a shape near the middle of the board it can open over that panel.

### What stays as it is

* **Words that belong to someone else.** What your own media library says (the message of an error you throw), your file names and your library's `label`, the names of folders a customer made, and a sentence Widgetic's API sends back are shown as they arrive.
* **`error` events.** Their `message` is English: it is for your logs.
* **What is saved with a composition.** The title of a new item ("Item 03"), "Untitled Composition" and the names a font weight or a position is saved under ("ExtraBold", "top-left") are written into the composition when they are made, so they stay English. The editor shows the weights and the positions in your language.
* **The widget author's words.** The names, labels, descriptions and placeholders of a widget's settings, its default texts and the names of typefaces are shown as the author wrote them.
* **The team's words in Workflows.** The names of workflows and of compositions, the event names a team types into a trigger, and the names of the tables, columns and actions of a widget are shown as they were written.
* **What the customer and the AI write.** The prompts, the answers, the names of widgets and the code are the customer's and the AI's. In the widget creator, lines the creator saved into the chat stay English in the API; on screen it translates known lines (a failed generation, a restore) at display time. The `message` of its `limitReached` event stays English (use `code`).
* **What a session never draws.** The widget creator's sign-in screen, its standalone header, its debug panels, the preset **Libraries** section of the chat's generation options, its list of keyboard shortcuts and its composition steps are not shown in a session, so they are not translated.

## Tokens

`getToken` is the only credential the SDK takes. It is called with `{ reason }`:

* `'initial'`: the piece is starting.
* `'expiring'`: the current token runs out in about a minute. Return its replacement now.
* `'expired'`: the current token has already run out (the page was asleep, or an earlier refresh failed).

Return the token itself, or `{ token, expiresAt }` (an ISO string, epoch milliseconds or a `Date`). With `expiresAt` the piece renews the token in time and the customer is never interrupted. The answer of your own endpoint can pass on what Widgetic gave you.

When your `getToken` answers an `'expired'` call with the very token the API had refused, the piece takes it that you have no better one. It does not ask again for five seconds, whatever asks, so a session that was ended is reported once and not once per request.

An `apiKey` option is refused with `API_KEY_NOT_ALLOWED`. The key must stay on your server.

[Workflows](#workflows) takes an editor session with the `workflows` surface, like the other pieces, or the token of a signed-in person, and asks for it in the same way.

## Show your own media library

If your platform already has a media library (a CMS, an S3 bucket, a DAM), give the File Browser plain functions that read and write it. Your customers browse, upload and pick from **your** files with the same screen. The piece then never talks to Widgetic, so there is no session to make and no `getToken`.

```javascript theme={null}
const fileBrowser = await mountModule('file-browser', {
  container: '#media-library',
  mediaLibrary: {
    id: 'acme-assets',
    label: 'Acme assets', // shown as the list's title. Default "My uploads"
    // Required. One page of files, newest first. `cursor` is the `nextCursor` you returned for the page before.
    list: async ({ cursor, limit, signal }) => {
      const response = await fetch(`/api/assets?cursor=${cursor ?? ''}&limit=${limit}`, { signal });
      if (!response.ok) throw Object.assign(new Error('Could not load your files'), { status: response.status });
      const page = await response.json();
      return {
        items: page.assets.map((asset) => ({ id: asset.id, name: asset.fileName, url: asset.publicUrl, mimeType: asset.contentType })),
        nextCursor: page.next ?? null, // null: that was the last page
      };
    },
    // Optional. A button is shown only for the functions you give.
    upload: async ({ file, onProgress, signal }) => uploadToAcme(file, onProgress, signal), // return the stored file
    rename: async ({ id, name, signal }) => renameInAcme(id, name, signal),
    delete: async ({ id, signal }) => deleteInAcme(id, signal),
  },
  on: {
    select: ({ files }) => useFiles(files), // your files, as your library described them
    error: ({ message, code, action }) => report(code, action, message),
  },
});
```

* A failure inside your functions is reported through the `error` event with `fatal: false`. The File Browser shows the customer a list that cannot be read (with "Try again"), an upload that fails (in the upload panel) and a `rename` or `delete` that your library refuses (in a pop-up, with your library's sentence under it): the file stays as it was, and your page hears of it as well, so use the event for your log, not for a second message. The code is `MEDIA_LIBRARY_` plus `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `NETWORK`, `INVALID_RESPONSE`, `NOT_SUPPORTED` or `FAILED`, and `action` says what the customer was doing (`load`, `upload`, `rename` or `delete`). Throw an error with a `status` property (a failed `fetch` response) and it is sorted for you.
* Everything your functions return is checked before it is shown. A file without an id, a name or a usable address is skipped with a console warning rather than breaking the list.
* The `url` of a file is what a widget stores and shows. Once your customer saves or publishes, everyone who sees the widget loads that address, not only the customer who picked it: give one that stays valid and needs no sign-in. A link that expires after a few minutes, or one that checks a cookie, breaks the published widget. Widgetic keeps the `id` with the file only so that the picker shows it as selected when it is opened again.
* The composition editor and the widget creator take the same `mediaLibrary`: their file pickers then show your files.
* Text, PDF and font previews read the file with a script, so a file on another website needs an `Access-Control-Allow-Origin` header for your page's origin. Pictures, audio and video are shown without it.

## Workflows

`workflows` is the screen where a team builds workflows: compositions chained together, with filters between them that decide which composition a visitor sees next. It opens on the list of the workspace's workflows (search, sort, show or hide the archived ones, new, blank or from a starting point you offer, duplicate and delete in any state), and a workflow opens in the editor: a canvas of blocks that go forward only (up to 25), a side panel for the block that is selected, a draft that is saved while you work, **Publish** (a published version never changes) and **Discard changes**, the history of versions, a **Test run** that plays a visitor through the draft, a **Runs** tab that counts the visitors who went through each block, and **Settings** (time zone, embed code, pause, archive). What each version of the screen changes is in [Versions & Changelog](https://widgetic.com/docs/partners/module-versions.md).

What a workflow does **on a page** is the work of the Embed SDK: a workflow only decides which composition the embed shows next, and the embed follows it from one more attribute on the tag you already use. See [Show a path of compositions in one place](https://widgetic.com/resources/sdks.md#show-a-path-of-compositions-in-one-place).

To give the screen to your customers, one at a time, with an editor session, see [Workflows](https://widgetic.com/docs/partners/workflows.md). The rest of this section is true for a session and for a signed-in person alike, except where it says otherwise.

```javascript theme={null}
import { mountModule } from '@widgetic/ui-sdk';

const workflows = await mountModule('workflows', {
  container: document.querySelector('#workflows'), // give it a height: the screen fills it and scrolls inside it
  workspaceId: 'personal', // always 'personal' with an editor session. A person may give the id of the organization whose workflows are shown
  getToken: async ({ reason }) => (await fetch(`/my-server/widgetic-token?reason=${reason}`)).text(),
  workflowId: workflowOfTheAddress(), // optional: the workflow to open first. Leave it out for the list
  theme: 'system',
  on: {
    ready: () => console.log('Workflows is on screen'),
    openWorkflow: ({ workflowId }) => history.pushState(null, '', workflowId ? `/workflows/${workflowId}` : '/workflows'),
    showEmbedCode: ({ workflowId }) => showMyEmbedCodeDialog(workflowId),
    error: ({ message, code, fatal }) => report(code, message),
  },
});

// The page owns where the person is: when it moves (the back button), tell the screen.
window.addEventListener('popstate', () => workflows.module.setWorkflow(workflowOfTheAddress()));

// later:
await workflows.unmount();
```

* **`workspaceId` is required.** It is `'personal'`, or the id of the organization (the team) whose workflows are shown and edited. Workflows is for team accounts and partners: the API refuses any other workspace, and the screen says so. **With an editor session it is always `'personal'`:** a session works in that one workspace, and the workflows it shows are its customer's own. Any other `workspaceId` is refused with `403` and `EDITOR_SESSION_ROUTE_NOT_ALLOWED`.
* **`getToken` gives an editor session's token, or the token of a signed-in person.** For one of your customers it resolves with the token of an editor session made for them with the `workflows` surface (see [Workflows](https://widgetic.com/docs/partners/workflows.md)): the screen then reaches that customer's own workflows and compositions, and nothing else. For your own team it resolves with the token of a signed-in person who may work in that workspace (a member of the team, or your own account), the way the Widgetic app itself calls the API. **An editor session that was made without the `workflows` surface is refused** with `403` and `EDITOR_SESSION_ROUTE_NOT_ALLOWED`. `getToken` is called once before the screen appears (a `getToken` that fails fails the mount), and again when the token is about to run out or the API refuses it (`reason: 'expired'`, once per refusal), as in [Tokens](#tokens). The screen never stores a token, and no API key ever reaches the page.
* **Your page owns where the person is.** The screen opens `workflowId` first, and the list when you leave it out. When the person opens a workflow or goes back to the list, the screen says so with `openWorkflow` (`workflowId` is `null` for the list), so your page can keep its address in step. When your page moves, as with the back button, `workflows.module.setWorkflow(workflowId)` moves the screen (`null` for the list). The screen writes nothing to your address bar, your storage or your cookies.
* **The embed code is yours to show.** The screen has its **Embed code** button only when something listens for `showEmbedCode` at the moment it mounts, and only for a workflow that has been published. It tells your page which workflow, and your page shows the tag, because it knows how its own embed is built: the tag of the workflow's first composition with `workflow-id` added, as in [Show a path of compositions in one place](https://widgetic.com/resources/sdks.md#show-a-path-of-compositions-in-one-place).
* **Look and language are as for the other pieces.** `theme`, `setTheme`, `locale`, `messages` and `setLanguage` work as described in [Make the pieces look like your product](#make-the-pieces-look-like-your-product) and [Show the pieces in your customers' language](#show-the-pieces-in-your-customers-language), the twelve built-in languages included, and a language written right to left turns the screen around.
* **Events.** `ready`, `openWorkflow` and `showEmbedCode` (above), and `error` as in [Events](#events). `error` has `fatal: true` when the screen could not start (and `mountModule` rejects as well). With `fatal: false` the screen carries on: `RENDER_FAILED` (a part of it could not be drawn: the rest stays and the person is offered "Try again" in its place), `REQUEST_FAILED` (a request to the API failed: the screen tells the person by itself), `SESSION_ENDED` (the API no longer accepts the token `getToken` gives) and `TOKEN_REFRESH_FAILED` (a refresh of the token failed and will be tried again).
* **What it weighs.** It is downloaded when you mount it, and only then: about 0.2 MB gzipped to start, and about 0.3 MB gzipped once all the parts of the editor have been loaded.

## Content Security Policy

If your page sends a Content-Security-Policy, the File Browser works with this and nothing more. The build tests load it under the same policy, list, pick and upload images and preview a PDF; each source below is needed.

```text theme={null}
default-src 'none';
script-src  'self' https://cdn.widgetic.com;           # plus whatever your own page needs
connect-src https://cdn.widgetic.com https://api.widgetic.com;
style-src   'unsafe-inline';                           # the piece's styles are <style> elements in its shadow root
font-src    data:;                                     # its fonts are embedded
img-src     https://cdn.widgetic.com data: blob:;      # data: for its icons, blob: for an upload's preview
worker-src  blob:;                                     # only to preview a PDF: pdf.js reads it in a worker made from a blob
```

`img-src` also needs the address your customers' files are served from (the `url` of a picked file). To preview a PDF, that address also goes in `connect-src` (the PDF is downloaded with a request, so the file server has to allow your page's origin with CORS) and the policy needs the `worker-src blob:` above; without it the preview says "Failed to load PDF" and everything else works. No `data:` in `script-src` and no `'unsafe-eval'` are needed. The PDF viewer is downloaded when the first PDF is previewed, from the same folder of the CDN as the piece itself, so `script-src` needs nothing more.

* The **composition editor** works under the same policy plus `frame-src https://cdn.widgetic.com` (the live preview is the widget's published page). Its font picker also needs `style-src https://fonts.googleapis.com` and `font-src https://fonts.gstatic.com`.
* A **font of your own** (`theme.font.stylesheetUrl`) needs its stylesheet's host in `style-src` and its font files' host in `font-src`. See [A font your page does not load yet](#a-font-your-page-does-not-load-yet).
* The **widget creator** needs what the composition editor needs plus `frame-src https://wbuilder.widgetic.com` and `connect-src https://wbuilder.widgetic.com` (the live previews of widgets are served from there).
* **Workflows** needs less than the File Browser: `script-src`, `style-src 'unsafe-inline'` and `font-src data:`, plus two sources that the first version of the screen did not need. `connect-src` has to allow the API (your `apiUrl`, `https://api.widgetic.com` unless you run your own), and `img-src https://cdn.widgetic.com` is for the pictures of compositions in the list and on the canvas. It needs no `frame-src`, no `worker-src`, no inline script and no `eval`. Together: `default-src 'none'; script-src 'self' https://cdn.widgetic.com; connect-src https://cdn.widgetic.com https://api.widgetic.com; style-src 'unsafe-inline'; font-src data:; img-src https://cdn.widgetic.com`. The `connect-src https://cdn.widgetic.com` is the SDK's request for the manifest, as for every piece, and it is not made when you pin a `version`. **A page that allowed only what version 0.1.5 needed has to add the API and the pictures.**

## What the SDK puts on your page

* On import: nothing.
* While a piece downloads: one `div[data-widgetic-ui-loading]` in your container, removed when the piece is up.
* A mounted piece: one `<widgetic-ui-module>` element in your container, holding a shadow root. The pieces also add one `<style data-widgetic-ui-document-rules>` to `<head>` with `@font-face` rules for fonts named `Widgetic UI …`, so they cannot collide with yours. That `<style>` is shared by every piece on the page and never added twice, so it stays when a piece is unmounted. The composition editor also adds Google's stylesheet for each font the customer looks at in its font picker; those `<link>`s go when the editor is unmounted. Your own `<link>`s and `<meta>`s are never touched.
* Never: global CSS or CSS variables, changes to `<html>` or `<body>`, `postMessage`, or removing your own elements.

## React

```jsx theme={null}
import { useEffect, useRef } from 'react';
import { mountModule } from '@widgetic/ui-sdk';

export function MediaLibrary({ getToken, onPick }) {
  const containerRef = useRef(null);
  const onPickRef = useRef(onPick);
  onPickRef.current = onPick;

  useEffect(() => {
    const controller = new AbortController();
    let mounted = null;

    mountModule('file-browser', {
      container: containerRef.current,
      getToken,
      signal: controller.signal,
      on: { select: ({ files }) => onPickRef.current(files) },
    }).then(
      (handle) => { mounted = handle; },
      (error) => { if (error.name !== 'AbortError') console.error(error); },
    );

    return () => {
      controller.abort();   // before the piece is running: it never appears
      mounted?.unmount();   // after: it is taken down
    };
  }, []); // StrictMode runs this twice; the first run is cancelled cleanly

  return <div ref={containerRef} style={{ height: 600 }} />;
}
```

The same component mounts the other pieces: give `mountModule` the piece's name (`'composition-editor'`, `'widget-creator'` or `'workflows'`) and its options. The Widgetic build tests run it in a React 18 page, with and without `<React.StrictMode>`, for all four pieces. The same pattern works in Vue and Svelte: mount in the component's mount hook with an `AbortController`, and abort and unmount when it goes away. Mounting needs a page, so start it from an effect or an event handler: calling `mountModule` while rendering on a server rejects with `NOT_IN_BROWSER`.

## Versions and self-hosting

Pieces are static files, so nothing can change under your feet when you pin a version:

```text theme={null}
https://cdn.widgetic.com/modules/<name>/manifest.json            { "name": "file-browser", "version": "0.1.0", "entry": "0.1.0/index.js" }
https://cdn.widgetic.com/modules/<name>/changelog.json           what changed in each version, up to the one the manifest names, newest first
https://cdn.widgetic.com/modules/<name>/<version>/index.js       never changes once published
https://cdn.widgetic.com/modules/<name>/<version>/chunks/<file>.js    the parts a piece fetches later (the PDF viewer); never change either
```

* Without `version` the SDK reads the manifest and loads the newest version. With `version: '0.1.0'` it loads that file directly. **Pin the version in production** and upgrade when you have looked at the release.
* **Read what changed before you move.** Every version has an entry in the changelog, and a version that can break your page is marked `breaking` and is always a new major version (a new minor version while the number starts with `0`). See [Versions & Changelog](https://widgetic.com/docs/partners/module-versions.md).
* To serve the pieces yourself, copy that layout to your own origin (the whole `<version>` folder: `index.js` fetches its `chunks/` files from the folder it was loaded from) and set `moduleBaseUrl`. The manifest's `entry` must be a `.js` file inside the piece's own folder on the same origin.
* Failed downloads are retried twice (network errors and server errors, not 404). A piece also asks twice more, the same way, when a file it fetches later fails. Requests to the CDN carry no cookies.

## Errors

Every error carries a `code`.

| Code | Meaning |
| - | - |
| `API_KEY_NOT_ALLOWED` | An `apiKey` option was given. Make an editor session on your server instead. |
| `INVALID_GET_TOKEN` | `getToken` was given but is not a function. |
| `NOT_IN_BROWSER` | Mounting on a server. |
| `MISSING_CONTAINER`, `CONTAINER_NOT_FOUND`, `INVALID_CONTAINER` | `container` is missing, matches nothing, or is not an element. |
| `INVALID_MODULE_NAME`, `INVALID_VERSION`, `INVALID_MODULE_BASE_URL` | The name, the pinned version or the module address is not valid. |
| `MODULE_NOT_FOUND` | The CDN has no such piece. |
| `MANIFEST_UNAVAILABLE`, `MANIFEST_INVALID` | The manifest could not be fetched, or it is not valid. |
| `MODULE_LOAD_FAILED`, `MODULE_INVALID` | The file could not be imported, or it does not export a piece. |
| `MOUNT_FAILED` | The piece loaded but failed to start. Nothing is left in the container. |
| `MOUNT_ABORTED` | The mount was cancelled through `signal`. |
| `INVALID_THEME` | A theme cannot be used: `updateTheme` was given something that is not a mode or settings, or a piece refused the settings (`setTheme`, a mount call). The message names the setting. |

## Next Steps

> [card] [Editor Sessions](https://widgetic.com/docs/partners/editor-sessions.md)
>
> Make the token on your server

> [card] [Canvas Embedding](https://widgetic.com/docs/partners/canvas-embedding.md)
>
> The widget creator and the composition editor

> [card] [Managed Users](https://widgetic.com/docs/partners/managed-users.md)
>
> File your customers' content under them

> [card] [Credits & Billing](https://widgetic.com/docs/partners/credits-billing.md)
>
> How the AI is paid for


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.