Files
Forge-Engine/docs/plan/maquettes/editeur-formation-maquette.html
T
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

785 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Éditeur de support de formation — Forge</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0b0e14;
--bg-2:#11151d;
--card:#161b25;
--border:#232a38;
--text:#e8ecf4;
--muted:#8b95a7;
--accent1:#ff5f2e;
--accent2:#ffb020;
--gradient:linear-gradient(120deg,#ff5f2e,#ffb020);
--glow:0 0 0 1px rgba(255,95,46,0.25), 0 8px 24px -6px rgba(255,95,46,0.25);
}
:root[data-theme="light"]{
--bg:#F6F5F2;
--bg-2:#FFFFFF;
--card:#FFFFFF;
--border:#E3E1DB;
--text:#14161C;
--muted:#6B7280;
--glow:0 0 0 1px rgba(255,95,46,0.3), 0 8px 24px -6px rgba(255,95,46,0.3);
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;height:100%;}
body{
font-family:'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color:var(--text);
background:var(--bg);
height:100vh;
display:flex;
flex-direction:column;
overflow:hidden;
}
svg{ display:block; }
/* ===== LOGO ===== */
.forge-mark{ flex-shrink:0; }
/* ===== TOPBAR ===== */
.topbar{
height:56px; flex-shrink:0;
background:var(--bg-2); border-bottom:1px solid var(--border);
display:flex; align-items:center; justify-content:space-between;
padding:0 20px; gap:12px;
}
.brand{ display:flex; align-items:center; gap:10px; flex-shrink:0; }
.crumb{ display:flex; align-items:center; gap:8px; font-size:14px; font-weight:600; min-width:0; overflow:hidden; }
.crumb .dim{ color:var(--muted); font-weight:400; white-space:nowrap; }
.crumb .cur{ font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.topbar-actions{ display:flex; align-items:center; gap:10px; flex-shrink:0; }
.icon-btn{
width:34px; height:34px; border-radius:10px; border:1px solid var(--border);
background:transparent; display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.icon-btn:hover{ border-color:var(--muted); }
.theme-toggle svg{ color:var(--text); }
.divider-v{ width:1px; height:22px; background:var(--border); margin:0 4px; }
.forge-btn-secondary{
height:34px; padding:0 14px; border-radius:10px; border:1px solid var(--border);
background:transparent; color:var(--text); font-size:14px; font-weight:600;
display:flex; align-items:center; gap:7px; cursor:pointer;
}
.forge-btn-secondary:hover{ border-color:var(--muted); }
.forge-btn-secondary.primary{ border-color:var(--accent1); color:var(--accent1); }
.forge-btn-secondary.primary:hover{ background:rgba(255,95,46,0.08); }
/* ===== BODY LAYOUT ===== */
.body-wrap{ flex:1; display:flex; min-height:0; position:relative; }
/* ---- LEFT SIDEBAR ---- */
.sidebar-left{
width:248px; flex-shrink:0; background:var(--bg-2); border-right:1px solid var(--border);
padding:20px 16px; overflow-y:auto; display:flex; flex-direction:column; gap:22px;
}
.section-label{
font-size:12px; font-weight:700; letter-spacing:0.04em;
color:var(--muted); margin-bottom:10px;
}
.shapes-grid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.lib-item{
border:1px solid var(--border); border-radius:10px; cursor:pointer; background:var(--card);
transition:border-color .15s ease, box-shadow .15s ease;
}
.lib-item:hover{ border-color:var(--accent1); box-shadow:var(--glow); }
.lib-item.shape{ padding:10px 8px; display:flex; flex-direction:column; align-items:center; gap:6px; }
.lib-item.row{ padding:9px 10px; display:flex; align-items:center; gap:10px; }
.lib-item span{ font-size:12.5px; font-weight:600; }
.lib-list{ display:flex; flex-direction:column; gap:6px; }
/* ---- CENTER CANVAS ---- */
.canvas-area{
flex:1; background:var(--bg); padding:36px 40px; overflow-y:auto;
display:flex; justify-content:center; position:relative;
}
.doc-page{
width:760px; max-width:100%; height:fit-content; background:var(--card);
border:1px solid var(--border); border-radius:16px;
box-shadow:0 1px 3px rgba(0,0,0,0.3);
padding:48px 52px; display:flex; flex-direction:column; gap:18px;
}
.doc-block{
position:relative; border-radius:8px; padding:6px; margin:-6px;
cursor:pointer; transition:background .1s ease;
}
.doc-block:hover{ background:rgba(255,95,46,0.06); }
.doc-block.selected{ background:rgba(255,95,46,0.1); }
.handle{ position:absolute; width:8px; height:8px; border-radius:2px; background:var(--accent1); display:none; }
.doc-block.selected .handle{ display:block; }
.handle.tl{ top:-4px; left:-4px; } .handle.tr{ top:-4px; right:-4px; }
.handle.bl{ bottom:-4px; left:-4px; } .handle.br{ bottom:-4px; right:-4px; }
.text-content{ outline:none; }
.text-content[data-bold="1"]{ font-weight:800 !important; }
.text-content[data-italic="1"]{ font-style:italic; }
.text-content[data-underline="1"]{ text-decoration:underline; }
.image-block{
width:100%; height:190px; border-radius:10px; background:var(--bg-2);
border:1px dashed var(--border); display:flex; align-items:center; justify-content:center;
gap:8px; color:var(--muted);
}
.image-block span{ font-size:13px; font-weight:600; }
.cols-block{ display:flex; gap:24px; flex-wrap:wrap; }
.cols-block .col{ flex:1; min-width:220px; }
.steps{ display:flex; flex-direction:column; gap:8px; }
.step{ display:flex; align-items:baseline; gap:8px; font-size:14px; }
.step .num{ font-weight:800; color:var(--accent1); }
.sep{ height:1px; background:var(--border); margin:2px 0; }
.game-block{
border:1px solid var(--accent1); border-radius:12px; padding:18px 20px;
display:flex; align-items:center; justify-content:space-between; background:rgba(255,95,46,0.08);
transition:box-shadow .15s ease;
}
.game-block:hover{ box-shadow:var(--glow); }
.game-block .left{ display:flex; align-items:center; gap:12px; min-width:0; }
.game-icon{
width:36px; height:36px; border-radius:10px; background:var(--gradient);
display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.game-title{ font-size:15px; font-weight:700; }
.game-sub{ font-size:12.5px; color:var(--muted); }
.footer-note{ font-size:13.5px; color:var(--muted); }
.zoom-pill{
position:absolute; bottom:24px; left:50%; transform:translateX(-50%);
background:var(--card); border:1px solid var(--border); border-radius:999px;
height:36px; padding:0 6px; display:flex; align-items:center; gap:2px;
box-shadow:0 2px 6px rgba(0,0,0,0.3);
}
.zoom-pill button{
width:26px; height:26px; border-radius:999px; border:none; background:transparent;
display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.zoom-pill button:hover{ background:var(--bg-2); }
.zoom-pill span{ font-size:12.5px; font-weight:700; color:var(--muted); width:40px; text-align:center; }
/* ---- RIGHT SIDEBAR ---- */
.sidebar-right{
width:300px; flex-shrink:0; background:var(--bg-2); border-left:1px solid var(--border);
padding:20px 18px; overflow-y:auto; display:flex; flex-direction:column; gap:22px;
}
.field{ display:flex; flex-direction:column; gap:6px; }
.field-row{ display:flex; align-items:center; justify-content:space-between; }
.field label, .field-row .flabel{ font-size:13px; color:var(--muted); }
.field input, .field textarea{
border:1px solid var(--border); border-radius:8px; padding:8px 10px; font-size:13px;
color:var(--text); background:var(--card); font-family:'Inter', sans-serif; width:100%; resize:vertical;
}
.swatches{ display:flex; gap:8px; }
.swatch{ width:22px; height:22px; border-radius:50%; border:2px solid transparent; cursor:pointer; }
.swatch.active{ border-color:var(--text); }
.segmented{ display:flex; flex-wrap:wrap; gap:6px; }
.seg-btn{
padding:6px 10px; border-radius:8px; border:1px solid var(--border); background:var(--card);
font-size:12.5px; font-weight:600; color:var(--text); cursor:pointer;
}
.seg-btn.active{ background:rgba(255,95,46,0.12); color:var(--accent1); border-color:var(--accent1); }
.toolbar-row{ display:flex; gap:6px; }
.fmt-btn{
width:32px; height:32px; border-radius:8px; border:1px solid var(--border); background:var(--card);
display:flex; align-items:center; justify-content:center; cursor:pointer;
font-size:14px; font-weight:800; color:var(--text);
}
.fmt-btn.active{ background:rgba(255,95,46,0.12); border-color:var(--accent1); color:var(--accent1); }
.fmt-btn.italic{ font-style:italic; font-weight:400; }
.fmt-btn.underline{ text-decoration:underline; font-weight:400; }
.align-btn{
width:32px; height:32px; border-radius:8px; border:1px solid var(--border); background:var(--card);
display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.align-btn.active{ background:rgba(255,95,46,0.12); border-color:var(--accent1); }
.toggle{ width:38px; height:22px; border-radius:999px; background:var(--border); position:relative; cursor:pointer; flex-shrink:0; }
.toggle .knob{ width:16px; height:16px; border-radius:50%; background:var(--text); position:absolute; top:3px; left:3px; transition:left .15s ease; }
.toggle.on{ background:var(--accent1); }
.toggle.on .knob{ left:19px; background:#14100c; }
.trigger-row{
border:1px solid var(--border); border-radius:10px; padding:10px 12px; background:var(--card);
display:flex; flex-direction:column; gap:4px; margin-bottom:8px;
}
.trigger-event{ display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted); }
.trigger-action{ display:flex; align-items:center; gap:6px; font-size:13px; font-weight:600; padding-left:19px; }
.add-trigger{
width:100%; height:36px; border-radius:10px; border:1px dashed var(--border); background:transparent;
color:var(--muted); font-size:13px; font-weight:600; display:flex; align-items:center; justify-content:center;
gap:6px; cursor:pointer;
}
.add-trigger:hover{ border-color:var(--accent1); color:var(--accent1); }
.tags-title{ font-size:11.5px; color:var(--muted); margin-bottom:8px; }
.tags{ display:flex; flex-wrap:wrap; gap:6px; }
.tag{ font-size:11.5px; background:var(--card); border:1px solid var(--border); border-radius:999px; padding:4px 10px; color:var(--muted); }
.tag.hl{ background:rgba(255,95,46,0.1); border-color:rgba(255,95,46,0.35); color:var(--accent1); font-weight:600; }
.empty-state{ font-size:13px; color:var(--muted); text-align:center; padding:30px 10px; }
/* ===== BOTTOM NAV (mobile only) ===== */
.bottom-nav{
display:none; position:fixed; bottom:0; left:0; right:0; height:58px;
background:var(--bg-2); border-top:1px solid var(--border); z-index:40;
align-items:stretch; justify-content:space-around;
}
.bottom-nav button{
flex:1; background:transparent; border:none; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:3px; color:var(--muted); cursor:pointer; font-size:11px; font-weight:600;
}
.bottom-nav button.active{ color:var(--accent1); }
.sidebar-close{ display:none; }
/* ===== RESPONSIVE ===== */
@media (max-width:900px){
.crumb .dim{ display:none; }
.topbar{ padding:0 14px; }
.topbar-actions .forge-btn-secondary span.label{ display:none; }
.forge-btn-secondary{ padding:0 10px; }
.canvas-area{ padding:18px 14px 84px; }
.doc-page{ padding:28px 20px; }
.sidebar-left, .sidebar-right{
position:fixed; top:56px; bottom:58px; left:0; right:0; width:100%;
z-index:30; transition:transform .25s ease;
}
.sidebar-left{ transform:translateX(-100%); }
.sidebar-right{ transform:translateX(100%); }
.sidebar-left.open, .sidebar-right.open{ transform:translateX(0); }
.sidebar-close{
display:flex; align-items:center; gap:8px; font-size:13px; font-weight:600; color:var(--muted);
margin-bottom:16px; cursor:pointer;
}
.bottom-nav{ display:flex; }
}
</style>
</head>
<body>
<div class="topbar">
<div class="brand">
<svg class="forge-mark" width="30" height="30" viewBox="0 0 120 120">
<rect x="0" y="0" width="120" height="120" rx="24" fill="#161b25"/>
<polygon points="60,15 30,75 60,105" fill="#ff5f2e"/>
<polygon points="60,15 90,75 60,105" fill="#ffb020"/>
<path d="M46,43 L60,35 L74,43" stroke="#161b25" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38,59 L60,51 L82,59" stroke="#161b25" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M30,75 L60,67 L90,75" stroke="#161b25" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="crumb">
<span class="dim">Formation incendie</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
<span class="cur">Module 3 — Consignes d'évacuation</span>
</div>
<div class="topbar-actions">
<button class="icon-btn" aria-label="Annuler"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6L4 10l4 4"/><path d="M4 10h9a6 6 0 1 1-4.2 10.2"/></svg></button>
<button class="icon-btn" aria-label="Rétablir"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 6l4 4-4 4"/><path d="M20 10h-9a6 6 0 1 0 4.2 10.2"/></svg></button>
<div class="divider-v"></div>
<button class="forge-btn-secondary"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="3"/></svg><span class="label">Aperçu</span></button>
<button class="forge-btn-secondary primary"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 16V8M8.5 11.5L12 8l3.5 3.5"/></svg><span class="label">Publier</span></button>
<div class="divider-v"></div>
<button class="icon-btn theme-toggle" id="themeToggle" aria-label="Changer de thème"></button>
</div>
</div>
<div class="body-wrap">
<!-- SIDEBAR GAUCHE -->
<div class="sidebar-left" id="sidebarLeft">
<div class="sidebar-close" id="closeLeft">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>
Retour au document
</div>
<div>
<div class="section-label">Mise en page</div>
<div class="shapes-grid">
<div class="lib-item shape" data-add="rect"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/></svg><span>Rectangle</span></div>
<div class="lib-item shape" data-add="circle"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/></svg><span>Cercle</span></div>
<div class="lib-item shape" data-add="triangle"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 L20 19 L4 19 Z"/></svg><span>Triangle</span></div>
<div class="lib-item shape" data-add="line"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="12" x2="20" y2="12"/></svg><span>Trait</span></div>
</div>
</div>
<div>
<div class="section-label">Contenu</div>
<div class="lib-list">
<div class="lib-item row" data-add="titre"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5v14M4 12h6M10 5v14"/><path d="M15 10v9M19 10v9M15 14.2c0-2.2 4-2.2 4 0"/></svg><span>Titre</span></div>
<div class="lib-item row" data-add="paragraphe"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg><span>Paragraphe</span></div>
<div class="lib-item row" data-add="image"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M21 16.5l-5-5-4 4-3-3-5 5"/></svg><span>Image</span></div>
<div class="lib-item row" data-add="bouton"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="9" width="16" height="7" rx="3.5"/><circle cx="9" cy="12.5" r="0.9" fill="currentColor"/></svg><span>Bouton</span></div>
</div>
</div>
<div>
<div class="section-label">Mini-jeux</div>
<div class="lib-list">
<div class="lib-item row" data-add="quiz"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><text x="12" y="16.2" text-anchor="middle" font-size="11" fill="#ff5f2e" stroke="none" font-weight="800">?</text></svg><span>Quiz</span></div>
<div class="lib-item row" data-add="association"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.7" stroke-linecap="round"><circle cx="7" cy="7" r="3"/><circle cx="17" cy="17" r="3"/><path d="M9.3 9.3l5.4 5.4"/></svg><span>Association</span></div>
<div class="lib-item row" data-add="memory"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.7"><rect x="4" y="7" width="11" height="14" rx="2"/><rect x="9" y="3" width="11" height="14" rx="2"/></svg><span>Memory</span></div>
<div class="lib-item row" data-add="mots"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.5"><rect x="4" y="4" width="16" height="16"/><line x1="4" y1="10.5" x2="20" y2="10.5"/><line x1="4" y1="15.5" x2="20" y2="15.5"/><line x1="10.5" y1="4" x2="10.5" y2="20"/><line x1="15.5" y1="4" x2="15.5" y2="20"/></svg><span>Mots mêlés</span></div>
<div class="lib-item row" data-add="scenario"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.7" stroke-linecap="round"><circle cx="6" cy="5" r="1.8"/><circle cx="6" cy="19" r="1.8"/><circle cx="18" cy="12" r="1.8"/><path d="M6 7v10"/><path d="M6 12h10"/></svg><span>Scénario</span></div>
<div class="lib-item row" data-add="zones"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s7-7.5 7-12a7 7 0 1 0-14 0c0 4.5 7 12 7 12z"/><circle cx="12" cy="9" r="2.2"/></svg><span>Zones à risque</span></div>
</div>
</div>
</div>
<!-- CANVAS CENTRAL -->
<div class="canvas-area">
<div class="doc-page" id="docPage">
<div class="doc-block" data-type="titre" data-style="titre1" data-bold="0" data-italic="0" data-underline="0" data-align="left" data-color="var(--text)">
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="text-content" style="font-weight:800;font-size:clamp(1.6rem,4vw,2rem);line-height:1.15;text-align:left;color:var(--text);">Consignes d'évacuation</div>
</div>
<div class="doc-block" data-type="paragraphe" data-style="paragraphe" data-bold="0" data-italic="0" data-underline="0" data-align="left" data-color="var(--muted)">
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="text-content" style="font-weight:400;font-size:15px;line-height:1.5;text-align:left;color:var(--muted);">Ce module présente les étapes à suivre en cas de déclenchement de l'alarme incendie, ainsi que les bons réflexes à adopter avant l'arrivée des secours.</div>
</div>
<div class="doc-block" data-type="image" data-content="Image — plan d'évacuation de l'étage">
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="image-block">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M21 16.5l-5-5-4 4-3-3-5 5"/></svg>
<span>Image — plan d'évacuation de l'étage</span>
</div>
</div>
<div class="doc-block" data-type="paragraphe" data-style="paragraphe" data-bold="0" data-italic="0" data-underline="0" data-align="left" data-color="var(--text)">
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="cols-block">
<div class="col">
<div class="text-content" style="font-weight:400;font-size:14px;line-height:1.6;text-align:left;color:var(--text);">Dès le signal d'alarme, l'évacuation est immédiate. Aucune tâche en cours ne justifie un délai, même de quelques secondes.</div>
</div>
<div class="col">
<div class="steps">
<div class="step"><span class="num">1</span><span>Donner l'alerte</span></div>
<div class="step"><span class="num">2</span><span>Évacuer par l'issue la plus proche</span></div>
<div class="step"><span class="num">3</span><span>Rejoindre le point de rassemblement</span></div>
</div>
</div>
</div>
</div>
<div class="sep"></div>
<div class="doc-block" data-type="quiz" data-content="Élément — Quiz|5 questions · thème : réflexes d'alerte">
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="game-block">
<div class="left">
<div class="game-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#14100c" stroke-width="1.9"><circle cx="12" cy="12" r="9"/><text x="12" y="16.2" text-anchor="middle" font-size="11" fill="#14100c" stroke="none" font-weight="800">?</text></svg></div>
<div>
<div class="game-title">Élément — Quiz</div>
<div class="game-sub">5 questions · thème : réflexes d'alerte</div>
</div>
</div>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
</div>
</div>
<div class="footer-note">Section suivante : Utilisation des extincteurs</div>
</div>
<div class="zoom-pill">
<button aria-label="Zoom -"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<span id="zoomVal">100%</span>
<button aria-label="Zoom +"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
</div>
<!-- SIDEBAR DROITE -->
<div class="sidebar-right" id="sidebarRight">
<div class="sidebar-close" id="closeRight">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>
Retour au document
</div>
<div id="propsPanel">
<div class="section-label">Propriétés</div>
<div class="empty-state">Sélectionnez un élément du document pour voir ses propriétés.</div>
</div>
<div style="height:1px;background:var(--border);"></div>
<div>
<div class="section-label">Déclencheurs — Flow</div>
<div id="triggerList">
<div class="trigger-row">
<div class="trigger-event">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="3"/></svg>
À l'affichage du document
</div>
<div class="trigger-action">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
Remplir « texte_1 »
</div>
</div>
<div class="trigger-row">
<div class="trigger-event">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="2"><circle cx="12" cy="12" r="9"/><text x="12" y="16.2" text-anchor="middle" font-size="10" fill="#ff5f2e" stroke="none" font-weight="800">?</text></svg>
Quiz terminé
</div>
<div class="trigger-action">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
Aller à « Section 4 »
</div>
</div>
</div>
<button class="add-trigger" id="addTriggerBtn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Ajouter un déclencheur
</button>
<div style="margin-top:18px;padding-top:14px;border-top:1px solid var(--border);">
<div class="tags-title">Éléments détectés automatiquement</div>
<div class="tags" id="tagsList">
<span class="tag hl">titre_1</span>
<span class="tag">paragraphe_1</span>
<span class="tag">image_hero</span>
<span class="tag">quiz_reflexes</span>
</div>
</div>
</div>
</div>
</div>
<!-- BOTTOM NAV MOBILE -->
<div class="bottom-nav" id="bottomNav">
<button data-target="left">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="10" x2="20" y2="10"/></svg>
Éléments
</button>
<button data-target="doc" class="active">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="3" width="14" height="18" rx="2"/><line x1="8" y1="8" x2="16" y2="8"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Document
</button>
<button data-target="right">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M4 12h2M18 12h2M12 4v2M12 18v2M6.3 6.3l1.4 1.4M16.3 16.3l1.4 1.4M6.3 17.7l1.4-1.4M16.3 7.7l1.4-1.4"/></svg>
Propriétés
</button>
</div>
<script>
const docPage = document.getElementById('docPage');
const propsPanel = document.getElementById('propsPanel');
const sidebarLeft = document.getElementById('sidebarLeft');
const sidebarRight = document.getElementById('sidebarRight');
let selected = null;
let elementCounter = 4;
const STYLE_PRESETS = {
titre1: { label:'Titre 1', fontSize:'clamp(1.6rem,4vw,2rem)', fontWeight:800, lineHeight:1.15 },
titre2: { label:'Titre 2', fontSize:'1.3rem', fontWeight:800, lineHeight:1.25 },
paragraphe: { label:'Paragraphe', fontSize:'15px', fontWeight:400, lineHeight:1.5 },
legende: { label:'Légende', fontSize:'12.5px', fontWeight:600, lineHeight:1.4 }
};
function applyTextStyles(block){
const content = block.querySelector('.text-content');
if(!content) return;
const preset = STYLE_PRESETS[block.dataset.style] || STYLE_PRESETS.paragraphe;
content.style.fontWeight = block.dataset.bold === '1' ? '800' : String(preset.fontWeight);
content.style.fontSize = preset.fontSize;
content.style.lineHeight = preset.lineHeight;
content.style.textAlign = block.dataset.align;
content.style.color = block.dataset.color;
}
function closeMobilePanels(){
sidebarLeft.classList.remove('open');
sidebarRight.classList.remove('open');
document.querySelectorAll('.bottom-nav button').forEach(b => b.classList.remove('active'));
document.querySelector('.bottom-nav button[data-target="doc"]').classList.add('active');
}
function renderProps(block){
const type = block.dataset.type;
let html = '';
if(type === 'titre' || type === 'paragraphe'){
const content = block.querySelector('.text-content').textContent;
const style = block.dataset.style;
const bold = block.dataset.bold === '1';
const italic = block.dataset.italic === '1';
const underline = block.dataset.underline === '1';
const align = block.dataset.align;
const color = block.dataset.color;
html += `<div class="section-label">Propriétés — texte</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div class="field">
<span class="flabel">Style</span>
<div class="segmented" id="styleSeg">
${Object.entries(STYLE_PRESETS).map(([key, p]) =>
`<button class="seg-btn ${style===key?'active':''}" data-style="${key}">${p.label}</button>`
).join('')}
</div>
</div>
<div class="field"><label>Contenu</label><textarea id="contentInput" rows="3">${content}</textarea></div>
<div class="field">
<span class="flabel">Mise en forme</span>
<div class="toolbar-row">
<button class="fmt-btn ${bold?'active':''}" id="boldBtn" title="Gras">B</button>
<button class="fmt-btn italic ${italic?'active':''}" id="italicBtn" title="Italique">I</button>
<button class="fmt-btn underline ${underline?'active':''}" id="underlineBtn" title="Souligné">U</button>
</div>
</div>
<div class="field">
<span class="flabel">Alignement</span>
<div class="toolbar-row">
<button class="align-btn ${align==='left'?'active':''}" data-align="left" title="Gauche"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="14" y2="12"/><line x1="4" y1="18" x2="18" y2="18"/></svg></button>
<button class="align-btn ${align==='center'?'active':''}" data-align="center" title="Centre"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="5" y1="18" x2="19" y2="18"/></svg></button>
<button class="align-btn ${align==='right'?'active':''}" data-align="right" title="Droite"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/><line x1="6" y1="18" x2="20" y2="18"/></svg></button>
</div>
</div>
<div class="field">
<span class="flabel">Couleur du texte</span>
<div class="swatches" id="colorSwatches">
<div class="swatch ${color==='var(--text)'?'active':''}" data-color="var(--text)" style="background:var(--text);"></div>
<div class="swatch ${color==='var(--muted)'?'active':''}" data-color="var(--muted)" style="background:var(--muted);"></div>
<div class="swatch ${color==='#ff5f2e'?'active':''}" data-color="#ff5f2e" style="background:#ff5f2e;"></div>
<div class="swatch ${color==='#ffb020'?'active':''}" data-color="#ffb020" style="background:#ffb020;"></div>
</div>
</div>
</div>`;
propsPanel.innerHTML = html;
document.getElementById('contentInput').addEventListener('input', (e) => {
block.querySelector('.text-content').textContent = e.target.value;
});
document.querySelectorAll('#styleSeg .seg-btn').forEach(btn => {
btn.addEventListener('click', () => {
block.dataset.style = btn.dataset.style;
block.dataset.type = (btn.dataset.style === 'titre1' || btn.dataset.style === 'titre2') ? 'titre' : 'paragraphe';
applyTextStyles(block);
renderProps(block);
});
});
document.getElementById('boldBtn').addEventListener('click', () => {
block.dataset.bold = block.dataset.bold === '1' ? '0' : '1';
applyTextStyles(block); renderProps(block);
});
document.getElementById('italicBtn').addEventListener('click', () => {
block.dataset.italic = block.dataset.italic === '1' ? '0' : '1';
applyTextStyles(block); renderProps(block);
});
document.getElementById('underlineBtn').addEventListener('click', () => {
block.dataset.underline = block.dataset.underline === '1' ? '0' : '1';
applyTextStyles(block); renderProps(block);
});
propsPanel.querySelectorAll('.align-btn').forEach(btn => {
btn.addEventListener('click', () => {
block.dataset.align = btn.dataset.align;
applyTextStyles(block); renderProps(block);
});
});
propsPanel.querySelectorAll('#colorSwatches .swatch').forEach(sw => {
sw.addEventListener('click', () => {
block.dataset.color = sw.dataset.color;
applyTextStyles(block); renderProps(block);
});
});
} else if(type === 'image'){
const label = block.dataset.content;
html = `<div class="section-label">Propriétés — image</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div class="field-row"><span class="flabel">Type</span><span style="font-size:13px;font-weight:700;">Image</span></div>
<div class="field"><label>Texte alternatif</label><input type="text" value="${label}"></div>
<button class="forge-btn-secondary" style="width:100%;justify-content:center;height:38px;">Remplacer l'image</button>
</div>`;
propsPanel.innerHTML = html;
} else if(type === 'quiz'){
html = `<div class="section-label">Propriétés — quiz</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div class="field-row"><span class="flabel">Type</span><span style="font-size:13px;font-weight:700;">Quiz</span></div>
<div class="field"><label>Nombre de questions</label><input type="text" value="5"></div>
<div class="field-row">
<label class="flabel">Chronomètre</label>
<div class="toggle" id="chronoToggle"><div class="knob"></div></div>
</div>
<div class="field"><span class="flabel">Thème visuel</span><div class="swatches">
<div class="swatch active" style="background:#ff5f2e;"></div>
<div class="swatch" style="background:#ffb020;"></div>
<div class="swatch" style="background:var(--muted);"></div>
<div class="swatch" style="background:var(--text);"></div>
</div></div>
</div>`;
propsPanel.innerHTML = html;
document.getElementById('chronoToggle').addEventListener('click', (e) => e.currentTarget.classList.toggle('on'));
propsPanel.querySelectorAll('.swatch').forEach(sw => {
sw.addEventListener('click', () => {
sw.parentElement.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
sw.classList.add('active');
});
});
} else {
html = `<div class="section-label">Propriétés</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div class="field-row"><span class="flabel">Type</span><span style="font-size:13px;font-weight:700;text-transform:capitalize;">${type}</span></div>
<div style="font-size:12.5px;color:var(--muted);">Élément ajouté depuis la bibliothèque — propriétés à définir.</div>
</div>`;
propsPanel.innerHTML = html;
}
}
function selectBlock(block){
if(selected) selected.classList.remove('selected');
selected = block;
block.classList.add('selected');
renderProps(block);
if(window.innerWidth <= 900){
sidebarLeft.classList.remove('open');
sidebarRight.classList.add('open');
document.querySelectorAll('.bottom-nav button').forEach(b => b.classList.remove('active'));
document.querySelector('.bottom-nav button[data-target="right"]').classList.add('active');
}
}
function attachSelect(block){
block.addEventListener('click', (e) => { e.stopPropagation(); selectBlock(block); });
}
docPage.querySelectorAll('.doc-block').forEach(attachSelect);
document.querySelectorAll('.lib-item[data-add]').forEach(item => {
item.addEventListener('click', () => {
const type = item.dataset.add;
const label = item.querySelector('span').textContent;
elementCounter++;
const block = document.createElement('div');
block.className = 'doc-block';
if(type === 'titre' || type === 'paragraphe'){
block.dataset.type = type;
block.dataset.style = type === 'titre' ? 'titre2' : 'paragraphe';
block.dataset.bold = '0'; block.dataset.italic = '0'; block.dataset.underline = '0';
block.dataset.align = 'left'; block.dataset.color = 'var(--text)';
const defaultText = type === 'titre' ? 'Nouveau titre' : 'Nouveau paragraphe de texte.';
block.innerHTML = `<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div class="text-content">${defaultText}</div>`;
docPage.insertBefore(block, docPage.querySelector('.footer-note'));
applyTextStyles(block);
} else {
block.dataset.type = type;
block.dataset.content = `Nouvel élément — ${label}`;
block.innerHTML = `
<span class="handle tl"></span><span class="handle tr"></span><span class="handle bl"></span><span class="handle br"></span>
<div style="border:1px dashed var(--border); border-radius:10px; padding:14px 16px; display:flex; align-items:center; gap:10px; color:var(--muted); font-size:13px;">
${item.querySelector('svg').outerHTML}
<span>Nouvel élément — ${label}</span>
</div>`;
docPage.insertBefore(block, docPage.querySelector('.footer-note'));
}
attachSelect(block);
selectBlock(block);
block.scrollIntoView({behavior:'smooth', block:'center'});
const tagsList = document.getElementById('tagsList');
const tag = document.createElement('span');
tag.className = 'tag';
tag.textContent = `${type}_${elementCounter}`;
tagsList.appendChild(tag);
});
});
document.getElementById('addTriggerBtn').addEventListener('click', () => {
const list = document.getElementById('triggerList');
const row = document.createElement('div');
row.className = 'trigger-row';
row.innerHTML = `
<div class="trigger-event">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#ff5f2e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/></svg>
Nouveau déclencheur
</div>
<div class="trigger-action">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
À configurer
</div>`;
list.appendChild(row);
});
document.querySelector('.canvas-area').addEventListener('click', (e) => {
if(e.target.closest('.doc-block')) return;
if(selected){
selected.classList.remove('selected');
selected = null;
propsPanel.innerHTML = '<div class="section-label">Propriétés</div><div class="empty-state">Sélectionnez un élément du document pour voir ses propriétés.</div>';
}
});
let zoom = 100;
document.querySelectorAll('.zoom-pill button').forEach((btn, i) => {
btn.addEventListener('click', () => {
zoom += (i === 0 ? -10 : 10);
zoom = Math.max(50, Math.min(150, zoom));
document.getElementById('zoomVal').textContent = zoom + '%';
docPage.style.transform = `scale(${zoom/100})`;
docPage.style.transformOrigin = 'top center';
});
});
/* ---- navigation mobile ---- */
document.querySelectorAll('.bottom-nav button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.bottom-nav button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const target = btn.dataset.target;
sidebarLeft.classList.toggle('open', target === 'left');
sidebarRight.classList.toggle('open', target === 'right');
});
});
document.getElementById('closeLeft').addEventListener('click', closeMobilePanels);
document.getElementById('closeRight').addEventListener('click', closeMobilePanels);
selectBlock(docPage.querySelector('[data-type="titre"]'));
if(window.innerWidth <= 900){ closeMobilePanels(); }
/* ---- thème clair / sombre ---- */
const SUN_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2 12h2M20 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>';
const MOON_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/></svg>';
const themeToggle = document.getElementById('themeToggle');
function applyTheme(theme){
document.documentElement.setAttribute('data-theme', theme);
themeToggle.innerHTML = theme === 'light' ? MOON_ICON : SUN_ICON;
themeToggle.setAttribute('aria-label', theme === 'light' ? 'Passer en thème sombre' : 'Passer en thème clair');
}
let storedTheme = 'dark';
try {
const saved = localStorage.getItem('forge-editor-theme');
if(saved === 'light' || saved === 'dark') storedTheme = saved;
} catch(e) { /* stockage indisponible, on reste sur le thème par défaut */ }
applyTheme(storedTheme);
themeToggle.addEventListener('click', () => {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
applyTheme(next);
try { localStorage.setItem('forge-editor-theme', next); } catch(e) { /* ignore */ }
});
</script>
</body>
</html>