Référence technique

Normalisation des modules

Standard que tous les modules OpenOverlay doivent respecter — structure, API, conventions de nommage, permissions et assets.

🌱 Tu débutes ? Commence par le guide de développement → — cette page est la référence technique à consulter avant soumission.

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.

Convention de nommage — L'identifiant d'un module (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.
  • localStorage est réservé aux données gérées par le core, toutes préfixées oo_ (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 (utilise tools.token et tools.username dans render() 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 et onOverlayMessage(payload) côté admin — jamais localStorage ni 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 ou tools.saveModState(modId, data) côté admin. La restauration au démarrage se fait via le hook onStateRestore(data, api), appelé par le core avant init() (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 localStorage pour 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 de localStorage dans 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

ChampTypeDescription
idstringIdentifiant unique en snake_case. Correspond au nom du dossier.
namestringNom affiché dans l'interface (ex : "Ticker").
descriptionstringDescription courte (< 120 caractères).
longDescriptionstringDescription complète pour la fiche Store.
iconstringEmoji représentant le module.
versionstringVersion sémantique "MAJOR.MINOR.PATCH".
authorstringNom ou pseudonyme de l'auteur.
typesstring[]Tableau de types (voir section Types de module).
configKeystringClé sous laquelle la config est stockée. Égal à id dans la quasi-totalité des cas.
defaultConfigobjectValeurs par défaut de la configuration.
tagsstring[]Mots-clés pour la recherche dans le Store.
screenshotsstring[]Chemins relatifs vers les captures d'écran (peut être vide []).
filesstring[]Liste des fichiers du module.
changelogobject[]Historique des versions [{ version, date, notes }].

Types de module

Le champ types est un tableau contenant une ou plusieurs des valeurs suivantes :

ValeurSignification
"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éthodeParamètresDéclenchement
initapiUne fois, après le chargement de la config et la validation du token.
onConfigReloadapiChaque fois que la config est rechargée depuis le dashboard.
onChatCommandcmd, args, api, senderQuand un message chat commence par !. cmd = mot clé, args = tableau de mots suivants.
onAdminMessagedata, apiMessage envoyé depuis le dashboard via sendToOverlay().
onTwitchEventtype, event, apiÉvénement EventSub Twitch reçu. type = nom de l'abonnement EventSub.
onFirstChatterusername, isFirstEver, apiPremier message du stream (isFirstEver = false) ou depuis la création de la chaîne (true).
onStateRestoredata, apiAppelé 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éthodeDescription
api.configConfig complète de l'overlay. Accès via api.config.[configKey].
api.tokenToken OAuth Twitch courant.
api.broadcasterIdID Twitch du broadcaster.
api.channelNameNom 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éthodeDescription
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 :

ValeurQui 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 via api.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.

Initialiser la config — Toujours s'assurer que l'objet config du module existe avant de l'utiliser :
if (!config.mon_module) config.mon_module = {};

Helpers disponibles

HelperSignatureUsage
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 }) → HTMLElementCré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) → stringRé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.
testUsernamestringPseudo 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).
broadcasterIdstringID Twitch du broadcaster, pour les requêtes Helix dans l'admin.
saveModState(modId, data)asyncSauvegarde 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 | nullCharge 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.

Règle universelle — Toujours retourner [] 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éTypeDescription
cmdstringCommande complète avec le ! (ex : "!pause").
descstringDescription de ce que fait la commande.
permissionstringNiveau d'accès requis.
argsstringOptionnel — 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.

Ne jamais utiliser 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

ClasseEffet
.card--warnBordure orange atténuée (état expiré / avertissement)
.card--premiumBordure violette atténuée (contenu Nexus)
.card--accentBordure gauche violette 3px (carte mise en avant)
.card--mb-smmargin-bottom: 12px
.card-body--flushpadding: 0 — listes bord-à-bord
.card-body--centerCentré, padding: 24px 16px — états vides
.card-body--no-toppadding-top: 0

Utilitaires texte & couleur

ClasseCSS
.text-mutedcolor: var(--muted)
.text-greencolor: var(--green)
.text-redcolor: var(--red)
.text-yellowcolor: var(--yellow)
.text-purplecolor: var(--purple)
.text-accent2color: var(--accent2) — liens, emphases violet clair
.text-emcolor: var(--text) — emphase dans contexte muted
.text-smfont-size: 13px
.text-xsfont-size: 12px
.text-xxsfont-size: 11px
.text-centertext-align: center
.text-righttext-align: right
.text-italicfont-style: italic
.nowrapwhite-space: nowrap
.monofont-family: monospace
.fw-boldfont-weight: 600
.fw-normalfont-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é

