Appearance
window.KappappsPrefs (prefs panel custom)
Le dev peut fournir un script/document exécuté dans le formulaire de préférences du streamer (à la Stream Deck Property Inspector), avec lecture/écriture temps réel des préférences via window.KappappsPrefs. Deux étages, cumulables : un script global (déclaré dans infos.json, monté au-dessus de tout le formulaire) et des zones WU customPanel (ciblées dans l'arbre de préférences, preferences.config.js).
Déclaration (infos.json)
prefs-panel(dossier) /prefs-panel-index(fichier d'entrée) — miroir destreamer-plugin/streamer-plugin-index. Monté au-dessus de tout le formulaire, peu importe la validité de la structure de préférences.- ⚠️ La zone du script global démarre repliée à 0 px : appelle
KappappsPrefs.resize(h)ouautoResize()dansonReadypour la rendre visible. Un script purement logique (sans UI) peut simplement ne jamais resize — il reste invisible, c'est voulu. - Le fichier d'entrée (et
wuOptions.sourced'uncustomPanel, voir plus bas) peut être.html/.htm(servi tel quel) ou.js(coquille HTML générée par le serveur, CSP durcie, charge le SDK puis ton script — voir « Sucre.js» plus bas).
WU customPanel (zone ciblée)
Dans preferences.config.js :
js
demoPanel: {
type: 'customPanel',
label: { en: 'My panel', fr: 'Mon panneau' },
wuOptions: { source: 'zone.js', height: 200, autoResize: true }
}wuOptions.source(obligatoire) : fichier relatif au dossierprefs-panel(.html/.htm/.js) — pas de traversal (.., chemin absolu).wuOptions.height: hauteur initiale en px, clampée 0–2000 (défaut 260).wuOptions.autoResize:truelaisse le panneau piloter lui-même sa hauteur (KappappsPrefs.autoResize()).- Un
customPaneln'a aucune valeur propre (jamais mappé —getModelPreferences()expose sa clé ànull, comme unviewer) et est interdit commecollectionItemsd'une collection (CRIT à la validation de structure). La cléprefs-paneldoit exister dansinfos.json, sinon CRIT aussi.
window.KappappsPrefs
Singleton global disponible dans tout fichier prefs-panel (global ou WU) une fois le SDK chargé (<script src="/assets/prefs_panel_sdk/sdk.js">).
| Méthode | Rôle |
|---|---|
onReady(cb) | Appelé une fois le pont établi, avec {values, language, appli, version, kind} (kind : 'global' ou 'workUnit'). values est le snapshot complet des préférences au moment du handshake. |
getValues(paths?) | Promise<Record<string, unknown>> — relit les préférences depuis le formulaire, toujours un aller-retour frais (pas le snapshot d'onReady). Sans paths, retourne l'arbre complet. |
setValue(path, value) / setValues(changes) | Promise<{applied, errors}> — écrit une ou plusieurs préférences via le wu.setValue() natif du champ ciblé (validations/hideIf du champ toujours appliquées côté form). |
onChange(cb(path, value)) + watch(paths) | S'abonner aux changements d'un ou plusieurs chemins — voir « watch = préfixe » ci-dessous. |
onSubmit(cb) | Appelé juste avant la sauvegarde du formulaire. |
onState(cb({hidden, disabled})) | Zones WU uniquement : notifié quand le customPanel est caché/désactivé par un hideIf/disableIf, ou lors du submit. Jamais émis pour le script global (pas de WU associé). |
resize(height) | Redimensionne l'iframe (pré-clampé 0–2000 côté SDK, re-clampé et throttlé 100 ms côté hôte). |
autoResize(target = document.body) | Observe target (ResizeObserver) et appelle resize() automatiquement à chaque changement de taille. |
box({sticky?, collapsed?, hidden?}) | Modificateurs visuels du wrapper (position: sticky, hauteur forcée à 0, display: none). |
broadcast(data) / onBroadcast(cb) | Relais entre les iframes prefs-panel de la même app (script global + zones WU). Jamais de contact direct iframe↔iframe. |
Pattern (script global : lire puis écrire)
js
KappappsPrefs.onReady(async (info) => {
const values = await KappappsPrefs.getValues(['maSection.monChamp']);
console.log(values['maSection.monChamp']);
KappappsPrefs.onChange((path, value) => {
if (path === 'maSection.monChamp') console.log('changé →', value);
});
KappappsPrefs.watch(['maSection.monChamp']);
await KappappsPrefs.setValue('maSection.monChamp', 'nouvelle valeur');
});Sucre .js (coquille générée)
Si prefs-panel-index (ou wuOptions.source d'un customPanel) pointe vers un .js, le serveur génère automatiquement une coquille HTML (CSP script-src 'self') qui charge le SDK puis ton script — tu n'écris QUE le .js, aucun HTML :
js
KappappsPrefs.onReady((info) => {
document.body.textContent = 'kind: ' + info.kind;
KappappsPrefs.autoResize(document.body);
});Exemple complet
apps/testing/1.0.0/prefs-panel/ (script global index.html/index.js + zone WU .js zone.js, déclarés par l'onglet « Prefs panel (custom dev script) » de config/preferences.config.json).
Pièges
- Script global invisible ? Sa zone démarre à 0 px : sans
resize()/autoResize()dansonReady, ton panneau est monté mais ne se voit pas. - Fichier
.html: reset les marges UA ET le box-sizing (html, body { margin: 0 }+*, *::before, *::after { box-sizing: border-box }) — sinon 8 px de marge navigateur et les paddings en content-box parasitent le rendu et l'auto-resize. La coquille générée du sucre.jsfait les deux pour toi. kappapps.app/assets/styles/kappstrap.cssdans ton panel : autorisé par la CSP (style-src https:), mais kappstrap posebody { width: 100% }en supposant le resetborder-boxde Bootstrap, chargé par le site mais PAS par ton iframe — sans le reset ci-dessus, débordement horizontal garanti.- Sandbox opaque (
allow-scriptssansallow-same-origin) : ton origine est'null'— pas delocalStorage/cookies/accès au parent. Le pont passe exclusivement parwindow.KappappsPrefs. - CSP durcie
script-src 'self': ni CDN, ni<script>inline. Tout ton JS doit vivre dans un fichier séparé chargé en<script src="...">relatif — y compris pour le fichier.htmldéclaré enprefs-panel-index(voir l'exemple ci-dessus). - Remount à chaque sauvegarde : le formulaire de préférences est un fragment qui se recharge intégralement à chaque save (nouvelle iframe, nouveau
onReady). Ton état JS en mémoire ne survit pas — persiste ce qui compte via une préférence (setValue), jamais dans une variable globale. watch= préfixe de segment : observer un chemin de groupe (maSection) couvre déjà tous ses descendants (maSection.*) — inutile de lister chaque sous-champ.getValues()fait toujours un aller-retour : ce n'est pas une lecture du snapshot d'onReady,await-le à chaque fois où tu veux une valeur fraîche.customPaneln'a aucune valeur propre : ne t'attends pas à lire quoi que ce soit à son propre chemin — passe par d'autres champs du formulaire.