Spaces:
Build error
Build error
| <html lang="fr" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mailix | Éditeur de Code - Pro</title> | |
| <link rel="icon" type="image/png" href="https://i.imgur.com/7Gn3toV.png"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" | |
| href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/xml/xml.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/css/css.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/htmlmixed/htmlmixed.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/php/php.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/sql/sql.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/markdown/markdown.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/shell/shell.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/typescript/typescript.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/go/go.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/yaml/yaml.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/rust/rust.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/stex/stex.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/meta.js"></script> </head> | |
| <style> | |
| /* Configuration de la police Inter */ | |
| html, body { | |
| font-family: 'Inter', sans-serif; | |
| height: 100vh; /* Utiliser 100vh pour garantir la hauteur de la fenêtre */ | |
| overflow: hidden; /* Supprimer le défilement de l'écran principal */ | |
| } | |
| /* Transition pour un effet doux des couleurs */ | |
| .transition-colors-theme { transition: background-color 0.3s, color 0.3s, border-color 0.3s; } | |
| /* Définir les Variables de Thème - UNIQUEMENT Mode Sombre (Défaut) */ | |
| :root, .dark { | |
| --bg-page: #121212; | |
| --bg-card: #1E1E1E; | |
| --bg-sidebar: #1E1E1E; | |
| --bg-sidebar-active: #2C2C2C; | |
| --border-sidebar: #383838; | |
| --text-color: #FFFFFF; | |
| --text-secondary: #D1D5DB; | |
| --text-link: #3B82F6; | |
| --text-link-hover: #60A5FA; | |
| --input-bg: #2C2C2C; | |
| --input-border: #4B5563; | |
| --input-text-key: #6EE7B7; | |
| --primary-color: #3B82F6; | |
| --primary-hover: #2563EB; | |
| --warning-bg: #451A03; | |
| --warning-border: #78350F; | |
| --warning-text: #FBBF24; | |
| --editor-tab-bg: #1E1E1E; /* Fond des onglets de l'éditeur */ | |
| --editor-tab-active-bg: #2C2C2C; /* Fond de l'onglet actif */ | |
| --status-bar-bg: #007ACC; /* Bleu VS Code pour la barre de statut */ | |
| } | |
| /* Mode Clair (Supprimé selon le plan) */ | |
| /* Appliquer les variables au body et aux éléments (Conservées) */ | |
| .bg-page-var { background-color: var(--bg-page); } | |
| .bg-card-var { background-color: var(--bg-card); } | |
| .bg-sidebar-var { background-color: var(--bg-sidebar); } | |
| .border-sidebar-var { border-color: var(--border-sidebar); } | |
| .text-color-var { color: var(--text-color); } | |
| .text-secondary-var { color: var(--text-secondary); } | |
| .text-link-var { color: var(--text-link); } | |
| .hover\:text-link-hover-var:hover { color: var(--text-link-hover); } | |
| .bg-input-var { background-color: var(--input-bg); } | |
| .border-input-var { border-color: var(--input-border); } | |
| .text-input-key-var { color: var(--input-text-key); } | |
| .bg-primary-color-var { background-color: var(--primary-color); } | |
| .hover\:bg-primary-hover-var:hover { background-color: var(--primary-hover); } | |
| .focus\:ring-primary-color-var:focus { --tw-ring-color: var(--primary-color); } | |
| .focus\:border-primary-color-var:focus { border-color: var(--primary-color); } | |
| .bg-sidebar-active-var { background-color: var(--bg-sidebar-active); } | |
| .bg-warning-var { background-color: var(--warning-bg); } | |
| .border-warning-var { border-color: var(--warning-border); } | |
| .text-warning-var { color: var(--warning-text); } | |
| /* Styles pour les sidebars - FIXES */ | |
| .sidebar { | |
| transition: transform 0.3s ease-in-out; | |
| position: fixed; | |
| top: 0; | |
| z-index: 30; | |
| height: 100vh; | |
| } | |
| /* Sidebar Gauche - Masquée par défaut sur toutes les tailles */ | |
| .sidebar-left { | |
| transform: translateX(-100%); | |
| left: 0; | |
| } | |
| .sidebar-left.open { | |
| transform: translateX(0); | |
| } | |
| /* FIX 1.1: Sidebar Droite - Masquée par défaut, positionnée à droite */ | |
| .sidebar-right { | |
| transform: translateX(100%); | |
| right: 0; | |
| } | |
| .sidebar-right.open { | |
| transform: translateX(0); | |
| } | |
| /* Conteneur principal pour décaler le contenu sur les grands écrans (lg) */ | |
| @media (min-width: 1024px) { | |
| .main-content { | |
| margin-left: 0; | |
| margin-right: 0; | |
| } | |
| /* Décalage si sidebar gauche ouverte */ | |
| .main-content.sidebar-left-open { | |
| margin-left: 256px; | |
| } | |
| /* Décalage si sidebar droite ouverte */ | |
| .main-content.sidebar-right-open { | |
| margin-right: 256px; | |
| } | |
| /* Décalage si les deux sidebars sont ouvertes */ | |
| .main-content.sidebar-left-open.sidebar-right-open { | |
| margin-left: 256px; | |
| margin-right: 256px; | |
| } | |
| } | |
| /* Ajustement du bouton de Thème (Supprimé) */ | |
| .theme-switch { | |
| display: none; | |
| } | |
| /* Styles Spécifiques à l'Éditeur */ | |
| .editor-container { | |
| /* Maintient la disposition en colonne pour header, tabs, editor, footer */ | |
| display: flex; | |
| flex-direction: column; | |
| /* Le conteneur s'étire maintenant grâce au body flex-column */ | |
| height: 100%; | |
| flex-grow: 1; /* Permet au main-content de prendre l'espace restant dans la ligne flex body */ | |
| padding-top: 0; | |
| } | |
| /* FIX 3: Réduction du padding du header */ | |
| #main-header { | |
| padding-top: 0.5rem; /* p-2 */ | |
| padding-bottom: 0.5rem; /* p-2 */ | |
| padding-left: 1rem; /* px-4 */ | |
| padding-right: 1rem; /* px-4 */ | |
| } | |
| /* FIX 2.2: Le wrapper de l'éditeur prend l'espace restant dans la colonne flex */ | |
| #code-editor-wrapper { | |
| flex-grow: 1; | |
| overflow: hidden; /* Empêche le CodeMirror de faire défiler le parent */ | |
| } | |
| /* CodeMirror Styling - Pour un look sombre VS Code (Monokai) */ | |
| .CodeMirror { | |
| flex-grow: 1; | |
| height: 100% ; /* Essentiel pour que le défilement CodeMirror fonctionne */ | |
| font-size: 14px; | |
| line-height: 1.5; | |
| background: #272822; /* Fond Monokai */ | |
| color: #f8f8f2; | |
| border-top: 1px solid var(--border-sidebar); | |
| border-bottom: 1px solid var(--border-sidebar); | |
| } | |
| /* Style de la barre d'onglets */ | |
| #tab-bar { | |
| background-color: var(--editor-tab-bg); | |
| border-bottom: 1px solid var(--border-sidebar); | |
| min-height: 40px; | |
| padding-left: 10px; | |
| /* MODIFICATION 2.1 - 2.3: Permet le défilement horizontal */ | |
| overflow-x: auto; | |
| white-space: nowrap; /* Empêche le wrapping des onglets */ | |
| } | |
| .tab-item { | |
| /* MODIFICATION 2.2: S'assure que les onglets ne se rétrécissent pas pour le défilement */ | |
| flex-shrink: 0; | |
| display: inline-flex; /* Utilisation de inline-flex au lieu de flex pour un meilleur comportement avec white-space: nowrap */ | |
| align-items: center; | |
| padding: 8px 15px; | |
| border-right: 1px solid var(--border-sidebar); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| transition: background-color 0.2s; | |
| } | |
| .tab-item.active { | |
| background-color: var(--editor-tab-active-bg); | |
| color: var(--text-color); | |
| position: relative; | |
| } | |
| .tab-item:hover:not(.active) { | |
| background-color: var(--bg-sidebar-active); | |
| } | |
| .tab-close { | |
| margin-left: 8px; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| } | |
| /* Style de la barre de statut */ | |
| #status-bar { | |
| background-color: var(--status-bar-bg); | |
| color: white; | |
| min-height: 25px; | |
| font-size: 0.75rem; | |
| } | |
| /* Modale */ | |
| .modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 50; | |
| } | |
| .modal.open { | |
| display: flex; | |
| } | |
| .modal-content { | |
| padding: 1.5rem; | |
| border-radius: 0.5rem; | |
| width: 90%; | |
| max-width: 400px; | |
| } | |
| .input-text { | |
| border-width: 1px; | |
| padding: 0.75rem; | |
| border-radius: 0.375rem; | |
| width: 100%; | |
| font-size: 1rem; | |
| } | |
| /* Masquer la marge du main-content sur mobile pour l'éditeur */ | |
| @media (max-width: 1023px) { | |
| .main-content { | |
| padding: 0; | |
| } | |
| .editor-container { | |
| /* Calcule la hauteur totale - (Header + barre de statut + barre d'onglets) pour l'éditeur */ | |
| /* Cette règle est moins critique si flex-grow est appliqué correctement */ | |
| height: calc(100vh - 40px - 40px - 25px); /* Environ */ | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-page-var text-color-var min-h-screen transition-colors-theme flex flex-col"> | |
| <aside id="sidebar-left" class="sidebar sidebar-left w-64 bg-sidebar-var border-r border-sidebar-var p-6 flex flex-col transition-colors-theme"> | |
| <div class="flex items-center mb-6 shrink-0"> | |
| <img src="https://i.imgur.com/7Gn3toV.png" alt="Nexus Pro Logo" class="h-8"> | |
| <span class="text-xl font-bold text-color-var ml-2 transition-colors-theme">Mailix</span> | |
| </div> | |
| <div class="flex-grow overflow-y-auto pr-2" id="scrollable-nav-container"> | |
| <h3 class="text-xs font-semibold uppercase tracking-wider text-secondary-var mb-2">Fichiers Ouverts</h3> | |
| <nav id="file-explorer" class="space-y-1 pb-4 border-b border-sidebar-var/50 mb-4"> | |
| </nav> | |
| <h3 class="text-xs font-semibold uppercase tracking-wider text-secondary-var mb-2">Navigation</h3> | |
| <nav class="space-y-3 pb-4"> | |
| <a href="{{ url_for('user_bp.dashboard') }}" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">dashboard</span> | |
| <span class="ml-3">Dashboard</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.profile') }}" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">person</span> | |
| <span class="ml-3">Profil</span> | |
| </a> | |
| <a href="/parametres" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">settings</span> <span class="ml-3">Paramètres</span> | |
| </a> | |
| <a href="/aide" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">help</span> | |
| <span class="ml-3">Aide & Support</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.profile') }}" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">person</span> | |
| <span class="ml-3">Profil</span> | |
| </a> | |
| <a href="/tarifs" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">payments</span> | |
| <span class="ml-3">Tarifs</span> | |
| </a> | |
| <a href="/statut" class="flex items-center p-3 bg-sidebar-active-var text-color-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">monitor_heart</span> | |
| <span class="ml-3">Statut de l'API</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.deconnexion') }}" id="logout-button-sidebar" class="flex items-center p-3 text-red-500 hover:text-red-400 hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">deconnexion</span> | |
| <span class="ml-3">Déconnexion</span> | |
| </a> | |
| </nav> | |
| </div> | |
| <div class="mt-auto pt-4 text-xs text-secondary-var transition-colors-theme border-t border-sidebar-var/50 shrink-0"> | |
| <p>Connecté en tant que: <span class="font-semibold text-color-var transition-colors-theme">{{ user.email if user and user.email else 'Chargement...' }}</span></p> | |
| <p>Plan: <span class="font-semibold text-color-var transition-colors-theme">{{ (user.plan | upper) if user and user.plan else 'GRATUIT' }}</span></p> | |
| </div> | |
| </aside> | |
| <aside id="sidebar-right" class="sidebar sidebar-right w-64 bg-sidebar-var border-l border-sidebar-var p-4 flex flex-col transition-colors-theme overflow-y-auto"> | |
| <div class="flex items-center mb-4 shrink-0 border-b border-sidebar-var/50 pb-2"> | |
| <span class="material-symbols-rounded text-xl mr-2">settings</span> | |
| <span class="text-lg font-semibold text-color-var">Outils & Paramètres</span> | |
| </div> | |
| <div id="editor-settings-section" class="mb-6"> | |
| <h4 class="flex items-center text-sm font-bold uppercase tracking-wider text-secondary-var mb-2"> | |
| <span class="material-symbols-rounded text-base mr-1">settings</span> | |
| Paramètres de l'Éditeur | |
| </h4> | |
| <div class="space-y-3 p-2 bg-sidebar-active-var rounded-lg"> | |
| <div> | |
| <label class="block text-xs font-medium text-secondary-var mb-1">Thème du Code</label> | |
| <select class="w-full bg-input-var text-color-var p-2 rounded-md border border-input-var text-sm"> | |
| <option selected>Monokai (Défaut)</option> | |
| <option>VS Light</option> | |
| <option>Dracula</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-secondary-var mb-1">Taille de Police</label> | |
| <input type="range" min="10" max="20" value="14" class="w-full h-1 bg-primary-color-var rounded-lg appearance-none cursor-pointer"> | |
| <p class="text-xs text-secondary-var mt-1">Taille actuelle: 14px</p> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-secondary-var mb-1">Indentation</label> | |
| <select class="w-full bg-input-var text-color-var p-2 rounded-md border border-input-var text-sm"> | |
| <option selected>Espaces (4)</option> | |
| <option>Espaces (2)</option> | |
| <option>Tabulations</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="minimap-section" class="mb-6"> | |
| <h4 class="flex items-center text-sm font-bold uppercase tracking-wider text-secondary-var mb-2"> | |
| <span class="material-symbols-rounded text-base mr-1">map</span> | |
| Aperçu du Code (Minimap) | |
| </h4> | |
| <div class="h-20 bg-input-var border border-input-border rounded-lg flex items-center justify-center text-xs text-secondary-var/70"> | |
| [Zone de Minimap Simulé] | |
| </div> | |
| </div> | |
| <div id="inspection-section" class="flex-grow"> | |
| <h4 class="flex items-center text-sm font-bold uppercase tracking-wider text-secondary-var mb-2"> | |
| <span class="material-symbols-rounded text-base mr-1">search</span> | |
| Inspection/Propriétés | |
| </h4> | |
| <div class="p-3 bg-sidebar-active-var rounded-lg h-full overflow-y-auto text-xs space-y-2"> | |
| <p class="font-semibold text-color-var">Élément sélectionné: <body></p> | |
| <p><span class="text-input-key-var">background-color:</span> var(--bg-page);</p> | |
| <p><span class="text-input-key-var">color:</span> var(--text-color);</p> | |
| <p class="text-secondary-var mt-4">[Aide Contextuelle pour la balise HTML 'body']</p> | |
| </div> | |
| </div> | |
| </aside> | |
| <div id="main-content" class="main-content flex-grow transition-colors-theme editor-container"> | |
| <header id="main-header" class="flex justify-between items-center bg-page-var p-2 border-b border-sidebar-var shrink-0"> | |
| <div class="flex items-center"> | |
| <button id="sidebar-left-toggle" class="mobile-toggle-button p-2 rounded-lg bg-sidebar-active-var hover:bg-sidebar-active-var/80 transition-colors-theme mr-3" title="Afficher/Masquer le menu latéral"> | |
| <span class="material-symbols-rounded text-color-var">menu</span> | |
| </button> | |
| <a href="/" class="flex items-center"> <img src="https://i.imgur.com/7Gn3toV.png" alt="Nexus Pro Logo" class="h-8"> | |
| <span class="text-xl font-bold text-color-var ml-2 transition-colors-theme hidden sm:inline">Mailix Editor</span> | |
| </a> | |
| </div> | |
| <div class="flex items-center"> | |
| <a href="/html-run" | |
| class="flex items-center p-2 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme mr-2" | |
| title="Lancer le Lanceur HTML"> | |
| <span class="material-symbols-rounded">play_arrow</span> | |
| <span class="ml-3 hidden sm:inline">Run</span> | |
| </a> | |
| <button id="sidebar-right-toggle" class="mobile-toggle-button p-2 rounded-lg bg-sidebar-active-var hover:bg-sidebar-active-var/80 transition-colors-theme" title="Outils et Paramètres"> | |
| <span class="material-symbols-rounded text-color-var">more_vert</span> | |
| </button> | |
| </div> | |
| </header> | |
| <div id="tab-bar" class="flex items-center overflow-x-auto shrink-0"> | |
| <button id="add-tab-button" class="flex items-center p-2 text-secondary-var hover:text-color-var transition-colors-theme" title="Ajouter un nouveau fichier"> | |
| <span class="material-symbols-rounded text-xl">add</span> | |
| </button> | |
| <button id="import-button" class="flex items-center p-2 text-secondary-var hover:text-color-var transition-colors-theme" title="Importer un fichier local"> | |
| <span class="material-symbols-rounded text-xl">file_upload</span> | |
| </button> | |
| <input type="file" id="file-input" class="hidden" /> | |
| <button id="save-button" class="ml-auto flex items-center p-2 text-link-var hover:text-link-hover-var transition-colors-theme font-semibold border-l border-sidebar-var" title="Sauvegarder localement"> | |
| <span class="material-symbols-rounded text-xl mr-1">save</span> | |
| Sauvegarder | |
| </button> | |
| </div> | |
| <div id="code-editor-wrapper"> | |
| <textarea id="code-editor-textarea" style="display: none;"> | |
| <!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Mon Nouveau Projet</title> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body> | |
| <h1>Bienvenue dans l'Éditeur Mailix</h1> | |
| <p>Ce code est mis en évidence par CodeMirror.</p> | |
| </body> | |
| </html> | |
| </textarea> | |
| </div> | |
| <footer id="status-bar" class="flex items-center justify-between px-4 py-0.5 shrink-0"> | |
| <div class="flex items-center space-x-4"> | |
| <span class="material-symbols-rounded text-sm">code</span> | |
| <span id="status-language">Langage: HTML</span> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span id="status-encoding">UTF-8</span> | |
| <span id="status-line-col">Ligne: 1, Col: 1</span> | |
| </div> | |
| </footer> | |
| </div> | |
| <div id="create-file-modal" class="modal"> | |
| <div class="modal-content bg-card-var text-color-var"> | |
| <h2 class="text-xl font-bold mb-4">Créer un Nouveau Fichier</h2> | |
| <input type="text" id="new-file-name-input" class="input-text bg-input-var border-input-var text-color-var focus:border-primary-color-var focus:ring-1 focus:ring-primary-color-var outline-none mb-4" placeholder="Nom du fichier (ex: index.html, style.css, script.js)" required> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="cancel-create-file" class="px-4 py-2 text-secondary-var hover:text-color-var rounded-lg transition-colors-theme">Annuler</button> | |
| <button id="confirm-create-file" class="px-4 py-2 bg-primary-color-var hover:bg-primary-hover-var text-white font-semibold rounded-lg transition-colors-theme">Créer</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- PARTIE 1: GESTION DE LA SIDEBAR (Gauche et Droite) --- | |
| // Éléments de la Sidebar Gauche | |
| const sidebarLeftToggle = document.getElementById('sidebar-left-toggle'); | |
| const sidebarLeft = document.getElementById('sidebar-left'); // ID mis à jour | |
| const mainContent = document.getElementById('main-content'); | |
| // Éléments de la Sidebar Droite (NOUVEAU) | |
| const sidebarRightToggle = document.getElementById('sidebar-right-toggle'); | |
| const sidebarRight = document.getElementById('sidebar-right'); | |
| // Logique pour la Sidebar Gauche | |
| function toggleSidebarLeft() { | |
| sidebarLeft.classList.toggle('open'); | |
| // Gérer le décalage du contenu principal sur grand écran (lg) | |
| if (window.innerWidth >= 1024) { | |
| mainContent.classList.toggle('sidebar-left-open'); | |
| } | |
| } | |
| // Logique pour la Sidebar Droite (NOUVEAU) | |
| function toggleSidebarRight() { | |
| sidebarRight.classList.toggle('open'); | |
| // Gérer le décalage du contenu principal sur grand écran (lg) | |
| if (window.innerWidth >= 1024) { | |
| mainContent.classList.toggle('sidebar-right-open'); | |
| } | |
| } | |
| sidebarLeftToggle.addEventListener('click', toggleSidebarLeft); | |
| sidebarRightToggle.addEventListener('click', toggleSidebarRight); // Lier le nouveau bouton | |
| // Logique de Déconnexion (Conservée) | |
| document.getElementById('logout-button-sidebar').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| window.location.href = e.currentTarget.href; | |
| }); | |
| // --- PARTIE 2: GESTION DE L'ÉDITEUR DE CODE (CodeMirror + Onglets) --- | |
| // Structure de données pour les fichiers (V1: Stockage local) | |
| let files = [ | |
| { id: 1, name: 'index.html', content: document.getElementById('code-editor-textarea').value, mode: 'htmlmixed', isActive: true }, | |
| { id: 2, name: 'style.css', content: "body {\n font-family: sans-serif;\n color: #4CAF50; /* Green */\n}", mode: 'css', isActive: false }, | |
| { id: 3, name: 'script.js', content: "console.log('Editor Ready!');", mode: 'javascript', isActive: false } | |
| ]; | |
| let fileIdCounter = files.length + 1; | |
| let currentEditor = null; // L'instance CodeMirror | |
| const tabBar = document.getElementById('tab-bar'); | |
| const editorTextarea = document.getElementById('code-editor-textarea'); | |
| const statusBarLanguage = document.getElementById('status-language'); | |
| const statusBarLineCol = document.getElementById('status-line-col'); | |
| const fileExplorer = document.getElementById('file-explorer'); | |
| // Éléments de la modale | |
| const createFileModal = document.getElementById('create-file-modal'); | |
| const newFileNameInput = document.getElementById('new-file-name-input'); | |
| const confirmCreateFileButton = document.getElementById('confirm-create-file'); | |
| const cancelCreateFileButton = document.getElementById('cancel-create-file'); | |
| /** | |
| * Détermine le mode CodeMirror à partir de l'extension du nom de fichier. | |
| * @param {string} fileName Le nom du fichier. | |
| * @returns {string} Le mode CodeMirror. | |
| */ | |
| function getModeFromFileExtension(fileName) { | |
| const ext = fileName.split('.').pop().toLowerCase(); | |
| switch (ext) { | |
| // Web | |
| case 'html': | |
| case 'htm': | |
| return 'htmlmixed'; | |
| case 'css': | |
| return 'css'; | |
| case 'js': | |
| return 'javascript'; | |
| case 'ts': | |
| return 'typescript'; // Nouveau | |
| case 'json': | |
| return 'application/json'; // Nouveau (mode spécifique pour JSON) | |
| // Backend / Systèmes | |
| case 'py': | |
| return 'python'; | |
| case 'php': | |
| case 'phtml': | |
| return 'php'; | |
| case 'go': | |
| return 'go'; // Nouveau | |
| case 'rs': | |
| return 'rust'; // Nouveau | |
| case 'sh': | |
| return 'shell'; | |
| // Langages C-like (Java, C, C++, C#) | |
| case 'java': | |
| case 'c': | |
| case 'cpp': | |
| case 'cs': | |
| return 'clike'; | |
| // Données et Bases de données | |
| case 'sql': | |
| return 'sql'; | |
| case 'yaml': | |
| case 'yml': | |
| return 'yaml'; // Nouveau | |
| // Documentation et autres | |
| case 'md': | |
| case 'markdown': | |
| return 'markdown'; | |
| case 'tex': | |
| case 'latex': | |
| return 'stex'; // Nouveau (pour LaTeX) | |
| default: | |
| return 'text/plain'; // Mode texte brut pour les inconnus ou le texte simple | |
| } | |
| } | |
| /** | |
| * Initialise CodeMirror sur le textarea. | |
| */ | |
| function initializeEditor(initialContent, mode) { | |
| if (currentEditor) { | |
| currentEditor.toTextArea(); // Détruit l'ancienne instance | |
| currentEditor = null; | |
| } | |
| editorTextarea.value = initialContent; | |
| currentEditor = CodeMirror.fromTextArea(editorTextarea, { | |
| lineNumbers: true, | |
| mode: mode, | |
| theme: 'monokai', | |
| indentUnit: 4, | |
| tabSize: 4, | |
| indentWithTabs: false, | |
| autofocus: true, | |
| // MODIFICATION 1.2: Désactiver le retour à la ligne pour forcer le défilement horizontal | |
| lineWrapping: false, | |
| }); | |
| // Événement de mise à jour du contenu et de la barre de statut | |
| currentEditor.on("change", handleEditorChange); | |
| currentEditor.on("cursorActivity", updateStatusBar); | |
| // S'assurer que CodeMirror utilise 100% de la hauteur disponible dans son conteneur parent flexible | |
| const cmElement = currentEditor.getWrapperElement(); | |
| cmElement.style.height = '100%'; | |
| updateStatusBar(); | |
| } | |
| /** | |
| * Met à jour le contenu de l'objet fichier actif dans le tableau `files`. | |
| */ | |
| function handleEditorChange() { | |
| if (currentEditor) { | |
| const activeFile = files.find(f => f.isActive); | |
| if (activeFile) { | |
| activeFile.content = currentEditor.getValue(); | |
| } | |
| } | |
| } | |
| /** | |
| * Met à jour les indicateurs Ligne/Col dans la barre de statut. | |
| */ | |
| function updateStatusBar() { | |
| if (currentEditor) { | |
| const cursor = currentEditor.getCursor(); | |
| statusBarLineCol.textContent = `Ligne: ${cursor.line + 1}, Col: ${cursor.ch + 1}`; | |
| } | |
| } | |
| /** | |
| * Affiche l'éditeur avec le contenu du fichier sélectionné. | |
| * @param {number} fileId L'ID du fichier à activer. | |
| */ | |
| function setActiveFile(fileId) { | |
| files.forEach(f => f.isActive = f.id === fileId); | |
| const activeFile = files.find(f => f.isActive); | |
| if (activeFile) { | |
| // Mise à jour de CodeMirror | |
| if (currentEditor) { | |
| currentEditor.setValue(activeFile.content); | |
| currentEditor.setOption("mode", activeFile.mode); | |
| currentEditor.focus(); | |
| } else { | |
| // Initialisation au premier chargement | |
| initializeEditor(activeFile.content, activeFile.mode); | |
| } | |
| // Mise à jour de la barre de statut | |
| const modeName = activeFile.mode.includes('html') ? 'HTML' : (activeFile.mode === 'css' ? 'CSS' : 'JavaScript'); | |
| statusBarLanguage.textContent = `Langage: ${modeName}`; | |
| } | |
| renderTabs(); | |
| renderFileExplorer(); // Mise à jour de l'explorateur | |
| } | |
| /** | |
| * Génère et affiche les onglets dans la barre d'onglets. | |
| */ | |
| function renderTabs() { | |
| // Enlever tous les onglets existants sauf le bouton '+' et 'Sauvegarder' et 'Importer' | |
| // On cible spécifiquement les éléments avec la classe 'tab-item' | |
| const existingTabs = tabBar.querySelectorAll('.tab-item'); | |
| existingTabs.forEach(tab => tab.remove()); | |
| const saveButton = document.getElementById('save-button'); | |
| files.forEach(file => { | |
| const tab = document.createElement('div'); | |
| // 'flex items-center' est conservé mais 'inline-flex' est dans le CSS pour le white-space: nowrap | |
| tab.className = `tab-item flex items-center ${file.isActive ? 'active' : ''}`; | |
| tab.dataset.fileId = file.id; | |
| tab.innerHTML = ` | |
| <span>${file.name}</span> | |
| <span class="tab-close material-symbols-rounded text-sm hover:text-color-var transition-colors-theme ml-2">close</span> | |
| `; | |
| // Attacher l'onglet avant le bouton Sauvegarder (qui est 'ml-auto') | |
| // Remarque: La structure des boutons dans le HTML implique que les onglets sont insérés avant le saveButton (qui a ml-auto) | |
| tabBar.insertBefore(tab, saveButton); | |
| // Gérer le clic pour activer l'onglet | |
| tab.querySelector('span').addEventListener('click', (e) => { | |
| // Évite de déclencher l'activation si on clique sur l'icône de fermeture | |
| if (!e.target.classList.contains('tab-close')) { | |
| setActiveFile(file.id); | |
| } | |
| }); | |
| // Gérer le clic pour fermer l'onglet | |
| tab.querySelector('.tab-close').addEventListener('click', () => closeFile(file.id)); | |
| }); | |
| } | |
| /** | |
| * Génère et affiche la liste des fichiers dans l'explorateur de fichiers. | |
| */ | |
| function renderFileExplorer() { | |
| fileExplorer.innerHTML = ''; // Nettoyer l'explorateur | |
| files.forEach(file => { | |
| const item = document.createElement('a'); | |
| const isActiveClass = file.isActive ? 'bg-sidebar-active-var text-color-var' : 'text-secondary-var hover:text-color-var hover:bg-sidebar-active-var'; | |
| item.href = '#'; | |
| item.className = `flex items-center p-2 text-sm rounded-lg transition duration-150 transition-colors-theme ${isActiveClass}`; | |
| item.dataset.fileId = file.id; | |
| item.innerHTML = ` | |
| <span class="material-symbols-rounded text-base">insert_drive_file</span> | |
| <span class="ml-2 truncate">${file.name}</span> | |
| `; | |
| item.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| setActiveFile(file.id); | |
| }); | |
| fileExplorer.appendChild(item); | |
| }); | |
| } | |
| /** | |
| * Ajoute un nouveau fichier/onglet. | |
| * @param {string} fileName Le nom du fichier. | |
| * @param {string} fileContent Le contenu du fichier (par défaut vide). | |
| */ | |
| function createNewFile(fileName, fileContent = "// Nouveau fichier") { | |
| const mode = getModeFromFileExtension(fileName); | |
| const newFile = { | |
| id: fileIdCounter++, | |
| name: fileName, | |
| content: fileContent, | |
| mode: mode, | |
| isActive: false // Sera activé par setActiveFile | |
| }; | |
| files.push(newFile); | |
| setActiveFile(newFile.id); | |
| } | |
| /** | |
| * Ferme un fichier/onglet. | |
| * @param {number} fileId L'ID du fichier à fermer. | |
| */ | |
| function closeFile(fileId) { | |
| const index = files.findIndex(f => f.id === fileId); | |
| if (index === -1) return; | |
| const wasActive = files[index].isActive; | |
| files.splice(index, 1); // Retire le fichier | |
| if (files.length === 0) { | |
| // Si plus de fichiers, ajoute un fichier vierge | |
| createNewFile(`untitled-${fileIdCounter}.js`); | |
| return; | |
| } | |
| if (wasActive) { | |
| // Active le fichier précédent ou le premier s'il n'y a pas de précédent | |
| const newActiveFile = files[Math.max(0, index - 1)]; | |
| setActiveFile(newActiveFile.id); | |
| } else { | |
| // Si ce n'était pas l'actif, on rafraîchit juste les onglets et l'explorateur | |
| renderTabs(); | |
| renderFileExplorer(); | |
| } | |
| } | |
| // --- Logique de la Modale de Création de Fichier --- | |
| document.getElementById('add-tab-button').addEventListener('click', () => { | |
| newFileNameInput.value = ''; // Réinitialiser l'input | |
| createFileModal.classList.add('open'); | |
| newFileNameInput.focus(); | |
| }); | |
| cancelCreateFileButton.addEventListener('click', () => { | |
| createFileModal.classList.remove('open'); | |
| }); | |
| confirmCreateFileButton.addEventListener('click', () => { | |
| const fileName = newFileNameInput.value.trim(); | |
| if (fileName) { | |
| createNewFile(fileName); | |
| createFileModal.classList.remove('open'); | |
| } else { | |
| alert("Veuillez entrer un nom de fichier."); | |
| } | |
| }); | |
| // --- Logique d'Importation de Fichier --- | |
| const importButton = document.getElementById('import-button'); | |
| const fileInput = document.getElementById('file-input'); | |
| importButton.addEventListener('click', () => { | |
| fileInput.click(); // Simuler le clic sur l'input file caché | |
| }); | |
| fileInput.addEventListener('change', (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const content = e.target.result; | |
| // Créer le nouveau fichier avec le nom et le contenu du fichier importé | |
| createNewFile(file.name, content); | |
| }; | |
| reader.readAsText(file); // Lire le fichier en tant que texte | |
| } | |
| // Réinitialiser l'input file pour permettre l'importation du même fichier à nouveau | |
| fileInput.value = null; | |
| }); | |
| // 3. Événements et initialisation | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialiser CodeMirror avec le fichier actif par défaut | |
| const activeFile = files.find(f => f.isActive); | |
| if (activeFile) { | |
| initializeEditor(activeFile.content, activeFile.mode); | |
| } | |
| // Événement factice pour le bouton Sauvegarder | |
| document.getElementById('save-button').addEventListener('click', () => { | |
| const active = files.find(f => f.isActive); | |
| alert(`Fichier "${active.name}" sauvegardé localement!\nContenu: ${active.content.substring(0, 50)}...`); | |
| }); | |
| // Rendu initial des onglets et de l'explorateur | |
| renderTabs(); | |
| renderFileExplorer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |