Appearance
window.KappappsPrefs (prefs panel custom)
Le dev peut fournir un document HTML 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 panneau 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 panneau global démarre repliée à 0 px : appelle
KappappsPrefs.resize(h)ouautoResize()dansonReadypour la rendre visible. Un panneau purement logique (sans UI) peut simplement ne jamais resize — il reste invisible, c'est voulu. - Le fichier d'entrée, comme
wuOptions.source, doit être un document.htmlou.htmfourni par l'app. La plateforme ne génère, ne réécrit et ne complète aucun fichier de l'app.
WU customPanel (zone ciblée)
Dans preferences.config.js :
js
demoPanel: {
type: 'customPanel',
label: { en: 'My panel', fr: 'Mon panneau' },
wuOptions: { source: 'zone.html', height: 200, autoResize: true }
}wuOptions.source(obligatoire) : document.htmlou.htmrelatif au dossierprefs-panel— pas de traversal (.., chemin absolu). Un fichier.jsseul n'est pas une page d'iframe et est refusé.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é depuis l'origine Kappapps :
html
<script src="https://kappapps.app/assets/prefs_panel_sdk/sdk.js"></script>Cette URL absolue est obligatoire. Le panneau de préférences reste lu depuis la dernière version poussée en ligne, même lorsque l'app principale ou son plugin est prévisualisé en localhost. Le SDK prefs n'est pas publié sur Bunny et un chemin racine /assets/prefs_panel_sdk/sdk.js ne doit jamais être utilisé.
| Méthode | Rôle |
|---|---|
onReady(cb) | Appelé une fois le pont établi, avec {values, language, appli, version, kind, layer} (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. Le callback peut retourner une Promise ; le formulaire attend sa fin et les écritures en cours (délai maximal de 10 secondes). |
getOrigins(paths?) | Retourne {id, name, origins} : couche sélectionnée et origine base ou variant de chaque chemin atomique. |
inherit(paths) | Supprime les surcharges indiquées dans la couche sélectionnée ; les valeurs principales redeviennent visibles. Les collections sont des chemins atomiques. |
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. |
Couche sélectionnée et héritage
Les panneaux globaux et les customPanel éditent la même couche que le formulaire : principales ou variante sélectionnée. setValue()/setValues() enregistrent une intention explicite même si la valeur ne change pas. Le bouton habituel de sauvegarde persiste les changements ; une navigation vers une autre couche demande confirmation si une saisie est en attente.
js
const layer = await KappappsPrefs.getOrigins(['theme.color']);
// layer.id === null : principales ; sinon identifiant stable de la variante.
await KappappsPrefs.setValue('theme.color', '#ffffff'); // surcharge explicite
await KappappsPrefs.inherit(['theme.color']); // retour aux principalesonReady().layer est un snapshot ; utilisez getOrigins() pour connaître l’origine courante. Les anciennes méthodes restent compatibles. Le protocole négocie l’attente de sauvegarde via la capacité additive submit-ack ; les anciens panneaux continuent à recevoir leur notification de sauvegarde.
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');
});Structure du document HTML
Le document charge explicitement le SDK Kappapps puis le JavaScript propre à l'app avec un chemin relatif :
html
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<style>
html, body { margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
</style>
</head>
<body>
<script src="https://kappapps.app/assets/prefs_panel_sdk/sdk.js"></script>
<script src="./zone.js"></script>
</body>
</html>Le fichier zone.js peut ensuite utiliser le pont normalement :
js
KappappsPrefs.onReady((info) => {
document.body.textContent = 'kind: ' + info.kind;
KappappsPrefs.autoResize(document.body);
});Exemple complet
apps/testing/1.0.0/prefs-panel/ (panneau global index.html + zone WU zone.html, chacun pouvant charger ses propres fichiers JS relatifs, 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. 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' https://kappapps.app: seul le SDK officiel peut venir de Kappapps ; aucun autre CDN ni<script>inline. Tout le JS propre à l'app 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.