Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1420 lines
38 KiB
CSS
1420 lines
38 KiB
CSS
/* Tokens (--bg, --panel, --accent...) définis une seule fois dans
|
|
forge-theme.scss, qui @importe ce fichier APRÈS Bulma — ce fichier
|
|
continue de référencer les mêmes noms de variable qu'avant la charte
|
|
Forge (voir les alias --bg/--panel/--panel2/--border/--text/--text-dim/
|
|
--danger dans forge-theme.scss), pour n'avoir à toucher qu'aux VALEURS
|
|
des tokens, jamais aux ~600 lignes de règles ci-dessous qui les
|
|
consomment déjà. */
|
|
*{ box-sizing:border-box; }
|
|
|
|
body {
|
|
margin:0;
|
|
font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
background:var(--bg);
|
|
color:var(--text);
|
|
}
|
|
|
|
h1 {
|
|
font-size:20px;
|
|
margin:0 0 6px;
|
|
}
|
|
|
|
h2 {
|
|
font-size:13px;
|
|
text-transform:uppercase;
|
|
letter-spacing:.05em;
|
|
color:var(--text-dim);
|
|
margin:24px 0 8px;
|
|
}
|
|
|
|
h3 {
|
|
font-size:13px;
|
|
color:var(--accent);
|
|
margin:14px 0 6px;
|
|
}
|
|
|
|
label {
|
|
display:block;
|
|
font-size:12px;
|
|
color:var(--text-dim);
|
|
margin:8px 0 4px;
|
|
}
|
|
|
|
input[type="text"], input[type="number"], input[type="date"], textarea, select{
|
|
width:100%; background:var(--panel2); border:1px solid var(--border); border-radius:6px;
|
|
color:var(--text); padding:8px 10px; font-size:13px; font-family:inherit;
|
|
}
|
|
|
|
input[type="checkbox"] {
|
|
width:auto;
|
|
margin-top:6px;
|
|
}
|
|
|
|
/* .topbar : Bulma fournit déjà la mise en page (navbar) — cette classe ne
|
|
sert plus qu'à la repérer dans les layouts plein-écran
|
|
(body.builderBody/objectEditBody, voir plus bas) et à la rattacher au
|
|
thème sombre. */
|
|
.topbar{ border-bottom:1px solid var(--border); }
|
|
|
|
/* Barre de navigation d'UN JEU (voir base.html) — toujours visible entre
|
|
les différents éditeurs d'un même jeu, en ligne (contrairement à
|
|
.navList/.navRow ci-dessous, empilés verticalement dans le panneau
|
|
"Créer" du tableau de bord). */
|
|
.gameNavBar {
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
justify-content:center;
|
|
gap:6px;
|
|
padding:8px 20px;
|
|
background:transparent;
|
|
border-bottom:1px solid var(--border);
|
|
}
|
|
|
|
.gameNavLink {
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
padding:6px 10px;
|
|
border:1px solid var(--border);
|
|
border-radius:6px;
|
|
background:var(--panel);
|
|
text-decoration:none;
|
|
color:var(--text);
|
|
font-size:12.5px;
|
|
}
|
|
|
|
.gameNavLink:hover {
|
|
background:var(--panel2);
|
|
border-color:var(--accent);
|
|
}
|
|
|
|
.gameNavLink.active {
|
|
background:var(--panel2);
|
|
border-color:var(--accent);
|
|
color:var(--accent);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.inline-form .fieldRow {
|
|
display:flex;
|
|
gap:8px;
|
|
align-items:flex-end;
|
|
}
|
|
.inline-form .fieldRow input{ flex:1; }
|
|
|
|
.fieldRow {
|
|
display:flex;
|
|
gap:10px;
|
|
}
|
|
.fieldRow > div{ flex:1; }
|
|
.formField{ margin-bottom:6px; }
|
|
|
|
button {
|
|
background:var(--panel2);
|
|
border:1px solid var(--border);
|
|
color:var(--text);
|
|
border-radius:10px;
|
|
padding:9px 14px;
|
|
cursor:pointer;
|
|
font-size:13px;
|
|
}
|
|
|
|
/* Un <button> qui doit se comporter comme un <a> visuellement (ex. ouvrir
|
|
un panneau plutôt que suivre un href, mais garder l'apparence d'un lien
|
|
dans un tableau) — annule le style bouton par défaut ci-dessus. */
|
|
.linkLikeButton {
|
|
background:none;
|
|
border:none;
|
|
padding:0;
|
|
color:var(--accent);
|
|
text-decoration:none;
|
|
font-size:inherit;
|
|
}
|
|
.linkLikeButton:hover{ text-decoration:underline; }
|
|
|
|
/* button.primary : couleur/dégradé posés une seule fois, centralement,
|
|
dans forge-theme.scss (".button.is-primary, button.primary") — pas ici,
|
|
pour ne pas entrer en conflit de cascade avec cette règle centrale. */
|
|
button.smallBtn {
|
|
padding:5px 10px;
|
|
font-size:11.5px;
|
|
}
|
|
|
|
button.danger {
|
|
color:var(--danger);
|
|
border-color:rgb(240 68 56 / 40%);
|
|
}
|
|
.actionsRow{ margin-top:16px; }
|
|
|
|
/* Nav compacte à une seule ligne (icône + libellé) — pour une liste
|
|
d'actions fixe (ex. colonne "Créer" du tableau de bord d'un jeu) : sobre
|
|
et dense plutôt qu'une carte à deux lignes. */
|
|
.navList {
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:3px;
|
|
}
|
|
|
|
.navRow {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:8px;
|
|
padding:7px 10px;
|
|
border:1px solid var(--border);
|
|
border-radius:6px;
|
|
background:var(--panel);
|
|
text-decoration:none;
|
|
color:var(--text);
|
|
font-size:12.5px;
|
|
}
|
|
|
|
.navRow:hover {
|
|
background:var(--panel2);
|
|
border-color:var(--accent);
|
|
}
|
|
|
|
.hint {
|
|
font-size:11.5px;
|
|
color:var(--text-dim);
|
|
line-height:1.5;
|
|
}
|
|
|
|
.autosaveStatus {
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
transition:color .2s ease;
|
|
}
|
|
.autosaveStatus.saving{ color:var(--text-dim); }
|
|
.autosaveStatus.saved{ color:var(--status-success); }
|
|
|
|
.warningBanner{ border:1px solid var(--danger); background:rgb(240 68 56 / 10%); color:var(--text);
|
|
border-radius:8px; padding:10px 14px; font-size:12.5px; margin:12px 0; }
|
|
|
|
.dangerZone {
|
|
border:1px solid rgb(240 68 56 / 40%);
|
|
border-radius:8px;
|
|
padding:10px 12px;
|
|
margin-top:14px;
|
|
}
|
|
|
|
.dangerZone h3 {
|
|
color:var(--danger);
|
|
margin-top:0;
|
|
}
|
|
|
|
.fieldsExistingRow{ display:flex; align-items:center; justify-content:space-between;
|
|
padding:8px 10px; border:1px solid var(--border); border-radius:6px; margin-bottom:6px; font-size:12.5px; }
|
|
.fieldsExistingRow form{ margin:0; }
|
|
|
|
.fieldRowCard{ margin-bottom:10px; }
|
|
.relationTarget{ margin-top:6px; }
|
|
|
|
/* Liste de conditions de "Donnée liée" (voir c_clause_list.py) : un nombre
|
|
ILLIMITÉ de lignes peut s'y accumuler — sans limite de hauteur, le
|
|
panneau de propriétés entier deviendrait interminable à faire défiler
|
|
dès qu'on en pose plus de 3-4. max-height + overflow-y borne cette
|
|
LISTE elle-même à une hauteur confortable, le reste du panneau (et le
|
|
bouton "+ Ajouter une condition" juste en dessous) reste toujours à
|
|
portée sans avoir à chercher le bas de la liste. */
|
|
.clauseListWrap {
|
|
max-height:260px;
|
|
overflow-y:auto;
|
|
padding-right:2px;
|
|
}
|
|
.clauseRow{ margin-bottom:8px; }
|
|
.clauseRow:last-child{ margin-bottom:0; }
|
|
|
|
/* .dataTable : Bulma fournit déjà la mise en page du tableau (classes
|
|
"table is-fullwidth is-hoverable", voir data_list.html) — ceci ne fait
|
|
plus que tronquer les cellules trop longues avec "...", pour garder une
|
|
vue d'ensemble compacte (le détail complet s'ouvre dans la modale Bulma
|
|
ci-dessous via le bouton 👁️). */
|
|
.dataTable td {
|
|
max-width:220px;
|
|
white-space:nowrap;
|
|
overflow:hidden;
|
|
text-overflow:ellipsis;
|
|
}
|
|
|
|
/* ---------- Détail d'une entrée (bouton 👁️) ---------- */
|
|
|
|
/* Contenu du panneau de détail (voir openRowDetailPanel() dans
|
|
game_dashboard.html) — affiche chaque champ en entier, sans troncature,
|
|
contrairement au tableau dense qui les tronque. */
|
|
.rowDetailField {
|
|
padding:10px 0;
|
|
border-bottom:1px solid var(--border);
|
|
}
|
|
.rowDetailField:last-child{ border-bottom:none; }
|
|
|
|
.rowDetailLabel {
|
|
font-size:11px;
|
|
color:var(--text-dim);
|
|
text-transform:uppercase;
|
|
letter-spacing:.05em;
|
|
margin-bottom:4px;
|
|
}
|
|
|
|
.rowDetailValue {
|
|
font-size:13.5px;
|
|
white-space:pre-wrap;
|
|
word-break:break-word;
|
|
}
|
|
|
|
.content-wide{ max-width:1600px; }
|
|
|
|
.twoCol {
|
|
display:flex;
|
|
gap:24px;
|
|
align-items:flex-start;
|
|
margin-top:10px;
|
|
}
|
|
.twoCol .colLeft{ flex:0 0 340px; }
|
|
|
|
.twoCol .colRight {
|
|
flex:1;
|
|
min-width:0;
|
|
}
|
|
|
|
@media (width <= 820px){
|
|
.twoCol{ flex-direction:column; }
|
|
|
|
.twoCol .colLeft {
|
|
flex:none;
|
|
width:100%;
|
|
}
|
|
|
|
/* En dessous de 820px, les deux colonnes s'empilent : le défilement
|
|
interne séparé par colonne n'a plus de sens (il n'y a plus qu'une
|
|
colonne) — on revient à une page classique qui défile normalement,
|
|
comme pour l'éditeur d'écran ci-dessous. */
|
|
body.objectEditBody {
|
|
height:auto;
|
|
overflow:visible;
|
|
}
|
|
body.objectEditBody > main.content{ overflow:visible; }
|
|
|
|
.content-objectEdit .colLeft, .content-objectEdit .colRight {
|
|
height:auto;
|
|
overflow:visible;
|
|
}
|
|
.fieldsTableWrap{ max-height:60vh; }
|
|
}
|
|
|
|
/* ---------- Modifier un objet : tableau des champs existants ---------- */
|
|
|
|
/* Comme l'éditeur d'écran (body.builderBody, plus bas) : la page ne défile
|
|
jamais elle-même, seules les zones internes (colonne de gauche, tableau
|
|
des champs) défilent chacune de leur côté — indispensable dès qu'un objet
|
|
a beaucoup de champs, pour garder une vue d'ensemble sans perdre le
|
|
formulaire "Ajouter un champ" hors de l'écran. */
|
|
body.objectEditBody {
|
|
display:flex;
|
|
flex-direction:column;
|
|
height:100vh;
|
|
margin:0;
|
|
overflow:hidden;
|
|
}
|
|
body.objectEditBody > #pageChrome{ flex:0 0 auto; }
|
|
|
|
body.objectEditBody > main.content {
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
overflow:hidden;
|
|
display:flex;
|
|
flex-direction:column;
|
|
}
|
|
|
|
.content-objectEdit {
|
|
width:100%;
|
|
display:flex;
|
|
flex-direction:column;
|
|
min-height:0;
|
|
}
|
|
.content-objectEdit > h1, .content-objectEdit > .dashPanelHeader, .content-objectEdit > .hint, .content-objectEdit > form, .content-objectEdit > .warningBanner{ flex:0 0 auto; }
|
|
|
|
/* Un <form> qui doit envelopper les DEUX colonnes à la fois (un seul
|
|
formulaire soumis ensemble, ex. object_form.html : nom de l'objet à
|
|
gauche, liste de champs à droite) ne doit pas devenir lui-même une boîte
|
|
dans la mise en page flex — sinon .twoCol à l'intérieur perd la hauteur
|
|
disponible qu'il doit occuper. "display:contents" le rend transparent :
|
|
.twoCol se comporte comme s'il était directement enfant de
|
|
.content-objectEdit. */
|
|
.formPassthrough{ display:contents; }
|
|
|
|
.content-objectEdit .twoCol {
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
align-items:stretch;
|
|
}
|
|
|
|
.content-objectEdit .colLeft {
|
|
overflow-y:auto;
|
|
height:100%;
|
|
}
|
|
|
|
.content-objectEdit .colRight {
|
|
display:flex;
|
|
flex-direction:column;
|
|
min-height:0;
|
|
height:100%;
|
|
}
|
|
.content-objectEdit .colRight h2{ flex:0 0 auto; }
|
|
.content-objectEdit .colRight > .hint, .content-objectEdit .colRight > button{ flex:0 0 auto; }
|
|
|
|
.fieldsTableWrap {
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
overflow:auto;
|
|
border:1px solid var(--border);
|
|
border-radius:8px;
|
|
}
|
|
|
|
.fieldsTable {
|
|
width:100%;
|
|
border-collapse:collapse;
|
|
font-size:12.5px;
|
|
}
|
|
|
|
.fieldsTable th, .fieldsTable td {
|
|
border-bottom:1px solid var(--border);
|
|
padding:8px 10px;
|
|
text-align:left;
|
|
white-space:nowrap;
|
|
}
|
|
|
|
.fieldsTable th {
|
|
position:sticky;
|
|
top:0;
|
|
background:var(--panel2);
|
|
color:var(--text-dim);
|
|
z-index:1;
|
|
}
|
|
.fieldsTable tbody tr:hover{ background:rgb(255 255 255 / 2%); }
|
|
.fieldsTable tbody tr:last-child td{ border-bottom:none; }
|
|
|
|
.fieldsTable td input, .fieldsTable td select {
|
|
min-width:120px;
|
|
width:auto;
|
|
}
|
|
|
|
.fieldsTableActions {
|
|
display:flex;
|
|
gap:6px;
|
|
}
|
|
|
|
/* ---------- Éditeur d'écrans (façon Elementor/Bricks) ---------- */
|
|
|
|
/* La page de l'éditeur d'écran ne doit jamais défiler elle-même : elle
|
|
occupe exactement la hauteur de la fenêtre (topbar + zone de travail), et
|
|
ce sont les zones internes (panneaux, écran, panneau de logique) qui
|
|
défilent chacune de leur côté si besoin. */
|
|
body.builderBody {
|
|
display:flex;
|
|
flex-direction:column;
|
|
height:100vh;
|
|
margin:0;
|
|
overflow:hidden;
|
|
}
|
|
body.builderBody > #pageChrome{ flex:0 0 auto; }
|
|
|
|
body.builderBody > main.content {
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
overflow:hidden;
|
|
display:flex;
|
|
flex-direction:column;
|
|
}
|
|
|
|
.content-builder{
|
|
/* .content (ci-dessus) pose "margin:0 auto" pour centrer une page normale
|
|
— mais sur cette page, le body est un flex-container en colonne
|
|
(body.builderBody), et une marge horizontale "auto" y désactive le
|
|
stretch d'un flex-item : sa largeur est alors recalculée en
|
|
"shrink-to-fit" à partir de son contenu, un calcul qui traverse toute
|
|
la chaîne flex/%/aspect-ratio jusqu'au canvas et redonne un résultat
|
|
légèrement différent à chaque changement de contenu du panneau (élément
|
|
sélectionné, grille affichée...) — d'où le canvas qui grandit/rétrécit
|
|
sans raison apparente à chaque interaction. "width:100%" donne une
|
|
taille définie à la place : la marge "auto" ne fait alors que centrer
|
|
l'espace restant une fois "max-width" atteint, comme voulu à l'origine. */
|
|
width:100%;
|
|
max-width:1600px;
|
|
|
|
/* Sans fil d'Ariane sur cette page (voir breadcrumb_wrap dans
|
|
screen_edit.html), .content garde quand même son padding par défaut
|
|
(28px en haut) — réduit ici pour que le canevas commence le plus haut
|
|
possible sur la page. */
|
|
padding:10px 20px 8px;
|
|
}
|
|
|
|
/* 3 colonnes : ajout d'éléments (gauche) / écran en grand (centre) / propriétés (droite) —
|
|
toutes les trois occupent exactement la même hauteur (celle qui reste
|
|
disponible), avec leur propre défilement interne si leur contenu dépasse. */
|
|
.builder3 {
|
|
display:flex;
|
|
gap:20px;
|
|
align-items:stretch;
|
|
margin-top:8px;
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
}
|
|
|
|
/* overflow:hidden (pas auto) : le défilement doit se faire DANS le
|
|
canevas lui-même (voir .canvasFrame plus bas) ou dans l'onglet actif
|
|
(.flowCanvasWrap/.animTimelineWrap ont déjà leur propre défilement),
|
|
jamais sur cette zone entière — sans quoi la barre d'onglets et la barre
|
|
d'outils défileraient avec le contenu au lieu de rester fixes en haut. */
|
|
.builderCanvasArea {
|
|
flex:1 1 auto;
|
|
min-width:0;
|
|
height:100%;
|
|
overflow:hidden;
|
|
display:flex;
|
|
flex-direction:column;
|
|
}
|
|
|
|
/* ---------- Panneaux gauche/droite flottants ---------- */
|
|
|
|
/* Les deux panneaux (éléments de l'écran / propriétés) sont sortis du flux
|
|
flex de .builder3 (position:fixed) : .builderCanvasArea (flex:1 1 auto)
|
|
récupère automatiquement tout l'espace libéré, sans réglage
|
|
supplémentaire — les fermer donne donc une prévisualisation aussi large
|
|
que possible, aussi fidèle que possible à ce que "Jouer" affiche
|
|
réellement. Position et état ouvert/fermé mémorisés par écran (voir
|
|
floatPanel*() dans screen_edit.html) pour survivre à un changement de
|
|
sélection (qui remplace tout #builder3) ou un rechargement. */
|
|
.floatPanel{
|
|
position:fixed; top:90px; max-height:calc(100vh - 120px);
|
|
min-width:0; display:flex; flex-direction:column; z-index:60;
|
|
background:var(--panel); border:1px solid var(--border); border-radius:10px;
|
|
box-shadow:0 12px 40px rgb(0 0 0 / 55%);
|
|
}
|
|
.floatPanel.hidden{ display:none; }
|
|
|
|
/* Utilitaire générique (jusqu'ici .hidden n'existait que scopé à
|
|
.floatPanel/.columnFilterMenu — ex. la ligne d'édition d'un bloc de
|
|
logique dans screen_edit.html, un <tr>, avait besoin d'un .hidden qui
|
|
marche seul, sans classe compagne). */
|
|
.hidden{ display:none; }
|
|
|
|
/* Coin de redimensionnement (voir wireFloatPanelResize() dans
|
|
game_dashboard.html) — un panneau devient redimensionnable simplement en
|
|
lui ajoutant ce coin, JS mis à part rien d'autre à faire côté CSS. */
|
|
.floatPanelResizeHandle{
|
|
position:absolute; right:0; bottom:0; width:16px; height:16px; cursor:nwse-resize;
|
|
background:linear-gradient(135deg, transparent 50%, var(--border) 50%, var(--border) 60%, transparent 60%, transparent 70%, var(--border) 70%, var(--border) 80%, transparent 80%);
|
|
border-radius:0 0 10px;
|
|
}
|
|
|
|
.floatPanel--left {
|
|
left:24px;
|
|
width:280px;
|
|
}
|
|
|
|
.floatPanel--right {
|
|
right:24px;
|
|
width:340px;
|
|
}
|
|
|
|
.floatPanel--center {
|
|
left:50%;
|
|
width:560px;
|
|
max-width:calc(100vw - 48px);
|
|
transform:translateX(-50%);
|
|
}
|
|
|
|
/* Plein écran (voir makeFloatPanelFullscreenable() dans game_dashboard.html)
|
|
— juste un repère visuel (bordure accent) sur le panneau agrandi, tout le
|
|
positionnement réel est posé directement en style inline par le JS (pour
|
|
pouvoir revenir exactement à la position/taille d'avant en sortant). */
|
|
.floatPanel--fullscreen{ border-color:var(--accent); }
|
|
|
|
.floatPanelHeader{
|
|
display:flex; align-items:center; justify-content:space-between; gap:8px;
|
|
padding:8px 10px; border-bottom:1px solid var(--border); background:var(--panel2);
|
|
border-radius:10px 10px 0 0; cursor:move; flex:0 0 auto; user-select:none;
|
|
}
|
|
|
|
.floatPanelHeader .floatPanelTitle {
|
|
font-size:11px;
|
|
color:var(--text-dim);
|
|
text-transform:uppercase;
|
|
letter-spacing:.05em;
|
|
}
|
|
|
|
.floatPanelBody {
|
|
padding:12px;
|
|
overflow-y:auto;
|
|
min-height:0;
|
|
flex:1 1 auto;
|
|
}
|
|
|
|
.floatPanelReopenBtn {
|
|
position:fixed;
|
|
top:90px;
|
|
z-index:60;
|
|
display:none;
|
|
}
|
|
.floatPanelReopenBtn.visible{ display:inline-flex; }
|
|
#panelLeftReopenBtn{ left:24px; }
|
|
#propsPanelReopenBtn{ right:24px; }
|
|
|
|
.widgetGrid {
|
|
display:grid;
|
|
grid-template-columns:1fr 1fr;
|
|
gap:8px;
|
|
}
|
|
|
|
/* Icônes (widget Icône + galerie du panneau de propriétés) — SVG posé en
|
|
silhouette via "mask-image" plutôt qu'une police d'icônes (@font-face) :
|
|
se colore avec "color"/"background-color" comme du texte, MAIS ne dépend
|
|
d'aucun fichier de police externe — certains navigateurs/extensions
|
|
bloquent le chargement de polices web tierces (protection anti-
|
|
fingerprinting), ce qui rendait toutes les icônes Font Awesome invisibles
|
|
quelle que soit leur source (CDN ou même hébergées ici). Un simple SVG
|
|
affiché en masque n'est jamais concerné par ce blocage. Voir
|
|
render_icone.py (widget) et fontawesome_icons.py (liste + chemin des
|
|
fichiers, statiques dans static/icons/). */
|
|
.icon-svg{
|
|
display:inline-block; width:1em; height:1em; flex:0 0 auto;
|
|
background-color:currentcolor; mask-image:var(--icon-url); mask-size:contain; mask-repeat:no-repeat; mask-position:center;
|
|
}
|
|
|
|
/* Galerie d'icônes (panneau de propriétés) — voir
|
|
fontawesome_icons.py et addIconElement()/filterIconGallery() plus bas. */
|
|
.iconGallery {
|
|
display:grid;
|
|
grid-template-columns:repeat(6, 1fr);
|
|
gap:6px;
|
|
max-height:220px;
|
|
overflow-y:auto;
|
|
margin-top:8px;
|
|
}
|
|
|
|
.iconGalleryTile{
|
|
aspect-ratio:1; display:flex; align-items:center; justify-content:center;
|
|
background:var(--panel2); border:1px solid var(--border); border-radius:6px;
|
|
color:var(--text); cursor:grab; padding:0;
|
|
}
|
|
|
|
.iconGalleryTile .icon-svg {
|
|
width:16px;
|
|
height:16px;
|
|
}
|
|
|
|
.iconGalleryTile:hover {
|
|
border-color:var(--accent);
|
|
color:var(--accent);
|
|
}
|
|
.iconGalleryTile.dragging{ opacity:0.4; }
|
|
.iconGalleryTile.iconHidden{ display:none; }
|
|
.widgetBtnForm{ margin:0; }
|
|
|
|
.widgetBtn{
|
|
width:100%; display:flex; flex-direction:column; align-items:center; gap:4px;
|
|
padding:12px 6px; border:1px solid var(--border); border-radius:10px; background:var(--panel);
|
|
color:var(--text); cursor:pointer;
|
|
}
|
|
|
|
.widgetBtn:hover {
|
|
border-color:var(--accent);
|
|
background:var(--panel2);
|
|
}
|
|
.widgetIcon{ font-size:22px; }
|
|
|
|
.widgetLabel {
|
|
font-size:11px;
|
|
text-align:center;
|
|
line-height:1.2;
|
|
color:var(--text-dim);
|
|
}
|
|
|
|
.canvasToolbar {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:8px;
|
|
flex-wrap:wrap;
|
|
margin-bottom:10px;
|
|
}
|
|
|
|
.aspectBtn.active {
|
|
background:var(--accent);
|
|
border-color:var(--accent);
|
|
color:#fff;
|
|
}
|
|
|
|
.canvasFrame{
|
|
/* flex:1 1 auto + min-height:0 : occupe tout l'espace restant sous la
|
|
barre d'outils dans .builderTabPanel (voir screen_edit.html). Le
|
|
canevas (voir .canvas ci-dessous) est réglé pour remplir EXACTEMENT
|
|
ce cadre (100% x 100%, sans respecter l'aspect-ratio Portrait/Paysage/
|
|
Carré dans l'éditeur — ce réglage ne sert qu'à l'aperçu jouable/"Jouer",
|
|
voir screen_set_aspect.py) : "prendre toute la place disponible" et
|
|
"garder l'aspect-ratio" sont deux exigences contradictoires dès que la
|
|
fenêtre n'a pas exactement ce ratio, la première l'emporte ici. C'est
|
|
LUI qui défile (overflow:auto) si jamais le contenu déborde malgré
|
|
tout — jamais .builderCanvasArea (voir plus haut), pour garder la
|
|
barre d'onglets/d'outils toujours visible. */
|
|
flex:1 1 auto; min-height:0; width:100%; padding:4px; border:1px solid var(--border); border-radius:16px; background:var(--panel);
|
|
display:flex; overflow:auto;
|
|
}
|
|
|
|
.canvas{
|
|
position:relative; width:100%; height:100%; flex:1 1 auto; margin:0; background:#0b0d12; border-radius:8px;
|
|
overflow:hidden; border:1px solid var(--border);
|
|
}
|
|
|
|
.canvasElement{
|
|
/* Pas d'overflow:hidden ici : une échelle (transform:scale) ou une
|
|
animation posée sur cet élément (voir la Timeline d'animation) vit sur
|
|
CE cadre (voir filters/element_style_filter.py) — le rogner clippait
|
|
l'effet à la taille d'origine de la boîte au lieu de le laisser
|
|
grossir/bouger avec elle. La zone de l'écran elle-même (.canvas,
|
|
ci-dessus) reste rognée, donc rien ne peut déborder de l'écran final. */
|
|
box-sizing:border-box; cursor:move; user-select:none;
|
|
border:1px dashed transparent;
|
|
}
|
|
.canvasElement:hover{ border-color:rgb(91 140 255 / 50%); }
|
|
.canvasElement.selected{ border:2px solid var(--accent); }
|
|
|
|
.canvasElementInner {
|
|
width:100%;
|
|
height:100%;
|
|
pointer-events:none;
|
|
}
|
|
|
|
/* Surbrillance de l'élément sélectionné dans l'arborescence, posée en JS sur
|
|
sa propre balise (voir applySelectionHighlight) — seul moyen de la voir
|
|
sur le canevas quand cet élément est imbriqué dans un conteneur/répéteur/
|
|
groupe de champs, qui n'a pas de cadre ".canvasElement" séparé comme un
|
|
élément posé directement sur l'écran. Un "outline" qui dépasse la boîte
|
|
(offset positif) se fait couper par le premier ancêtre en overflow:hidden
|
|
(le canevas lui-même, ou un conteneur aux coins arrondis) dès que
|
|
l'élément touche un de ses bords — très fréquent pour une rangée de
|
|
jauges collée en haut d'un conteneur. Un box-shadow EN CREUX (inset)
|
|
reste toujours à l'intérieur de la boîte, jamais rogné, et épouse
|
|
naturellement les coins arrondis de l'élément lui-même. */
|
|
.elementHighlighted{
|
|
box-shadow: inset 0 0 0 2px var(--accent), inset 0 0 0 6px rgb(91 140 255 / 20%);
|
|
}
|
|
|
|
.resizeHandle{
|
|
position:absolute; right:0; bottom:0; width:14px; height:14px; cursor:nwse-resize;
|
|
background:var(--accent); border-radius:3px 0 6px; opacity:.85;
|
|
}
|
|
|
|
.listRow.selected {
|
|
border-color:var(--accent);
|
|
background:var(--panel2);
|
|
}
|
|
|
|
.childList {
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:6px;
|
|
margin:2px 0 10px 18px;
|
|
padding-left:10px;
|
|
border-left:2px dashed var(--border);
|
|
}
|
|
|
|
.childRow {
|
|
padding:7px 10px;
|
|
font-size:12px;
|
|
}
|
|
.childRow .rowTitle{ font-size:12px; }
|
|
|
|
/* Arborescence compacte des éléments de l'écran (panneau gauche) — une
|
|
ligne par élément, indentation par imbrication réelle (profondeur
|
|
illimitée) plutôt qu'un seul niveau d'enfants comme l'ancienne .childList,
|
|
pour rester lisible même avec beaucoup d'éléments. */
|
|
.elementTree {
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:1px;
|
|
max-height:55vh;
|
|
overflow-y:auto;
|
|
margin-bottom:14px;
|
|
}
|
|
|
|
.treeNode {
|
|
display:flex;
|
|
flex-direction:column;
|
|
}
|
|
|
|
.treeRow {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:2px;
|
|
}
|
|
|
|
.treeToggle{
|
|
flex:0 0 auto; width:16px; height:16px; padding:0; border:none; background:none; cursor:pointer;
|
|
color:var(--text-dim); font-size:10px; display:flex; align-items:center; justify-content:center;
|
|
transition:transform .12s ease;
|
|
}
|
|
.treeToggle:hover{ color:var(--text); }
|
|
|
|
.treeToggleSpacer {
|
|
flex:0 0 auto;
|
|
width:16px;
|
|
height:16px;
|
|
}
|
|
.treeNode.collapsed > .treeRow > .treeToggle{ transform:rotate(-90deg); }
|
|
.treeNode.collapsed > .treeChildren{ display:none; }
|
|
|
|
.treeRowLink{
|
|
flex:1; display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:5px;
|
|
text-decoration:none; color:var(--text); font-size:12.5px; line-height:1.5;
|
|
white-space:nowrap; overflow:hidden; min-width:0;
|
|
}
|
|
.treeRowLink:hover{ background:var(--panel2); }
|
|
|
|
.treeRowLink.selected {
|
|
background:var(--accent);
|
|
color:#0b0e14;
|
|
font-weight:600;
|
|
}
|
|
|
|
.treeIcon {
|
|
flex:0 0 auto;
|
|
font-size:12px;
|
|
}
|
|
|
|
.treeLabel {
|
|
overflow:hidden;
|
|
text-overflow:ellipsis;
|
|
white-space:nowrap;
|
|
}
|
|
|
|
.treeChildren {
|
|
display:flex;
|
|
flex-direction:column;
|
|
margin-left:10px;
|
|
padding-left:8px;
|
|
border-left:1px dashed var(--border);
|
|
}
|
|
.treeRow.dragging{ opacity:0.4; }
|
|
.treeRow.dropBefore{ box-shadow: inset 0 2px 0 0 var(--accent); }
|
|
.treeRow.dropAfter{ box-shadow: inset 0 -2px 0 0 var(--accent); }
|
|
|
|
.treeRow.dropInside {
|
|
background:rgb(91 140 255 / 25%);
|
|
box-shadow: inset 0 0 0 1px var(--accent);
|
|
border-radius:4px;
|
|
}
|
|
|
|
.contextMenu{
|
|
position:fixed; z-index:500; background:var(--panel); border:1px solid var(--border);
|
|
border-radius:8px; box-shadow:0 8px 24px rgb(0 0 0 / 40%); padding:4px; min-width:160px;
|
|
}
|
|
|
|
.contextMenu button{
|
|
display:block; width:100%; text-align:left; background:none; border:none; color:var(--text);
|
|
padding:7px 10px; border-radius:5px; font-size:13px; cursor:pointer;
|
|
}
|
|
.contextMenu button:hover{ background:var(--panel2); }
|
|
|
|
/* Élément à onglets (voir render_onglets.py) : la barre d'onglets elle-même
|
|
est un vrai composant Bulma ("tabs" > "ul" > "li", voir render_onglets.py)
|
|
— il ne reste ici que les panneaux, que Bulma n'a pas de composant pour. */
|
|
.forgeTabPanels{ min-width:0; }
|
|
.forgeTabPanel{ padding-top:10px; }
|
|
|
|
.propGroup {
|
|
border:1px solid var(--border);
|
|
border-radius:8px;
|
|
margin-bottom:8px;
|
|
background:var(--panel);
|
|
}
|
|
|
|
.propGroup > summary{
|
|
list-style:none; cursor:pointer; padding:9px 12px; font-size:13px; color:var(--accent);
|
|
font-weight:600; user-select:none; display:flex; align-items:center; justify-content:space-between;
|
|
}
|
|
.propGroup > summary::-webkit-details-marker{ display:none; }
|
|
|
|
.propGroup > summary::after {
|
|
content:"▸";
|
|
color:var(--text-dim);
|
|
font-size:11px;
|
|
transition:transform .15s;
|
|
}
|
|
.propGroup[open] > summary::after{ transform:rotate(90deg); }
|
|
.propGroup > summary:hover{ background:var(--panel2); }
|
|
.propGroupBody{ padding:2px 12px 12px; }
|
|
.propGroupBody h3{ margin-top:0; }
|
|
|
|
|
|
/* .posGrid/.controlRow/.sizeField/.sliderRow/.alignGroup : Bulma fournit
|
|
déjà la mise en page (columns/field/control/buttons has-addons, voir
|
|
screen_edit.html) — ce qui reste ici n'est que ce que Bulma ne fournit
|
|
pas nativement : l'espacement propre à un curseur (slider) et son
|
|
pastille de valeur affichée à côté. */
|
|
.posGrid input:disabled {
|
|
opacity:0.4;
|
|
cursor:not-allowed;
|
|
}
|
|
|
|
.sliderRow {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:10px;
|
|
}
|
|
.sliderRow input[type="range"]{ flex:1; }
|
|
|
|
.sliderValue {
|
|
min-width:44px;
|
|
text-align:right;
|
|
}
|
|
|
|
input[type="color"] {
|
|
width:52px;
|
|
height:34px;
|
|
padding:2px;
|
|
}
|
|
|
|
.propsPanel .sectionDivider, .floatPanel .sectionDivider {
|
|
border:none;
|
|
border-top:1px solid var(--border);
|
|
margin:14px 0;
|
|
}
|
|
.actionAddForm{ margin-top:10px; }
|
|
|
|
.uploadRow{ margin-top:6px; }
|
|
|
|
.uploadInput {
|
|
font-size:11px;
|
|
color:var(--text-dim);
|
|
}
|
|
|
|
.uploadStatus {
|
|
display:block;
|
|
margin-top:4px;
|
|
}
|
|
|
|
code {
|
|
background:var(--panel2);
|
|
border:1px solid var(--border);
|
|
border-radius:4px;
|
|
padding:1px 6px;
|
|
font-size:11.5px;
|
|
}
|
|
|
|
@media (width <= 1300px){
|
|
/* Sous cette largeur, les 3 colonnes s'empilent : le principe "tout tient
|
|
dans la fenêtre, chaque zone défile pour elle-même" n'a plus de sens
|
|
(il n'y a plus qu'une seule colonne) — on revient à une page classique
|
|
qui défile normalement. */
|
|
body.builderBody {
|
|
height:auto;
|
|
overflow:visible;
|
|
}
|
|
body.builderBody > main.content{ overflow:visible; }
|
|
|
|
.builder3 {
|
|
flex-direction:column;
|
|
flex:none;
|
|
}
|
|
|
|
.builderCanvasArea {
|
|
flex:none;
|
|
width:100%;
|
|
height:auto;
|
|
max-height:none;
|
|
overflow:visible;
|
|
}
|
|
|
|
/* Le "letterboxing" par hauteur (voir .canvas plus haut) suppose une
|
|
chaîne de hauteurs définies jusqu'ici — qui n'existe plus une fois
|
|
.builderCanvasArea repassée en "height:auto" ci-dessus. Revient donc à
|
|
un dimensionnement par LARGEUR, comme une page qui défile normalement. */
|
|
.canvasFrame {
|
|
flex:none;
|
|
overflow:visible;
|
|
}
|
|
|
|
.canvas {
|
|
height:auto;
|
|
width:100%;
|
|
}
|
|
|
|
/* Trop peu de largeur pour qu'un panneau flottant ait un sens (il
|
|
couvrirait tout l'écran) : les deux rejoignent la pile normale et
|
|
restent toujours ouverts/visibles, quel que soit l'état mémorisé —
|
|
.hidden est ignoré grâce au "!important" sur "display" ci-dessous. */
|
|
.floatPanel {
|
|
position:static;
|
|
flex:none;
|
|
width:100%;
|
|
height:auto;
|
|
max-height:none;
|
|
box-shadow:none;
|
|
top:auto;
|
|
left:auto;
|
|
right:auto;
|
|
display:flex !important;
|
|
}
|
|
.floatPanelReopenBtn{ display:none !important; }
|
|
}
|
|
|
|
/* ---------- Onglets du centre : Écran / Logique de la scène / Timeline
|
|
d'animation ---------- */
|
|
|
|
/* Un seul onglet visible à la fois (voir switchBuilderTab() dans
|
|
screen_edit.html), chacun occupant tout l'espace disponible dans
|
|
.builderCanvasArea — remplace l'ancien panneau du bas rétractable/
|
|
redimensionnable à la souris : un onglet "prend toute la place" par
|
|
nature, pas besoin d'une poignée de redimensionnement séparée, et en
|
|
changer d'onglet équivaut à "fermer" celui qu'on quitte. */
|
|
.builderTabs {
|
|
display:flex;
|
|
gap:2px;
|
|
flex:0 0 auto;
|
|
border-bottom:1px solid var(--border);
|
|
margin-bottom:10px;
|
|
}
|
|
|
|
.builderTabBtn{
|
|
background:transparent; border:none; border-bottom:2px solid transparent; border-radius:0;
|
|
padding:8px 12px; color:var(--text-dim); font-size:12.5px; cursor:pointer;
|
|
}
|
|
.builderTabBtn:hover{ color:var(--text); }
|
|
|
|
.builderTabBtn.active {
|
|
color:var(--accent);
|
|
border-bottom-color:var(--accent);
|
|
font-weight:600;
|
|
}
|
|
|
|
.builderTabPanel {
|
|
flex:1 1 auto;
|
|
min-height:0;
|
|
display:none;
|
|
flex-direction:column;
|
|
}
|
|
.builderTabPanel.active{ display:flex; }
|
|
|
|
/* Sous-onglets Champs/Données À L'INTÉRIEUR d'un panneau "Modifier un
|
|
objet" (voir game_dashboard.html) — classes distinctes de .builderTabs/
|
|
.builderTabBtn/.builderTabPanel (utilisées par le style, réutilisé tel
|
|
quel) pour que switchDashTab() (onglets du tableau de bord lui-même,
|
|
ciblage global) n'aille jamais les toucher par erreur. */
|
|
.objectPanelTabs{ margin:14px 0 10px; }
|
|
|
|
.objectPanelTabPanel {
|
|
display:none;
|
|
flex-direction:column;
|
|
}
|
|
.objectPanelTabPanel.active{ display:flex; }
|
|
|
|
/* Modale d'un bloc de logique (voir openLogicBlockPanel() dans
|
|
screen_edit.html) : le .floatPanelBody générique n'est pas un conteneur
|
|
flex par défaut — cette classe l'y transforme pour que .nodeForm/
|
|
.flowCanvasWrap gardent le même comportement (barre d'outils fixe,
|
|
canevas qui prend tout l'espace restant) qu'avant leur déménagement
|
|
hors de l'ancien onglet "Logique de la scène". */
|
|
.logicBlockPanelBody {
|
|
display:flex;
|
|
flex-direction:column;
|
|
height:100%;
|
|
}
|
|
|
|
.flowToolbar {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:10px;
|
|
flex-wrap:wrap;
|
|
margin-bottom:10px;
|
|
flex:0 0 auto;
|
|
}
|
|
|
|
.nodeForm {
|
|
border:1px solid var(--border);
|
|
border-radius:8px;
|
|
padding:12px;
|
|
margin-bottom:12px;
|
|
background:var(--panel2);
|
|
max-width:420px;
|
|
flex:0 0 auto;
|
|
}
|
|
.nodeForm label{ margin-top:8px; }
|
|
|
|
.flowCanvasWrap {
|
|
position:relative;
|
|
width:100%;
|
|
flex:1 1 auto;
|
|
min-height:120px;
|
|
overflow:auto;
|
|
border:1px dashed var(--border);
|
|
border-radius:8px;
|
|
background:var(--bg);
|
|
}
|
|
|
|
.flowCanvas {
|
|
position:relative;
|
|
width:2200px;
|
|
height:1400px;
|
|
}
|
|
|
|
.flowEdgesSvg {
|
|
position:absolute;
|
|
top:0;
|
|
left:0;
|
|
width:2200px;
|
|
height:1400px;
|
|
pointer-events:none;
|
|
z-index:0;
|
|
}
|
|
|
|
.flowEdgeLine {
|
|
stroke:var(--accent);
|
|
stroke-width:2;
|
|
pointer-events:stroke;
|
|
cursor:pointer;
|
|
}
|
|
.flowEdgeLine.flowEdgeTrue{ stroke:var(--status-success); }
|
|
.flowEdgeLine.flowEdgeFalse{ stroke:var(--danger); }
|
|
|
|
.flowNode{
|
|
position:absolute; width:220px; min-height:52px; border-radius:8px; border:1px solid var(--border);
|
|
background:var(--panel2); padding:10px 12px; font-size:12.5px; cursor:grab; z-index:1; user-select:none;
|
|
}
|
|
.flowNode-trigger{ border-color:var(--status-info); }
|
|
.flowNode-condition{ border-color:var(--status-warning); }
|
|
.flowNode-action{ border-color:var(--status-success); }
|
|
|
|
.flowNodeBody {
|
|
padding-right:16px;
|
|
line-height:1.4;
|
|
word-break:break-word;
|
|
}
|
|
|
|
.flowNodeDelete{
|
|
position:absolute; top:4px; right:4px; width:18px; height:18px; padding:0; line-height:1;
|
|
font-size:11px; border-radius:4px; background:transparent; border:none; color:var(--text-dim);
|
|
}
|
|
.flowNodeDelete:hover{ color:var(--danger); }
|
|
|
|
.flowPort{
|
|
position:absolute; width:14px; height:14px; border-radius:50%; background:var(--panel);
|
|
border:2px solid var(--accent); top:50%; transform:translateY(-50%); cursor:crosshair; z-index:2;
|
|
}
|
|
.flowPortIn{ left:-8px; }
|
|
.flowPortOut{ right:-8px; }
|
|
|
|
.flowPortTrue {
|
|
right:-8px;
|
|
top:35%;
|
|
border-color:var(--status-success);
|
|
}
|
|
|
|
.flowPortFalse {
|
|
right:-8px;
|
|
top:65%;
|
|
border-color:var(--danger);
|
|
}
|
|
|
|
/* ---------- Timeline d'animation : panneau sous "Logique de la scène" ---------- */
|
|
.animTimelineWrap{
|
|
position:relative; width:100%; flex:1 1 auto; min-height:120px; overflow:auto;
|
|
border:1px dashed var(--border); border-radius:8px; background:var(--bg);
|
|
}
|
|
|
|
.animRuler {
|
|
display:flex;
|
|
height:22px;
|
|
border-bottom:1px solid var(--border);
|
|
position:sticky;
|
|
top:0;
|
|
background:var(--bg);
|
|
z-index:3;
|
|
}
|
|
|
|
.animRulerSpacer {
|
|
position:sticky;
|
|
left:0;
|
|
width:140px;
|
|
flex:0 0 140px;
|
|
background:var(--panel);
|
|
z-index:2;
|
|
border-right:1px solid var(--border);
|
|
}
|
|
|
|
.animRulerTicksWrap {
|
|
position:relative;
|
|
flex:0 0 auto;
|
|
}
|
|
|
|
.animRulerTick{
|
|
position:absolute; top:0; bottom:0; font-size:10px; color:var(--text-dim);
|
|
border-left:1px solid var(--border); padding-left:3px;
|
|
}
|
|
.animTracks{ position:relative; }
|
|
|
|
.animTrack {
|
|
position:relative;
|
|
height:38px;
|
|
border-bottom:1px solid var(--border);
|
|
display:flex;
|
|
align-items:stretch;
|
|
}
|
|
|
|
.animTrackLabel{
|
|
position:sticky; left:0; width:140px; flex:0 0 140px; background:var(--panel); z-index:2;
|
|
padding:0 8px; font-size:12px; display:flex; align-items:center; border-right:1px solid var(--border);
|
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
}
|
|
|
|
.animTrackLane {
|
|
position:relative;
|
|
flex:0 0 auto;
|
|
}
|
|
|
|
.animClip{
|
|
position:absolute; top:5px; height:28px; border-radius:6px; background:var(--accent); color:#fff;
|
|
font-size:11px; padding:5px 8px; cursor:grab; overflow:hidden; white-space:nowrap; user-select:none;
|
|
box-sizing:border-box;
|
|
}
|
|
.animClip.selected{ outline:2px solid #fff; }
|
|
|
|
.animClipResizeHandle {
|
|
position:absolute;
|
|
right:0;
|
|
top:0;
|
|
width:8px;
|
|
height:100%;
|
|
cursor:ew-resize;
|
|
}
|
|
.animKeyframeRow{ align-items:flex-start; }
|
|
|
|
.animKeyframeRow textarea {
|
|
font-family:monospace;
|
|
font-size:11px;
|
|
}
|
|
|
|
/* ---------- Tableau de bord d'un jeu (game_dashboard.html) ---------- */
|
|
|
|
/* Une seule fenêtre, un onglet par catégorie (écrans/objets/éléments de
|
|
jeu/variables) — réutilise .builderTabs/.builderTabBtn/.builderTabPanel
|
|
(voir plus bas, éditeur d'écran) pour la même sensation de "fenêtre à
|
|
onglets" qu'ailleurs dans l'outil, plutôt que d'inventer un 2e système.
|
|
Contrairement à leur usage dans l'éditeur d'écran, ici la page défile
|
|
normalement (pas de hauteur figée) : une liste peut être longue, pas
|
|
besoin d'un défilement interne séparé par panneau. */
|
|
.dashTabCount {
|
|
font-size:10px;
|
|
color:var(--text-dim);
|
|
background:var(--panel2);
|
|
border-radius:999px;
|
|
padding:1px 6px;
|
|
margin-left:4px;
|
|
}
|
|
|
|
.dashPanelHeader {
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:space-between;
|
|
gap:10px;
|
|
margin-bottom:8px;
|
|
}
|
|
.dashPanelHeader h2{ margin:0; }
|
|
|
|
.dashCreateBar {
|
|
display:flex;
|
|
gap:8px;
|
|
align-items:center;
|
|
margin-bottom:12px;
|
|
padding:10px;
|
|
border:1px solid var(--border);
|
|
border-radius:8px;
|
|
background:var(--panel);
|
|
}
|
|
|
|
.dashCreateBar input, .dashCreateBar select {
|
|
width:auto;
|
|
flex:1 1 160px;
|
|
}
|
|
.dashDangerZone{ margin-top:28px; }
|
|
|
|
/* ---------- 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
|
|
masquer certains (voir toggleDataColumn() : agit directement sur
|
|
[data-col] plutôt que de reconstruire le tableau). */
|
|
.columnFilterWrap {
|
|
position:relative;
|
|
display:inline-block;
|
|
}
|
|
|
|
.columnFilterMenu{
|
|
position:absolute; top:100%; left:0; margin-top:4px; z-index:70;
|
|
background:var(--panel); border:1px solid var(--border); border-radius:8px;
|
|
padding:8px; box-shadow:0 12px 40px rgb(0 0 0 / 55%);
|
|
display:flex; flex-direction:column; gap:2px; min-width:180px; max-height:280px; overflow-y:auto;
|
|
}
|
|
.columnFilterMenu.hidden{ display:none; }
|
|
|
|
.columnFilterOption {
|
|
display:flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
padding:4px 6px;
|
|
border-radius:4px;
|
|
font-size:12.5px;
|
|
white-space:nowrap;
|
|
}
|
|
.columnFilterOption:hover{ background:var(--panel2); }
|
|
|
|
/* ---------- Panneau "Configurer la valeur" (variable Objet/Tableau) ---------- */
|
|
|
|
/* La colonne "Clé" n'a de sens que pour un Objet (paires clé/valeur) — un
|
|
Tableau n'est qu'une liste de valeurs, voir openVarValuePanel() dans
|
|
game_dashboard.html. */
|
|
#varValuePanel[data-mode="tableau"] .varValueKeyField{ display:none; }
|
|
|
|
/* ---------- Inscription : schéma visuel du mot de passe ---------- */
|
|
|
|
/* Jauge + liste de critères mise à jour en direct (voir register.html) —
|
|
les mêmes 5 critères que auth/password_strength.py côté serveur. */
|
|
.passwordStrengthBar {
|
|
height:6px;
|
|
border-radius:3px;
|
|
background:var(--panel2);
|
|
overflow:hidden;
|
|
margin-top:8px;
|
|
}
|
|
|
|
.passwordStrengthFill {
|
|
height:100%;
|
|
width:0;
|
|
border-radius:3px;
|
|
background:var(--status-danger);
|
|
transition:width .15s ease, background-color .15s ease;
|
|
}
|
|
.passwordStrengthFill.weak{ background:var(--status-danger); }
|
|
.passwordStrengthFill.medium{ background:var(--status-warning); }
|
|
.passwordStrengthFill.good{ background:var(--accent-2); }
|
|
.passwordStrengthFill.strong{ background:var(--status-success); }
|
|
|
|
.passwordChecklist {
|
|
list-style:none;
|
|
margin:8px 0 0;
|
|
padding:0;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:2px;
|
|
}
|
|
|
|
.passwordChecklist li {
|
|
font-size:12.5px;
|
|
color:var(--text-dim);
|
|
padding-left:18px;
|
|
position:relative;
|
|
}
|
|
|
|
.passwordChecklist li::before {
|
|
content:"○";
|
|
position:absolute;
|
|
left:0;
|
|
}
|
|
.passwordChecklist li.ok{ color:var(--status-success); }
|
|
.passwordChecklist li.ok::before{ content:"✓"; }
|
|
|
|
.passwordChecklist-compact {
|
|
flex-flow:row wrap;
|
|
gap:2px 10px;
|
|
}
|
|
.passwordChecklist-compact li{ font-size:11px; }
|
|
|
|
/* ---------- Page de profil : compacte, sur 2 colonnes ---------- */
|
|
.profileSection {
|
|
padding-top:1.5rem;
|
|
padding-bottom:1.5rem;
|
|
}
|
|
.profileContainer{ max-width:980px; }
|
|
|
|
.profileNotif {
|
|
padding:0.6rem 0.9rem;
|
|
margin-bottom:0.75rem;
|
|
}
|
|
|
|
.profileGrid {
|
|
display:grid;
|
|
grid-template-columns:1fr 1fr;
|
|
gap:14px;
|
|
align-items:start;
|
|
}
|
|
|
|
.profileCol {
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:14px;
|
|
min-width:0;
|
|
}
|
|
.profileBox{ padding:0.85rem 1rem; }
|
|
.profileBox-danger{ border:1px solid var(--status-danger); }
|
|
.profileForm .field{ margin-bottom:8px; }
|
|
.profileForm .field:last-child{ margin-bottom:0; }
|
|
|
|
.profileForm-row {
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:8px;
|
|
}
|
|
|
|
.profileForm-row .field {
|
|
flex:1 1 120px;
|
|
margin-bottom:0;
|
|
}
|
|
.profileForm-row .field.profileForm-full{ flex-basis:100%; }
|
|
|
|
@media (width <=720px){
|
|
.profileGrid{ grid-template-columns:1fr; }
|
|
}
|
|
|
|
/* ---------- Inscription : QR code de la double authentification ---------- */
|
|
.totpQrWrap {
|
|
background:#fff;
|
|
padding:16px;
|
|
border-radius:8px;
|
|
display:inline-block;
|
|
max-width:220px;
|
|
}
|
|
|
|
.totpQrWrap svg {
|
|
display:block;
|
|
width:100%;
|
|
height:auto;
|
|
}
|
|
|
|
.recoveryCodesGrid {
|
|
display:grid;
|
|
grid-template-columns:1fr 1fr;
|
|
gap:8px;
|
|
}
|
|
|
|
.recoveryCode {
|
|
display:block;
|
|
text-align:center;
|
|
padding:8px;
|
|
background:var(--panel2);
|
|
border-radius:10px;
|
|
letter-spacing:1px;
|
|
}
|