The settings page.
An app may bring its own page for its settings, shown in the portal instead of the standard form: a preview of the screen, a slider, a camera layout. The settings are still the manifest's; the page proposes values and nothing is saved until the operator presses Update.
Where it goes
my-app/
app/
manifest.json the settings, as always
gui/ beside app/, never sent to a device
gui.json optional: {"settings": {"title": "Welcome screen", "height": 520}}
icon.svg | icon.png optional: the app's icon
settings/
index.html the settings page
settings.css, settings.js, img/…
- Files: HTML, CSS, JS, JSON, images (svg, png, jpg, gif, webp, avif, ico) and fonts (woff, woff2, ttf, otf); at most 200 files and 5 MB. Anything else, or a symbolic link, and the portal shows the standard form with the reason under it. It never stops the app's deploy.
- Push as usual. A commit that changes only
gui/does not update any device. - It shows in Setup › App settings (scope
project) and on each device's Setup tab for theperDevicesettings (scopedevice). The standard form is always one click away. - Every plan except Free. On Free the portal shows the standard form and says the app has a page of its own.
heightis a first guess; the frame then follows the page (120–2000 px).
What the page can and cannot do
It runs in a sandboxed frame on a separate site, inside the portal only:
- No network.
fetch, XHR, WebSocket, outside images, fonts and scripts are refused. Bundle everything ingui/; the page cannot reach the app's files either, so copy the pictures it needs. - No
localStorage,sessionStorageor cookies: they throw. Wrap any use intry. - No
alert,confirm,prompt, popups,target="_blank", form submission or downloads. Draw your own messages. - One page. Navigating anywhere, even to another file of your own, closes it. Use sections or tabs in one document.
- Opened in a tab of its own, it is refused.
The smallest working page
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<style>
html, body { background: transparent; color: var(--arda-fg); font-family: var(--arda-font); }
[hidden] { display: none !important; }
</style>
<script src="/_arda/gui.js"></script>
</head>
<body>
<label>Shop name <input id="name"></label>
<p id="note"></p>
<script src="settings.js"></script>
</body>
</html>
var name = document.getElementById('name');
var note = document.getElementById('note');
function draw() {
// never overwrite a field the operator is typing in
if (document.activeElement !== name) name.value = ArdaGUI.values.shop_name || '';
}
ArdaGUI.ready(draw); // once the portal has said what is being edited
ArdaGUI.on('values', draw); // after Update, or a change in the standard form
name.onchange = function () { // propose on change, not on every keystroke
ArdaGUI.set('shop_name', name.value.trim()).then(function (r) {
note.textContent = r.ok
? 'Changed — press Update to save'
: Object.keys(r.errors).map(function (k) { return r.errors[k]; }).join(' · ');
});
};
ArdaGUI
ArdaGUI.ready(fn) | fn(ctx) once the portal has sent what is being edited (at once if it has) |
ArdaGUI.definitions | the settings, in the manifest's words: {key, caption, help, type, perDevice, unit, options: [{value, caption}], min, max, item, fields}; a select drawn from the device's own list also carries device ("printers" or "screens"), with that device's list in options on the device scope and an empty one on the project |
ArdaGUI.values | typed: strings for text, memo, color and select; a number; a boolean; rows as an array of objects each carrying _row; null when not set |
ArdaGUI.secrets | per row, whether each secret field holds a value: {cameras: [{password: true}, {password: false}]} |
ArdaGUI.set(key, value), set({k: v, …}) | a proposal; resolves {ok, errors}. A refused value is taken back out of values. |
ArdaGUI.scope | 'project' or 'device' (only the perDevice settings arrive) |
ArdaGUI.following, ArdaGUI.inherited, ArdaGUI.follow(key) | device scope: which settings follow the project, the project's values, and going back to the project's value |
ArdaGUI.on('values' | 'view' | 'theme', fn) | values changed; the larger view opened or closed; the theme changed |
ArdaGUI.expand({title}), collapse(), expanded | the larger view (the portal's panel caption already has a button for it) |
ArdaGUI.resize(h), autoResize(false) | the frame's height, which otherwise follows the page |
- Look like the portal:
gui.jssets<html data-theme="dark|light">, the colour scheme and--arda-bg --arda-fg --arda-muted --arda-border --arda-accent --arda-font. Keep the background transparent; the portal draws the panel. No title of your own. - Two widths: the panel is about 500 px, the larger view about 1300. One column by default, with a
@media (min-width: 980px)layout for the larger view. - Height follows
<body>: do not givebodyheight: 100%, or it can never shrink. - The
hiddenattribute loses to a class that setsdisplay; the[hidden]rule above fixes that. - On the device scope, hide controls for settings you were not given:
ArdaGUI.definitions.some(function (d) { return d.key === key; }). A proposal for a key the manifest does not declare is refused. - A top-level
numberhas no range in the manifest; hold the range in your controls and clamp before you propose.
Passwords and secret fields
- A
rowsfield markedsecretarrives asnull;ArdaGUI.secrets[key][i][field]says whether that row has one. Send rows back with their_rowand the secret leftnullto keep it; a new row has no_rowand brings its own. The page may replace a secret, never read it. - A plain
textsetting holding a password is not secret: the page receives it as the standard form does. Mask the input, and never echo it in a preview.
Without the helper
var port = null;
window.addEventListener('message', function (e) {
if (port || !e.data || e.data.arda !== 'gui' || e.data.type !== 'hello' || !e.ports[0]) return;
port = e.ports[0];
port.onmessage = function (m) { /* m.data.type: init · values · result · view · theme */ };
port.postMessage({ type: 'ready', v: 1 });
});
// then: port.postMessage({type: 'set', id: 1, values: {shop_name: 'High Street'}})
- Portal → page:
init {v, page, scope, project, device, definitions, values, secrets, following, inherited, readOnly, theme},values,result {id, ok, errors},view {expanded},theme. - Page → portal:
ready {v: 1},set {id, values},follow {id, keys},resize {height},expand {title?},collapse. - The portal says hello with a fresh port until the page answers
ready, for 5 s. Messages up to 256 KB, about 50 a second. A page withoutgui.jsshould setcolor-schemeitself.
On your desk
$ ardaplayer -dev .
level=INFO msg="settings page: http://127.0.0.1:41729/__arda/gui/"
When gui/settings/index.html exists, developer mode (v1.1.13 and later) prints its address. The harness hosts the page the way the portal does — the same sandbox, headers, protocol and checks — and Apply puts the accepted values into the running app's Arda.config and fires 'config', in memory only; arda.dev.json is never rewritten. The harness shows the values it sends, so try the page on a copy with fake passwords.
Checked: ArdaGUI's members are from the helper's source (internal/devserve/gui.js, a byte copy of the portal's); the walls and the protocol are from the player's manual and the harness code. The calls are shaped like those of a settings page in use in the portal today. The page itself was not opened in the portal for this manual.