Une seule interface pour tout le serveur
Toute l'interface du Socle (HUD, menus, notifications, garage, tablettes, écrans d'ox_lib…) est une seule page web : resources/socle/ui/dist/index.html, déclarée par le ui_page du manifeste de socle. Chaque écran est une vue de cette page, affichée ou masquée à la demande.
Pourquoi une seule :
- chaque
ui_pageest une page web complète que chaque joueur charge à chaque connexion : une page par ressource, c'est autant de mémoire et de temps de chargement en plus ; - le focus (souris, clavier) est géré à un seul endroit (
Core.Focus) : deux pages qui se disputentSetNuiFocusdonnent des curseurs bloqués ; - le thème (couleur du serveur, apparence réglée au panel) s'applique partout d'un coup.
Règle pour ton code : n'ajoute jamais de `ui_page` à une ressource. Ajoute une vue à l'interface du Socle, ou utilise une vue existante.
Avant d'écrire une vue : celles qui existent déjà
Beaucoup de besoins sont couverts sans écrire de React ni recompiler :
| Besoin | Sans nouvelle vue |
|---|---|
| Formulaire de saisie | lib.inputDialog(...) d'ox_lib (rendu par le Socle) ou exports.socle:requestTextInput({ title, default }) |
| Confirmation avec curseur | lib.alertDialog(...) ou exports.socle:createPrompt(titre, texte, texte2, fn) |
| Oui / Non sans curseur | exports.socle:Decide({ ... }) |
| Menu de choix | lib.registerContext + lib.showContext, lib.showMenu, lib.registerRadial |
| Mini-jeu d'adresse | lib.skillCheck(...) |
| Barre de progression | lib.progressBar(...) ou exports.socle:StartProgress({ label, duration }) |
| Notification | exports.socle:Notify(...), lib.notify(...), ESX.ShowNotification(...) |
| Texte d'aide, barre de touches | exports.socle:HelpText(...), exports.socle:KeyHints(...), lib.showTextUI(...) |
| Panneau d'informations | exports.socle:drawInfo(titre, éléments) |
| Étiquette flottante | exports.socle:setWorldTag(id, { coords, title, ... }) |
Les menus ox_lib relayés
ox_lib contient des retouches marquées [Socle] : quand la ressource socle est démarrée, ses écrans ne s'affichent plus dans la page d'ox_lib mais dans l'interface Socle, avec le style du serveur. Ton code appelle l'API ox_lib normale :
-- client : un menu contextuel ox_lib, affiché par l'interface Socle
lib.registerContext({
id = "prime_menu",
title = Core.t("prime.menu.titre"),
options = {
{ title = Core.t("prime.menu.reclamer"), icon = "coins", onSelect = function() reclamer() end },
{ title = Core.t("prime.menu.infos"), icon = "circle-info", onSelect = function() infos() end },
},
})
lib.showContext("prime_menu")
-- saisie : la logique reste dans ox_lib, l'affichage passe par le Socle
local valeurs = lib.inputDialog(Core.t("prime.saisie.titre"), {
{ type = "number", label = Core.t("prime.saisie.montant"), min = 1, max = 1000 },
})Les vues correspondantes du Socle s'appellent oxinput, oxalert, oxskill, oxtextui, oxcontext, oxmenu et oxradial. Les menus natifs du Socle (Core.Menu.create, utilisés par les menus F5 et F10) passent eux aussi par cette interface, vue menu.
Comment le Lua parle à l'interface
Afficher, masquer, envoyer
-- client : affiche la vue « prime » et lui passe des données
Core.nui("interface:show", { id = "prime", data = { montant = 500, jour = "2026-10-11" } })
-- masque la vue
Core.nui("interface:hide", { id = "prime" })
-- message libre vers l'interface (reçu par useNuiEvent("prime:maj", ...))
Core.nui("prime:maj", { data = { montant = 750 } })Core.nui(action, données) attend que l'interface soit chargée avant d'envoyer : tu peux l'appeler dès le démarrage, dans un thread.
Recevoir une réponse de l'interface
-- client : Core.rnui attend le chargement de l'interface, d'où le thread
CreateThread(function()
Core.rnui("prime:fermer", function(data, respond)
Core.Focus.release("prime")
Core.nui("interface:hide", { id = "prime" })
respond({ ok = true }) -- facultatif : sans respond, l'interface reçoit { status = "received" }
end)
end)Une erreur dans le gestionnaire est attrapée : l'interface reçoit { status = "error", error = … }. Ce que l'interface envoie est une donnée du client : si l'action touche à la valeur, elle repart vers le serveur par un callback validé (voir Référence API).
Le focus
-- Core.Focus.acquire(id, curseur, garderLesTouchesDuJeu)
Core.Focus.acquire("prime", true, false) -- curseur visible, le jeu ne reçoit plus le clavier
Core.Focus.release("prime")Les demandes de focus s'empilent : quand ta vue relâche le sien, le focus revient à l'écran ouvert avant (un menu, par exemple). N'appelle jamais SetNuiFocus directement.
Ajouter une vue React
L'interface est une application React 18 + Vite, dans resources/socle/ui/.
ui/
index.html, loadscreen.html
src/
main.jsx ← registre des vues
nui.js ← onNui, useNuiEvent, fetchNui, postNui
interface.js ← useInterfaceShow, showInterface, hideInterface
i18n.js ← useLocale (textes du jeu)
style.css ← variables CSS du thème
components/
kit/ ← Sheet.jsx, KeyBar.jsx : composants partagés
<dossiers des vues>
dist/ ← le build chargé par le jeu1. Le composant
ui/src/components/prime/Prime.jsx :
import { useEffect, useState } from "react";
import { useInterfaceShow } from "../../interface";
import { postNui, useNuiEvent } from "../../nui";
import { useLocale } from "../../i18n";
import styles from "./Prime.module.css";
// Ticket de la prime de minuit (exemple de documentation).
export default function Prime() {
const t = useLocale();
const [ticket, setTicket] = useState(null);
// données passées par Core.nui("interface:show", { id = "prime", data = ... })
useInterfaceShow("prime", (data) => setTicket(data));
// message libre Core.nui("prime:maj", { data = ... })
useNuiEvent("prime:maj", (data) => setTicket((old) => ({ ...old, ...data })));
// Échap ferme la vue ; le Lua relâche le focus et masque la vue
useEffect(() => {
const onKey = (event) => event.key === "Escape" && postNui("prime:fermer");
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
if (!ticket) return null;
return (
<div className={styles.ticket}>
<span className={styles.titre}>{t("prime.ui.titre")}</span>
<strong className={styles.montant}>{t("prime.ui.montant", [ticket.montant])}</strong>
<button className={styles.bouton} onClick={() => postNui("prime:fermer")}>
{t("prime.ui.fermer")}
</button>
</div>
);
}Les outils disponibles :
| Fonction | Fichier | Rôle |
|---|---|---|
useInterfaceShow(id, fn) | interface.js | reçoit les data de interface:show (mises en attente si la vue n'est pas encore montée) |
useNuiEvent(action, fn) | nui.js | reçoit les messages Core.nui(action, { data }) |
fetchNui(nom, data) | nui.js | appelle Core.rnui(nom, …) et renvoie la réponse (promesse) |
postNui(nom, data) | nui.js | même chose sans attendre la réponse |
useLocale() | i18n.js | renvoie t(clé, [valeurs]) ; les textes viennent des fichiers i18n/langs du jeu |
Les données arrivent telles que le Lua les envoie ; une table Lua vide arrive sous forme de tableau vide [].
2. Les couleurs : uniquement des variables CSS
ui/src/components/prime/Prime.module.css :
/* Jamais de couleur en dur : tout suit le thème du serveur */
.ticket {
position: absolute;
right: 2vh;
top: 40%;
padding: 1.6vh 2vh;
background: rgb(var(--color-surface));
border-left: 0.4vh solid rgb(var(--color-primary));
color: #fff;
}
.titre { color: rgb(var(--color-muted)); font-size: 1.3vh; text-transform: uppercase; }
.montant { display: block; font-size: 3vh; color: rgb(var(--color-primary)); }
.bouton {
margin-top: 1vh;
background: rgb(var(--color-primary));
color: rgb(var(--color-on-primary)); /* noir ou blanc selon la clarté de la couleur */
}Les variables du thème contiennent trois nombres R G B, à utiliser avec rgb(var(--…)) (et une opacité si besoin : rgb(var(--color-primary) / 0.5)). Les principales, définies dans style.css :
| Variable | Rôle |
|---|---|
--color-primary | couleur du serveur (panel Informations › Identité › Couleur, colors.accent) ; toujours unie |
--fill-primary-image | dégradé de la couleur du serveur (linear-gradient(…)), ou none quand elle est unie (colors.accentGradient) |
--color-on-primary | texte lisible sur la couleur du serveur, calculé automatiquement (sur la moyenne des deux couleurs en mode dégradé) |
--color-success | réussite |
--color-dark, --color-surface, --color-surface-light, --color-panel, --color-card, --color-control, --color-button | fonds |
--color-muted | texte secondaire |
Les classes Tailwind du projet utilisent les mêmes variables (bg-primary, text-on-primary, bg-surface…). La classe bg-primary pose déjà le dégradé éventuel par-dessus la couleur unie. Dans un module CSS, pour une surface pleine qui doit suivre le dégradé :
.bouton {
background-color: rgb(var(--color-primary));
background-image: var(--fill-primary-image, none);
color: rgb(var(--color-on-primary));
}Garde --color-primary seul pour les textes, bordures et filets : c'est la règle suivie par toutes les vues livrées.
Comment le thème arrive :
- au chargement, le Lua envoie
ui:loadavec la couleur du serveur ; l'interface pose--color-primaryet--color-on-primary; ui:accentenvoie la couleur et son dégradé éventuel dès que les réglages sont chargés, puis à chaque changement au panel :--color-primary,--fill-primary-imageet--color-on-primarysont mis à jour à chaud ;- la page Apparence du panel envoie d'autres variables CSS et du CSS libre (
ui/dist/panel-theme.js) : ils s'appliquent par-dessus, sans rebuild.
Écris donc ta vue uniquement avec ces variables : elle suivra la couleur et l'apparence de chaque serveur.
3. Le registre
Dans ui/src/main.jsx, importe ta vue et ajoute-la au tableau interfaces :
import Prime from "./components/prime/Prime";
const interfaces = [
// … vues existantes …
{
id: "prime", // l'id utilisé par Core.nui("interface:show", { id = "prime" })
name: "Prime de minuit", // nom affiché dans l'outil de développement
component: Prime,
defaultVisible: false, // masquée au départ
devData: { montant: 500 }, // facultatif : données d'essai pour l'outil de développement
},
];4. Les textes
Ajoute les clés utilisées par la vue (prime.ui.titre, prime.ui.montant avec un %s, prime.ui.fermer) dans les quatre fichiers i18n/langs/*.json, comme expliqué dans Créer ton module. L'interface reçoit le dictionnaire de la langue active au chargement, et à nouveau quand un texte est modifié au panel.
5. Le build
cd resources/socle/ui
npm install
npm run buildnpm run build régénère ui/dist/ (page principale et écran de chargement). Le jeu ne charge que dist : sans build, ta vue n'existe pas en jeu. Redémarre ensuite la ressource socle sur ton serveur de développement, ou le serveur entier.
6. Le Lua qui l'ouvre
-- client : afficher le ticket après une prime versée
local function afficherTicket(montant)
Core.Focus.acquire("prime", true, false)
Core.nui("interface:show", { id = "prime", data = { montant = montant } })
end
CreateThread(function()
Core.rnui("prime:fermer", function()
Core.Focus.release("prime")
Core.nui("interface:hide", { id = "prime" })
end)
end)Travailler hors du jeu
cd resources/socle/ui
npm run devVite sert l'interface dans ton navigateur. En développement uniquement :
- F2 ouvre l'outil de développement : la liste des vues, à afficher ou masquer, avec leurs
devData; ?accent=255,0,0dans l'adresse rejoue la couleur du serveur, pour vérifier que toute ta vue la suit ; ajoute&accent2=236,72,153&angle=90pour la rejouer en dégradé ;- les textes du jeu sont servis depuis
i18n/langs, et les fichiers deui/dev-fixtures/(états de démonstration) ne sont jamais copiés dansdist.
Bundle léger
L'interface se charge à chaque connexion de chaque joueur. Garde-la légère :
- aucun CDN ni ressource externe : polices, images et sons sont locaux (
ui/public/,assets/du Socle ou médiathèque du panel) ; - pas de nouvelle dépendance npm sans raison forte : React 18 suffit pour une vue ;
- des images optimisées (WebP), pas de vidéo dans le bundle.
Les mises à jour et ta vue
Conséquences pratiques :
- préfère les vues existantes (tableau en haut de page) chaque fois que c'est possible ;
- garde tes vues dans leur propre dossier
ui/src/components/<ton_module>/et note les lignes ajoutées àmain.jsx: c'est tout ce qu'il faut remettre ; - avant de recompiler après une mise à jour, assure-toi que ton
ui/srccorrespond à la version installée.