ClasseCSS
.hiddendisplay: none !important — toggle JS : classList.toggle('hidden', !cond)
.flex-1flex: 1
.shrink-0flex-shrink: 0
.min-w-0min-width: 0 — évite le débordement flex
.row-flexdisplay: flex; align-items: center; gap: 10px
.row-flex--betweenidem + justify-content: space-between; gap: 12px
.row-flex--wrapidem .row-flex + flex-wrap: wrap
.flex-coldisplay: flex; flex-direction: column; gap: 6px; align-items: flex-start
.flex-col--lgidem .flex-col avec gap plus grand
.gap-6gap: 6px
.gap-8gap: 8px

Composants de page

ClasseUsage
.code-previewBloc URL ou code d'activation — fond surface2, monospace, violet
.tag-chipPill d'étiquette — fond surface2, 11px, border-radius 99px
.tags-rowConteneur flex-wrap pour un groupe de .tag-chip
.progress-trackPiste de barre de progression (height 4px, fond border)
.progress-fillRemplissage animé — style="width:X%" reste dynamique en JS
.history-rowLigne d'historique — flex row avec bordure basse
.list-spacedListe avec espacement vertical régulier entre items
.mod-metaMeta-info (version, auteur) — 11px, muted, ml-8
.border-top-sepSéparateur horizontal — bordure haut, padding-top 12px
.field--smChamp compact (hauteur réduite, font-size 12px)
.filename-hintIndication de nom de fichier — monospace, muted, truncated
.hint--spacedHint avec marge-top (variante espacée de .hint)
.store-mod-iconIcône de mod Store — taille et radius normalisés
.nexus-login-rowLigne de connexion Nexus — flex row avec gap
.nexus-login-inputInput de connexion Nexus — styles spécifiques
.btn-xsBouton très compact (padding réduit, font-size 11px)
.icon-lgIcô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 via setProperty() avant le premier rendu, jamais en fallback CSS.
  • ✅ Ajouter des classes propres au mod en supplément (ex : .cp-enabled-label en 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 fichiers admin.css des mods (var(--x, fallback) est interdit). Si une valeur par défaut est nécessaire, la définir via setProperty() 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) dans init() et onConfigReload().
  • ✅ Ne pas déclarer position: fixed dans overlay.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). Vaut 0px par 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 dans overlay.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éthodeCôtéDescription
api.saveState(modId, data)OverlaySauvegarde l'état côté serveur. Fire-and-forget.
api.loadState(modId)Overlayasync → 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)Adminasync → 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);
  },
});
Règles
  • 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();
}
Nommage — Préfixer toujours avec l'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.json contient tous les champs obligatoires.
  • version suit le format MAJOR.MINOR.PATCH et changelog est à jour.
  • minSystemVersion est 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 via permission par entrée (commandes multiples / slots).
  • overlay.js — les permissions sont vérifiées via api.checkPermission().
  • ✅ Aucun usage de localStorage pour l'état métier.
  • ✅ Les assets utilisateur sont résolus via api.resolveAsset().
  • admin.jsgetCommands() est implémenté et retourne [] quand enabled === false ou quand le module n'expose aucune commande.
  • overlay.jsgetCommands() 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 sendToOverlay suivent 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 fichiers admin.css : var(--x) uniquement, valeur par défaut définie via el.style.setProperty() en JS.
  • ✅ Les couleurs dynamiques (config streamer) passent par el.style.setProperty('--ma-var', valeur), pas par style="...".
  • ✅ 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 pas position: fixed dans son overlay.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)) dans overlay.css.
  • ✅ Si le module occupe la zone basse (barre, bandeau…), il écrit --oo-safe-bottom sur document.documentElement quand il s'affiche/se cache.
  • ✅ Si le module utilise api.saveState / onStateRestore, minSystemVersion est renseigné à "1.45.0" ou supérieur dans manifest.json.
  • ✅ Les appels à api.saveState, api.loadState, api.sendToAdmin se 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 respecter config.enabled === false dans son runtime.
  • ✅ Le ZIP de soumission au Store ne dépasse pas 10 Mo.
🖨️ Télécharger la checklist (PDF)