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
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>
785 lines
42 KiB
HTML
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>
|