Onboarding guidé systématique + tableau de bord simplifié
Comportement SYSTÉMATIQUE à chaque création de jeu (admin compris), pas une formalité réservée à l'inscription : /onboarding (routes/onboarding/) devient le point d'entrée unique — 4 cartes retournables (survol = explication au dos), défilement horizontal animé vers le nom du jeu. Un admin y repasse à volonté (pas de project_slug dédié, jamais bloqué/ redirigé vers un projet précédent) ; un compte "user" n'en a plus qu'un créé d'office (routes/auth/register_2fa.py), guidé ici à la place. - db/games/game_type_catalog.py : catalogue des 4 types (Quiz/ Embranchement-escape game/RPG/Créer mon jeu de A à Z), _meta['onboarding_type'] décide du "kind" du premier écran créé et si le tableau de bord complet reste accessible. - routes/games/game_dashboard.py, templates/game_dashboard_simple.html : un type restreint (quiz/embranchement/rpg) voit désormais SON tableau de bord (même route que "custom"), rendu en version simplifiée — juste ses écrans en cartes avec un aperçu RÉEL du contenu (scène mise à l'échelle par container query CSS, adaptée à la largeur réelle de la carte). "+ Ajouter un écran" n'y propose pas de choix de type : imposé par le projet (routes/screens/screens_new.py), verrouillé aussi côté serveur. - core/auth_guard.py : plus de blocage de game_dashboard par type — la restriction se fait au rendu, pas à l'accès à la route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0f23024882
commit
76a50fa87a
@@ -0,0 +1,15 @@
|
||||
// Dashboard simplifié (templates/game_dashboard_simple.html, voir
|
||||
// routes/games/game_dashboard.py) — affiche/masque le petit formulaire
|
||||
// "+ Ajouter un écran", même comportement que toggleDashCreate() du
|
||||
// tableau de bord complet (static/js/screen_edit/tabs-and-blocks.js),
|
||||
// repris ici pour ne pas dépendre de tout ce fichier (pensé pour l'éditeur
|
||||
// complet, pas cette page).
|
||||
function toggleDashCreate(tab) {
|
||||
var bar = document.getElementById("dashCreate-" + tab);
|
||||
if (!bar) return;
|
||||
bar.style.display = bar.style.display === "none" ? "flex" : "none";
|
||||
if (bar.style.display === "flex") {
|
||||
var firstInput = bar.querySelector("input, select");
|
||||
if (firstInput) firstInput.focus();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Onboarding guidé (templates/onboarding/onboarding_new.html) — interactions
|
||||
// pures DOM, aucune logique métier ici (voir routes/onboarding/
|
||||
// onboarding_new.py pour la validation réelle, côté serveur). Deux
|
||||
// "pages" (choix du type / nom du jeu) posées côte à côte dans
|
||||
// #onboardingTrack, révélées par un défilement horizontal animé en CSS
|
||||
// (voir .onboardingTrack--step2 dans static/style.css) plutôt qu'un vrai
|
||||
// changement de page — pas de rechargement, pas d'aller-retour serveur
|
||||
// tant que le formulaire n'est pas soumis pour de vrai.
|
||||
|
||||
function forgeOnboardingGoToStep(step) {
|
||||
var track = document.getElementById("onboardingTrack");
|
||||
if (track) track.classList.toggle("onboardingTrack--step2", step === 2);
|
||||
if (step === 2) {
|
||||
var nameField = document.getElementById("onboardingNameInput");
|
||||
// Après la fin de l'animation de défilement (voir la durée de
|
||||
// transition posée sur .onboardingTrack) — focus immédiat plus tôt
|
||||
// ferait sauter le scroll du champ à moitié visible.
|
||||
if (nameField) setTimeout(function () { nameField.focus(); }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function forgeOnboardingChooseType(card) {
|
||||
document.querySelectorAll(".onboardingCard").forEach(function (c) {
|
||||
c.classList.toggle("is-selected", c === card);
|
||||
});
|
||||
var hiddenType = document.getElementById("onboardingTypeHidden");
|
||||
if (hiddenType) hiddenType.value = card.dataset.type || "";
|
||||
forgeOnboardingUpdateSubmitState();
|
||||
forgeOnboardingGoToStep(2);
|
||||
}
|
||||
|
||||
function forgeOnboardingUpdateSubmitState() {
|
||||
var nameField = document.getElementById("onboardingNameInput");
|
||||
var hiddenType = document.getElementById("onboardingTypeHidden");
|
||||
var submitBtn = document.getElementById("onboardingSubmitBtn");
|
||||
if (!nameField || !hiddenType || !submitBtn) return;
|
||||
submitBtn.disabled = !(hiddenType.value && nameField.value.trim());
|
||||
}
|
||||
|
||||
function forgeOnboardingInit() {
|
||||
// Appelé directement (pas de DOMContentLoaded) : ce fichier est chargé
|
||||
// par un <script src> posé à la fin du contenu de la page — les
|
||||
// éléments dont on a besoin existent déjà à ce stade. Nécessaire aussi
|
||||
// pour la navigation pjax (voir static/pjax.js — runScriptsIn
|
||||
// ré-exécute ce script à chaque page swappée, mais DOMContentLoaded,
|
||||
// lui, ne se redéclenche jamais après le tout premier chargement réel).
|
||||
|
||||
// Entrée en cascade des cartes — délai posé en JS plutôt qu'en CSS pur
|
||||
// (nth-child) pour rester correct quel que soit le nombre de types
|
||||
// affichés (voir db.ONBOARDING_TYPES, appelé à générer la liste).
|
||||
document.querySelectorAll(".onboardingCard").forEach(function (card, i) {
|
||||
card.style.animationDelay = (i * 70) + "ms";
|
||||
card.addEventListener("click", function () { forgeOnboardingChooseType(card); });
|
||||
});
|
||||
|
||||
var backBtn = document.getElementById("onboardingBackBtn");
|
||||
if (backBtn) backBtn.addEventListener("click", function () { forgeOnboardingGoToStep(1); });
|
||||
|
||||
var nameField = document.getElementById("onboardingNameInput");
|
||||
if (nameField) nameField.addEventListener("input", forgeOnboardingUpdateSubmitState);
|
||||
|
||||
forgeOnboardingUpdateSubmitState();
|
||||
}
|
||||
|
||||
forgeOnboardingInit();
|
||||
@@ -226,6 +226,87 @@ input[type=checkbox]{ width:auto; margin-top:6px; }
|
||||
.content{ max-width:760px; margin:0 auto; padding:28px 20px; }
|
||||
|
||||
.card{ border:1px solid var(--border); border-radius:10px; padding:16px; background:var(--panel); margin:12px 0; }
|
||||
/* Onboarding guidé (voir routes/onboarding/onboarding_new.py) — comportement
|
||||
SYSTÉMATIQUE à chaque création de jeu (admin compris) : c'est la première
|
||||
impression du moteur, pensée pour "vendre du rêve" plutôt qu'un simple
|
||||
formulaire administratif. Deux "pages" côte à côte (choix du type / nom
|
||||
du jeu) dans #onboardingTrack, révélées par un défilement horizontal
|
||||
animé — voir static/js/onboarding/onboarding_new.js pour la mécanique
|
||||
(jamais de JS mélangé au HTML, voir CLAUDE.md). */
|
||||
.onboardingPage{ max-width:1200px; }
|
||||
.onboardingError{ text-align:center; color:var(--accent); font-weight:600; margin-bottom:12px; }
|
||||
.onboardingTitle{ display:block; text-align:center; font-size:2em; font-weight:700; margin:0 0 24px; }
|
||||
|
||||
.onboardingViewport{ overflow:hidden; }
|
||||
.onboardingTrack{
|
||||
display:flex; width:200%;
|
||||
transition:transform .65s cubic-bezier(.65,0,.35,1);
|
||||
}
|
||||
.onboardingTrack.onboardingTrack--step2{ transform:translateX(-50%); }
|
||||
.onboardingStep{ flex:0 0 50%; width:50%; box-sizing:border-box; padding:12px 24px; }
|
||||
|
||||
/* ---------- Étape 1 : cartes de type, grandes, retournables ---------- */
|
||||
.onboardingGrid{ display:flex; gap:18px; height:66vh; min-height:420px; max-height:640px; }
|
||||
.onboardingCard{
|
||||
flex:1; position:relative; cursor:pointer; padding:0; margin:0; border:none; background:none;
|
||||
border-radius:20px; perspective:1400px;
|
||||
opacity:0; animation:forgeOnboardCardIn .5s ease both;
|
||||
transition:flex-grow .3s ease;
|
||||
}
|
||||
.onboardingCard:hover{ flex-grow:1.25; }
|
||||
.onboardingCardInner{
|
||||
position:relative; display:block; width:100%; height:100%;
|
||||
transform-style:preserve-3d; transition:transform .6s cubic-bezier(.4,.2,.2,1);
|
||||
border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,.28);
|
||||
}
|
||||
.onboardingCard:hover .onboardingCardInner, .onboardingCard.is-selected .onboardingCardInner{ transform:rotateY(180deg); }
|
||||
.onboardingCardFace{
|
||||
position:absolute; inset:0; backface-visibility:hidden; border-radius:20px;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px;
|
||||
padding:20px; text-align:center;
|
||||
}
|
||||
.onboardingCardFront{ color:#fff; }
|
||||
.onboardingCardIcon{ font-size:3.4em; line-height:1; filter:drop-shadow(0 4px 10px rgba(0,0,0,.35)); }
|
||||
.onboardingCardLabel{ font-weight:800; font-size:1.3em; letter-spacing:.01em; }
|
||||
.onboardingCardBack{
|
||||
transform:rotateY(180deg); background:var(--panel); border:2px solid var(--accent);
|
||||
}
|
||||
.onboardingCardDesc{ color:var(--text); font-size:1em; line-height:1.5; }
|
||||
|
||||
/* Une couleur de dégradé par type — voir data-type dans le template,
|
||||
généré depuis db.ONBOARDING_TYPES (mêmes clés que ci-dessous). */
|
||||
.onboardingCard[data-type="quiz"] .onboardingCardFront{ background:linear-gradient(150deg, #2ea3ff, #1560d8); }
|
||||
.onboardingCard[data-type="embranchement"] .onboardingCardFront{ background:linear-gradient(150deg, #b06bff, #6a2fd6); }
|
||||
.onboardingCard[data-type="rpg"] .onboardingCardFront{ background:linear-gradient(150deg, #33d182, #159a5c); }
|
||||
.onboardingCard[data-type="custom"] .onboardingCardFront{ background:var(--gradient); color:#1a1108; }
|
||||
.onboardingCard[data-type="custom"] .onboardingCardIcon{ filter:drop-shadow(0 4px 10px rgba(0,0,0,.2)); }
|
||||
|
||||
/* ---------- Étape 2 : nom du jeu, grand champ centré ---------- */
|
||||
.onboardingStep--name{ display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:66vh; }
|
||||
.onboardingBackBtn{
|
||||
align-self:flex-start; background:none; border:none; color:var(--forge-text-muted, #8b95a7);
|
||||
font-size:.95em; cursor:pointer; padding:6px 0; margin-bottom:auto;
|
||||
}
|
||||
.onboardingBackBtn:hover{ color:var(--accent); }
|
||||
.onboardingNameBox{ width:100%; max-width:520px; text-align:center; }
|
||||
.onboardingNameInput{
|
||||
width:100%; text-align:center; font-size:1.5em; font-weight:600; padding:18px 16px;
|
||||
border-radius:14px; border:2px solid var(--border); background:var(--panel); color:var(--text);
|
||||
transition:border-color .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
.onboardingNameInput:focus{ outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(255,95,46,.2); }
|
||||
.onboardingSubmitBtn{
|
||||
margin-top:20px; background:var(--gradient); border:none; font-weight:700; letter-spacing:.01em;
|
||||
font-size:1.05em; padding:12px 28px; transition:opacity .18s ease, transform .12s ease;
|
||||
}
|
||||
.onboardingSubmitBtn:disabled{ opacity:.4; cursor:not-allowed; }
|
||||
.onboardingSubmitBtn:not(:disabled):hover{ transform:translateY(-2px); }
|
||||
|
||||
@keyframes forgeOnboardCardIn{ from{ opacity:0; transform:translateY(14px); } to{ opacity:1; transform:translateY(0); } }
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.onboardingCard{ animation:none; opacity:1; }
|
||||
.onboardingTrack, .onboardingCardInner{ transition:none; }
|
||||
}
|
||||
.inline-form .fieldRow{ display:flex; gap:8px; align-items:flex-end; }
|
||||
.inline-form .fieldRow input{ flex:1; }
|
||||
.fieldRow{ display:flex; gap:10px; }
|
||||
@@ -773,6 +854,43 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
|
||||
.dashCreateBar input, .dashCreateBar select{ width:auto; flex:1 1 160px; }
|
||||
.dashDangerZone{ margin-top:28px; }
|
||||
|
||||
/* Dashboard simplifié (templates/game_dashboard_simple.html, voir
|
||||
routes/games/game_dashboard.py) — un compte "restreint" (quiz/
|
||||
embranchement/rpg) ne voit que ses écrans, en cartes avec un aperçu
|
||||
RÉEL de leur contenu (élements % réutilisés tels quels pour un écran
|
||||
document — voir elstyle/eltransform — ou objets de scène mis à
|
||||
l'échelle pour un écran jeu_2d, voir --scenePreviewScale posé par
|
||||
carte). */
|
||||
.screenCardGrid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:20px; margin-top:14px; }
|
||||
.screenCard{
|
||||
border:1px solid var(--border); border-radius:14px; overflow:hidden; background:var(--panel);
|
||||
transition:transform .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
.screenCard:hover{ transform:translateY(-3px); box-shadow:0 10px 24px rgba(0,0,0,.25); }
|
||||
.screenCardPreviewLink{ display:block; }
|
||||
.screenPreviewBox{
|
||||
position:relative; width:100%; overflow:hidden; container-type:inline-size;
|
||||
background:#0b0d12; border-bottom:1px solid var(--border);
|
||||
}
|
||||
/* Scène "jeu_2d" (positionnement en PIXELS fixes, voir render_scene_object.py)
|
||||
mise à l'échelle EXACTEMENT à la largeur réelle de .screenPreviewBox —
|
||||
"cqw" (1% de la largeur du conteneur, posé ci-dessus) rend ce calcul
|
||||
toujours juste, quelle que soit la largeur réellement occupée par la
|
||||
carte (grille responsive) — jamais une largeur fixe supposée. */
|
||||
.screenPreviewScene{
|
||||
position:absolute; top:0; left:0; transform-origin:top left;
|
||||
transform:scale(calc(100cqw / var(--scene-w)));
|
||||
}
|
||||
.screenPreviewEmpty{
|
||||
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
||||
color:var(--forge-text-muted, #8b95a7); font-size:.85em;
|
||||
}
|
||||
.screenCardFooter{ display:flex; align-items:center; justify-content:space-between; gap:8px; padding:10px 12px; }
|
||||
.screenCardName{ font-weight:600; color:var(--text); text-decoration:none; }
|
||||
.screenCardName:hover{ color:var(--accent); }
|
||||
.screenCardActions{ display:flex; gap:4px; }
|
||||
.screenCardActions form{ display:inline; }
|
||||
|
||||
/* ---------- Filtre de colonnes (onglet Données, game_dashboard.html) ---------- */
|
||||
/* Menu déroulant compact sous le bouton "🔧 Colonnes" — un objet avec
|
||||
beaucoup de champs rend le tableau illisible, ce menu permet d'en
|
||||
|
||||
Reference in New Issue
Block a user