Principes généraux
Un module OpenOverlay est composé de trois fichiers principaux : manifest.json, overlay.js et admin.js. Cette page décrit les règles que chaque fichier doit respecter pour garantir la cohérence et la compatibilité entre modules.
id) est en snake_case. Le nom du dossier et le configKey correspondent à cet identifiant.
- Un module ne doit jamais modifier le DOM ou l'état d'un autre module directement.
- L'état métier (série active, statut visible, etc.) est stocké dans la config serveur, jamais dans
localStorage. localStorageest réservé aux données gérées par le core, toutes préfixéesoo_(oo_token,oo_channel,oo_broadcaster_id…). Un module ne crée jamais ses propres clés — et ne lit pas les clés core directement (utilisetools.tokenettools.usernamedansrender()si tu en as besoin côté admin).- Pour partager un état temps réel entre overlay et dashboard (panel live, file d'attente…), utilise
api.sendToAdmin(modId, payload)côté overlay etonOverlayMessage(payload)côté admin — jamaislocalStorageni polling. - Pour persister l'état d'un mod entre rechargements (ex : file active, scène en cours, score…), utilise
api.saveState(modId, data)côté overlay outools.saveModState(modId, data)côté admin. La restauration au démarrage se fait via le hookonStateRestore(data, api), appelé par le core avantinit()(système ≥ 1.45.0). Si le module utilise ces APIs, déclarer"minSystemVersion": "1.45.0"dans le manifest. - Exception documentée : le mod Générique utilise
localStoragepour persister les messages chat collectés pendant le stream. C'est le seul cas autorisé — les messages doivent survivre à un rechargement de l'overlay quand le dashboard n'est pas ouvert, et aucune alternative sans serveur n'existe. Toute autre utilisation delocalStoragedans un mod est interdite. - Toute commande chat expose un niveau d'accès configurable par le streamer.
manifest.json
Le fichier manifest décrit le module pour le système et pour le Store. Tous les champs listés ci-dessous sont obligatoires.
Champs obligatoires
| Champ | Type | Description |
|---|---|---|
id | string | Identifiant unique en snake_case. Correspond au nom du dossier. |
name | string | Nom affiché dans l'interface (ex : "Ticker"). |
description | string | Description courte (< 120 caractères). |
longDescription | string | Description complète pour la fiche Store. |
icon | string | Emoji représentant le module. |
version | string | Version sémantique "MAJOR.MINOR.PATCH". |
author | string | Nom ou pseudonyme de l'auteur. |
types | string[] | Tableau de types (voir section Types de module). |
configKey | string | Clé sous laquelle la config est stockée. Égal à id dans la quasi-totalité des cas. |
defaultConfig | object | Valeurs par défaut de la configuration. |
tags | string[] | Mots-clés pour la recherche dans le Store. |
screenshots | string[] | Chemins relatifs vers les captures d'écran (peut être vide []). |
files | string[] | Liste des fichiers du module. |
changelog | object[] | Historique des versions [{ version, date, notes }]. |
Types de module
Le champ types est un tableau contenant une ou plusieurs des valeurs suivantes :
| Valeur | Signification |
|---|---|
"display" | Affiche ou modifie un élément visuel dans l'overlay. |
"widget" | Affiche un widget persistant et autonome (statut, horloge, etc.). |
"chat" | Écoute ou envoie des messages dans le chat Twitch. |
"events" | Réagit à des événements Twitch (follow, sub, bits, raid…). |
"integration" | Intègre un service externe (Discord, webhook, API tierce…). |
"interactive" | Implique une participation active des viewers (jeu, économie, commandes participatives…). |
defaultConfig et permissions
Si le module expose une commande chat, la permission doit être configurable par le streamer. Deux cas :
Commande unique — ajouter ces champs dans defaultConfig :
{
"command": "nom_commande", // nom sans le !
"commandPermission": "streamer" // niveau par défaut
}
Commandes multiples (slots) — stocker les commandes dans un tableau d'entrées, chacune avec son propre champ permission. Pas de commandPermission global dans ce cas.
Les valeurs valides pour les permissions sont : "streamer", "moderator", "vip", "subscriber", "follower", "everyone".
overlay.js
Le fichier overlay s'enregistre via OO.Mods.register() avec un objet décrivant le module. Seule la propriété id est obligatoire ; toutes les méthodes sont optionnelles.
OO.Mods.register({
id: 'mon_module',
onStateRestore(data, api) { /* état restauré avant init */ },
init(api) { /* appelé au chargement */ },
onConfigReload(api) { /* config rechargée */ },
onChatCommand(cmd, args, api, sender){ /* commande chat reçue */ },
onAdminMessage(data, api) { /* message depuis admin */ },
onTwitchEvent(type, event, api) { /* événement Twitch */ },
onFirstChatter(username, first, api) { /* premier message */ },
});
Méthodes disponibles
| Méthode | Paramètres | Déclenchement |
|---|---|---|
init | api | Une fois, après le chargement de la config et la validation du token. |
onConfigReload | api | Chaque fois que la config est rechargée depuis le dashboard. |
onChatCommand | cmd, args, api, sender | Quand un message chat commence par !. cmd = mot clé, args = tableau de mots suivants. |
onAdminMessage | data, api | Message envoyé depuis le dashboard via sendToOverlay(). |
onTwitchEvent | type, event, api | Événement EventSub Twitch reçu. type = nom de l'abonnement EventSub. |
onFirstChatter | username, isFirstEver, api | Premier message du stream (isFirstEver = false) ou depuis la création de la chaîne (true). |
onStateRestore | data, api | Appelé par le core avant init() avec l'état sauvegardé via api.saveState() — ou null. Permet la restauration transparente de l'état d'affichage après rechargement (système ≥ 1.45.0). |
Objet api
L'objet api est passé à toutes les méthodes. Il expose :
| Propriété / méthode | Description |
|---|---|
api.config | Config complète de l'overlay. Accès via api.config.[configKey]. |
api.token | Token OAuth Twitch courant. |
api.broadcasterId | ID Twitch du broadcaster. |
api.channelName | Nom de la chaîne Twitch. |
api.sendChat(text) | Envoie un message dans le chat. |
api.playSound(src, volume) | Joue un son. src = chemin résolu. Relaie aussi le son vers /overlay-audio (système ≥ 1.57.0), sans action requise côté module. |
api.resolveAsset(path) | Résout un chemin user:// en URL accessible. |
api.updateStreamTitle(title) | Met à jour le titre du stream via l'API Helix. |
api.checkPermission(sender, level) | Vérifie le niveau d'accès du sender, de façon sync. Méthode officielle — ne pas accéder aux propriétés de sender directement. Traite 'follower' comme 'everyone' (statut réel non disponible en sync). |
api.checkPermissionAsync(sender, level) | async → boolean (système ≥ 1.31.0). Résout le vrai statut follower via Helix — à utiliser si le niveau 'follower' doit être exact. |
api.isFollower(userId) | async → boolean (système ≥ 1.31.0). Cache 5 minutes, appels concurrents dédupliqués. |
api.twitchFetch(endpoint, opts?) | async. Appel Helix authentifié (headers gérés automatiquement) — à préférer à un fetch() manuel. |
api.getMod(id) | Accède à un autre mod enregistré par son ID. |
api.emit(method, ...args) | Dispatch une méthode sur tous les mods. |
api.trigger(cmd, args?, sender?) | Déclenche une commande chat programmatiquement. |
api.logEvent({ modId, label, icon?, user?, detail? }) | Écrit un événement dans l'Historique du dashboard (système ≥ 1.26.0). Appeler via api.logEvent?.() pour la rétro-compatibilité. |
api.sendWhisper(toUserId, message) | Chuchotement Twitch depuis le compte du streamer (système ≥ 1.30.0, async → boolean). Prévoir un fallback chat. |
api.sendToAdmin(modId, payload) | Pousse un état vers le dashboard admin en temps réel via BroadcastChannel (système ≥ 1.37.0). Fire-and-forget — le module admin doit implémenter onOverlayMessage(payload) pour le recevoir. |
api.saveState(modId, data) | Sauvegarde l'état du mod côté serveur via /api/memory. Fire-and-forget. data = objet JSON sérialisable. Système ≥ 1.45.0. |
api.loadState(modId) | async → data | null. Charge l'état précédemment sauvegardé. Préférer onStateRestore pour la restauration au démarrage — loadState est utile pour un polling cross-device. Système ≥ 1.45.0. |
api.placeInZone(zoneId, element) | Positionne un élément dans une zone d'emplacement gérée par le core — remplace position: fixed codé en dur, respecte l'ordre des modules choisi dans le dashboard. |
api.ai(task, input, opts?) | async (système ≥ 1.32.0). Proxy IA sécurisé de l'instance — clé fournisseur jamais exposée au module. Réservé au propriétaire de l'instance depuis la v1.59.1. Prévoir un fallback si non configuré. |
api.tts(voiceId, text, opts?) | async → Blob (système ≥ 1.53.0). Proxy ElevenLabs de l'instance. Réservé au propriétaire de l'instance depuis la v1.59.1. Prévoir un fallback silencieux si non configuré. |
OO.Mods — méthodes disponibles
En plus de OO.Mods.register(), l'objet OO.Mods expose les méthodes suivantes, utilisables depuis n'importe quel module overlay :
| Méthode | Description |
|---|---|
OO.Mods.register(def) | Enregistre un mod. À appeler une seule fois dans overlay.js. |
OO.Mods.all() | Retourne le tableau de tous les mods enregistrés. À utiliser uniquement si api.getMod() ou api.emit() ne suffisent pas. |
Permissions dans onChatCommand
Toujours utiliser api.checkPermission() pour vérifier l'accès, en lisant le niveau depuis la config :
onChatCommand(cmd, args, api, sender) {
const cfg = api.config.mon_module;
if (!api.checkPermission(sender, cfg.commandPermission ?? 'streamer')) return;
// ...
}
Le broadcaster passe toujours, quelle que soit la valeur de commandPermission. Hiérarchie des niveaux :
| Valeur | Qui peut utiliser |
|---|---|
"streamer" | Broadcaster uniquement. |
"moderator" | Broadcaster + modérateurs. |
"vip" | Broadcaster + modérateurs + VIP. |
"subscriber" | Broadcaster + modérateurs + VIP + abonnés. |
"follower" | Followers. ⚠ Avec checkPermission() traité comme everyone (IRC ne fournit pas ce statut) — utiliser checkPermissionAsync() pour un vrai contrôle Helix. |
"everyone" | Tout le monde. |
Résolution d'assets
Deux origines possibles pour les fichiers (images, sons) stockés dans la config :
- Asset intégré — nom de fichier seul (ex :
"follow.gif"), servi depuis/overlay/assets/gifs/ou/overlay/assets/sounds/. - Asset utilisateur — chemin préfixé
user://(ex :"user://mon-logo.webp"), résolu viaapi.resolveAsset(path).
function resolveGif(gif, fallback, api) {
const file = gif || fallback;
if (!file) return null;
if (file.startsWith('user://')) return api.resolveAsset(file);
return `/overlay/assets/gifs/${file}`;
}
Dans l'admin, utiliser createAssetField() (voir ci-dessous) — il gère automatiquement les deux origines.
admin.js
Le fichier admin s'enregistre via OO.Admin.register(). Les méthodes render et getCommands sont obligatoires.
OO.Admin.register({
id: 'mon_module',
render(config, container, helpers) { /* ... */ },
getCommands(config) { return []; },
});
render(config, container, helpers)
config est l'objet de configuration complet de l'overlay. Accès à la config du module via config.[configKey]. container est l'élément DOM dans lequel le panneau est rendu.
if (!config.mon_module) config.mon_module = {};
Helpers disponibles
| Helper | Signature | Usage |
|---|---|---|
markDirty() | — | Signale un changement non sauvegardé. À appeler après chaque modification de config. |
sendToOverlay(data) | (data: object) | Envoie un message à l'overlay via BroadcastChannel. data.type requis. |
showToast(msg, type?) | (msg: string, type?: 'error') | Affiche une notification temporaire. |
createAssetField(opts) | ({ type, value, onChange }) → HTMLElement | Crée un sélecteur de fichier (assets intégrés + fichiers uploadés) avec bouton d'upload intégré. type : 'gif', 'image', 'sound' ou 'video'. Retourne un élément DOM à injecter. |
resolveAsset(path) | (path: string) → string | Résout un chemin user:// en URL affichable dans l'admin. |
uploadAsset(file) | (file: File) → Promise<string> | Upload un fichier et retourne le chemin user://. Préférer createAssetField pour les champs de config. |
addDropZone(el, types, cb) | (el, mimeTypes[], callback) | Active le glisser-déposer sur un élément. |
testUsername | string | Pseudo du broadcaster, pour pré-remplir les champs de test. |
twitchFetch(url, opts?) | (url, opts?) → Promise<Response> | Fetch authentifié vers l'API Twitch Helix (token + Client-Id injectés automatiquement). |
broadcasterId | string | ID Twitch du broadcaster, pour les requêtes Helix dans l'admin. |
saveModState(modId, data) | async | Sauvegarde l'état d'un mod depuis le dashboard via /api/memory. Équivalent de api.saveState côté overlay. Fire-and-forget. Système ≥ 1.45.0. |
loadModState(modId) | async → data | null | Charge l'état d'un mod depuis le dashboard. Utile pour synchroniser l'admin avec un état modifié par l'overlay (ex : polling pour cross-device). Système ≥ 1.45.0. |
Pour le sélecteur de niveau d'accès des commandes, utiliser le helper exposé dans tools :
tools.permissionSelect('mon-module-perm', cfg.commandPermission ?? 'streamer')
Ce helper génère un <select> avec les six niveaux standards pré-rempli à la valeur courante.
getCommands(config)
Méthode obligatoire. Retourne un tableau des commandes chat exposées par ce module, utilisé pour alimenter le tableau de bord "Commandes chat". Retourner [] si le module n'expose aucune commande.
[] si config.enabled === false, quelle que soit la nature du module (intégré ou Store). C'est la convention appliquée à tous les mods sans exception.
Commande unique :
getCommands(config) {
if (config.enabled === false || !config.command) return [];
return [{ cmd: `!${config.command}`, desc: 'Description courte', permission: config.commandPermission ?? 'streamer' }];
}
Commandes multiples (slots) :
getCommands(config) {
if (config.enabled === false) return [];
return (config.slots ?? [])
.filter(s => s.command)
.map(s => ({ cmd: `!${s.command}`, desc: s.label || '…', permission: s.permission ?? 'moderator' }));
}
Chaque entrée du tableau :
| Clé | Type | Description |
|---|---|---|
cmd | string | Commande complète avec le ! (ex : "!pause"). |
desc | string | Description de ce que fait la commande. |
permission | string | Niveau d'accès requis. |
args | string | Optionnel — description des arguments attendus. |
Framework CSS — modules
Le fichier admin/admin.css est chargé globalement dans le dashboard. Toutes les classes qu'il définit sont disponibles dans admin.js de n'importe quel module, sans import supplémentaire.
style="..." pour les patterns couverts par ce framework (couleurs, tailles, marges, visibilité). Utiliser les classes appropriées.
Utilitaires disponibles
Composants existants (§12 DESIGN_CHARTER)
Les composants de base du dashboard sont tous disponibles :
.card, .card-header, .card-title, .card-body, .card-body.grid, .card-section — .field, .field-inline, .hint, .span-full — .btn, .btn-primary, .btn-secondary, .btn-ghost, .btn-danger, .btn-sm — .toggle — .badge, .badge-green, .badge-red, .badge-muted, .badge-purple — .callout.info/tip/warn/plain — .table
Modificateurs de card
| Classe | Effet |
|---|---|
.card--warn | Bordure orange atténuée (état expiré / avertissement) |
.card--premium | Bordure violette atténuée (contenu Nexus) |
.card--accent | Bordure gauche violette 3px (carte mise en avant) |
.card--mb-sm | margin-bottom: 12px |
.card-body--flush | padding: 0 — listes bord-à-bord |
.card-body--center | Centré, padding: 24px 16px — états vides |
.card-body--no-top | padding-top: 0 |
Utilitaires texte & couleur
| Classe | CSS |
|---|---|
.text-muted | color: var(--muted) |
.text-green | color: var(--green) |
.text-red | color: var(--red) |
.text-yellow | color: var(--yellow) |
.text-purple | color: var(--purple) |
.text-accent2 | color: var(--accent2) — liens, emphases violet clair |
.text-em | color: var(--text) — emphase dans contexte muted |
.text-sm | font-size: 13px |
.text-xs | font-size: 12px |
.text-xxs | font-size: 11px |
.text-center | text-align: center |
.text-right | text-align: right |
.text-italic | font-style: italic |
.nowrap | white-space: nowrap |
.mono | font-family: monospace |
.fw-bold | font-weight: 600 |
.fw-normal | font-weight: 400 |
Espacement
Préfixe mb- (margin-bottom), mt- (margin-top), ml- (margin-left) — valeurs disponibles :
mb-4/6/8/10/12/14/16/20 · mt-4/6/8/10/12/16/20 · ml-6/8/10
Également disponibles : .py-8 (padding vertical 8px), .m-0 (marge zéro), .min-w-90/100/120/220 (min-width).
Flex & visibilité
| Classe | CSS |
|---|---|
.hidden | display: none !important — toggle JS : classList.toggle('hidden', !cond) |
.flex-1 | flex: 1 |
.shrink-0 | flex-shrink: 0 |
.min-w-0 | min-width: 0 — évite le débordement flex |
.row-flex | display: flex; align-items: center; gap: 10px |
.row-flex--between | idem + justify-content: space-between; gap: 12px |
.row-flex--wrap | idem .row-flex + flex-wrap: wrap |
.flex-col | display: flex; flex-direction: column; gap: 6px; align-items: flex-start |
.flex-col--lg | idem .flex-col avec gap plus grand |
.gap-6 | gap: 6px |
.gap-8 | gap: 8px |
Composants de page
| Classe | Usage |
|---|---|
.code-preview | Bloc URL ou code d'activation — fond surface2, monospace, violet |
.tag-chip | Pill d'étiquette — fond surface2, 11px, border-radius 99px |
.tags-row | Conteneur flex-wrap pour un groupe de .tag-chip |
.progress-track | Piste de barre de progression (height 4px, fond border) |
.progress-fill | Remplissage animé — style="width:X%" reste dynamique en JS |
.history-row | Ligne d'historique — flex row avec bordure basse |
.list-spaced | Liste avec espacement vertical régulier entre items |
.mod-meta | Meta-info (version, auteur) — 11px, muted, ml-8 |
.border-top-sep | Séparateur horizontal — bordure haut, padding-top 12px |
.field--sm | Champ compact (hauteur réduite, font-size 12px) |
.filename-hint | Indication de nom de fichier — monospace, muted, truncated |
.hint--spaced | Hint avec marge-top (variante espacée de .hint) |
.store-mod-icon | Icône de mod Store — taille et radius normalisés |
.nexus-login-row | Ligne de connexion Nexus — flex row avec gap |
.nexus-login-input | Input de connexion Nexus — styles spécifiques |
.btn-xs | Bouton très compact (padding réduit, font-size 11px) |
.icon-lg | Icône grande taille (1.4rem–1.6rem) |
Règles d'usage
- ✅ Utiliser les classes ci-dessus directement dans le HTML généré par
admin.js. - ✅ Basculer la visibilité par toggle de classe :
el.classList.toggle('hidden', !condition) - ✅ Passer les couleurs dynamiques (issues de config) via CSS custom property et
setProperty():
el.style.setProperty('--dot-color', color)(appelé en JS après rendu) +.ma-classe { background: var(--dot-color) }
La valeur par défaut de la propriété est définie viasetProperty()avant le premier rendu, jamais en fallback CSS. - ✅ Ajouter des classes propres au mod en supplément (ex :
.cp-enabled-labelen plus de.text-muted). - ❌ Ne pas utiliser
style="..."dans le HTML généré — aucune propriété visuelle (couleur, taille, marge, visibilité) ne doit passer par un attribut inline. - ❌ Ne pas utiliser de fallback dans
var()dans les fichiersadmin.cssdes mods (var(--x, fallback)est interdit). Si une valeur par défaut est nécessaire, la définir viasetProperty()en JS avant l'affichage. - ❌ Ne pas redéfinir les classes du framework dans
mods/{id}/admin.css. - ❌ Ne pas recréer des variantes locales (
.mon-mod-list,.mon-mod-item…).
Système de zones
OpenOverlay fournit 9 conteneurs .oo-zone couvrant toutes les positions du sélecteur : top-left, top-center, top-right, middle-left, middle-center, middle-right, bottom-left, bottom-center, bottom-right. Un module qui affiche un widget positionnable doit :
- ✅ Appeler
api.placeInZone(zoneId, element)dansinit()etonConfigReload(). - ✅ Ne pas déclarer
position: fixeddansoverlay.css— le conteneur de zone gère le positionnement. - ✅ Ne pas déclarer de propriétés
top / right / bottom / left— elles seront ignorées dans un conteneur flex. - ✅ Conserver
z-index: var(--mod-z-{id})si le module doit s'afficher devant d'autres éléments dans la même zone.
Variables CSS système dans overlay.css
L'overlay expose des variables CSS globales sur :root pour la coordination entre modules. Les utiliser plutôt que des valeurs figées :
-
--oo-safe-bottom— espace réservé en bas de l'overlay par une barre système active (ex : ticker). Vaut0pxpar défaut. Tout module avec un élément positionné en bas doit l'intégrer :
#mon-widget { bottom: calc(16px + var(--oo-safe-bottom, 0px)); } -
--mod-z-{id}— z-index automatique géré par le système selon l'ordre des modules dans le dashboard. À utiliser dansoverlay.css:z-index: var(--mod-z-mon-module, 10);— le fallback s'applique uniquement avant le premier chargement de config.
Si ton module occupe lui-même la zone basse (barre d'infos, bandeau…), il doit écrire --oo-safe-bottom en JS pour que les autres modules s'adaptent automatiquement :
document.documentElement.style.setProperty('--oo-safe-bottom', '40px'); // visible
document.documentElement.style.setProperty('--oo-safe-bottom', '0px'); // caché
Persistance d'état
Pour les modules avec un état métier significatif (file active, scène en cours, score…), le système expose /api/memory — un endpoint Netlify Blobs générique, un blob par utilisateur contenant { modId: data, … }.
APIs disponibles
| Méthode | Côté | Description |
|---|---|---|
api.saveState(modId, data) | Overlay | Sauvegarde l'état côté serveur. Fire-and-forget. |
api.loadState(modId) | Overlay | async → data | null. Charge l'état. Utile pour le polling cross-device. |
tools.saveModState(modId, data) | Admin | Équivalent de api.saveState côté dashboard. |
tools.loadModState(modId) | Admin | async → data | null. Charge l'état depuis l'admin. |
Hook onStateRestore(data, api)
Appelé par le core avant init() avec les données sauvegardées — ou null. C'est l'endroit recommandé pour la restauration : pas d'appel réseau bloquant, état disponible dès init().
OO.Mods.register({
id: 'mon_module',
onStateRestore(data, api) {
if (!data) return;
this._score = data.score ?? 0;
this._active = data.active ?? null;
// Ne pas déclencher d'effets visuels ici — init() s'en charge
},
init(api) {
// this._score et this._active sont déjà restaurés
this._render(api);
},
_onAction(api) {
this._score++;
api.saveState?.('mon_module', { score: this._score, active: this._active });
this._render(api);
},
});
- N'utiliser la persistance que pour un état métier significatif (ce que le streamer voudrait retrouver après un rechargement accidentel).
- Déclarer
"minSystemVersion": "1.45.0"dans le manifest si le module utilise ces APIs. - Appeler via optional chaining (
api.saveState?.()) pour la rétro-compatibilité.
Messages admin → overlay
La communication du dashboard vers l'overlay se fait via sendToOverlay(data) (côté admin) et onAdminMessage(data, api) (côté overlay).
La propriété type est obligatoire et suit la convention [modId]_[action] :
// admin.js
sendToOverlay({ type: 'mon_module_update' });
sendToOverlay({ type: 'mon_module_reset' });
sendToOverlay({ type: 'mon_module_test', eventType: 'follow', user: 'TestUser' });
// overlay.js
onAdminMessage(data, api) {
if (data.type === 'mon_module_update') this._start(api);
if (data.type === 'mon_module_reset') this._reset();
}
id du module évite les collisions entre modules dans le BroadcastChannel partagé. Si un module doit instancier le canal lui-même, il utilise la constante OO.CHANNEL — jamais une chaîne en dur.
Checklist avant soumission
- ✅
manifest.jsoncontient tous les champs obligatoires. - ✅
versionsuit le formatMAJOR.MINOR.PATCHetchangelogest à jour. - ✅
minSystemVersionest renseigné si le module dépend d'une API introduite dans une version précise du système. - ✅ Chaque commande chat a un niveau d'accès configurable : via
commandPermission(commande unique) ou viapermissionpar entrée (commandes multiples / slots). - ✅
overlay.js— les permissions sont vérifiées viaapi.checkPermission(). - ✅ Aucun usage de
localStoragepour l'état métier. - ✅ Les assets utilisateur sont résolus via
api.resolveAsset(). - ✅
admin.js—getCommands()est implémenté et retourne[]quandenabled === falseou quand le module n'expose aucune commande. - ✅
overlay.js—getCommands()aussi implémenté (au minimum{ cmd, permission }) — alimente la commande d'aide : chuchotement au viewer + confirmation chat, sans jamais exposer la liste publiquement (système ≥ 1.30.0). - ✅ Les niveaux d'accès utilisent
tools.permissionSelect()et les valeurs standard. - ✅ Les types de messages
sendToOverlaysuivent la convention[modId]_[action]. - ✅ Aucun attribut
style="..."dans le HTML généré dynamiquement — aucune propriété visuelle ne passe par un inline style. - ✅ Aucun fallback dans les
var()des fichiersadmin.css:var(--x)uniquement, valeur par défaut définie viael.style.setProperty()en JS. - ✅ Les couleurs dynamiques (config streamer) passent par
el.style.setProperty('--ma-var', valeur), pas parstyle="...". - ✅ Le module ne modifie pas le DOM ou l'état d'un autre module.
- ✅ Si le module affiche un widget positionnable, il utilise
api.placeInZone()et ne déclare pasposition: fixeddans sonoverlay.css. - ✅ Si le module affiche un élément positionné en bas hors zone (barre pleine largeur…), il utilise
calc(Npx + var(--oo-safe-bottom, 0px))dansoverlay.css. - ✅ Si le module occupe la zone basse (barre, bandeau…), il écrit
--oo-safe-bottomsurdocument.documentElementquand il s'affiche/se cache. - ✅ Si le module utilise
api.saveState/onStateRestore,minSystemVersionest renseigné à"1.45.0"ou supérieur dansmanifest.json. - ✅ Les appels à
api.saveState,api.loadState,api.sendToAdminse font via optional chaining (api.saveState?.()) pour la rétro-compatibilité avec les instances plus anciennes. - ✅ Le
render()admin n'ajoute PAS de toggle « Activer le module » — le dashboard en affiche déjà un natif ; le module se contente de respecterconfig.enabled === falsedans son runtime. - ✅ Le ZIP de soumission au Store ne dépasse pas 10 Mo.