diff --git a/assets/css/github-preview.css b/assets/css/github-preview.css new file mode 100644 index 0000000..1bdc089 --- /dev/null +++ b/assets/css/github-preview.css @@ -0,0 +1,454 @@ +/* CSS pour la prévisualisation du journal - Style GitHub authentique */ + +.markdown-preview { + /* Variables spécifiques GitHub */ + --github-text-color: #1f2328; + --github-text-light: #656d76; + --github-bg-color: #ffffff; + --github-border-color: #d0d7de; + --github-border-muted: #d8dee4; + --github-accent-emphasis: #0969da; + --github-accent-fg: #0969da; + --github-neutral-muted: #afb8c1; + --github-canvas-subtle: #f6f8fa; + --github-danger-fg: #cf222e; + --github-success-fg: #1a7f37; + --github-attention-fg: #9a6700; + --github-severe-fg: #bc4c00; + --github-done-fg: #8250df; + + /* Override pour thème sombre */ + color: var(--github-text-color); + background: var(--github-bg-color); + border: 1px solid var(--github-border-color); + border-radius: 6px; + + /* GitHub typography */ + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; + + /* Layout comme GitHub */ + max-width: none; + width: 100%; + padding: 48px; + margin: 0; + box-sizing: border-box; +} + +/* Mode sombre GitHub */ +body.dark-theme .markdown-preview { + --github-text-color: #e6edf3; + --github-text-light: #7d8590; + --github-bg-color: #0d1117; + --github-border-color: #30363d; + --github-border-muted: #21262d; + --github-accent-emphasis: #1f6feb; + --github-accent-fg: #58a6ff; + --github-neutral-muted: #6e7681; + --github-canvas-subtle: #161b22; + --github-danger-fg: #f85149; + --github-success-fg: #3fb950; + --github-attention-fg: #d29922; + --github-severe-fg: #db6d28; + --github-done-fg: #a5a3ff; + + background: var(--github-bg-color); + color: var(--github-text-color); + border-color: var(--github-border-color); +} + +/* Titres style GitHub */ +.markdown-preview h1, +.markdown-preview h2, +.markdown-preview h3, +.markdown-preview h4, +.markdown-preview h5, +.markdown-preview h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + color: var(--github-text-color); +} + +.markdown-preview h1 { + font-size: 2em; + border-bottom: 1px solid var(--github-border-muted); + padding-bottom: 0.3em; + margin-top: 0; +} + +.markdown-preview h2 { + font-size: 1.5em; + border-bottom: 1px solid var(--github-border-muted); + padding-bottom: 0.3em; +} + +.markdown-preview h3 { + font-size: 1.25em; +} + +.markdown-preview h4 { + font-size: 1em; +} + +.markdown-preview h5 { + font-size: 0.875em; +} + +.markdown-preview h6 { + font-size: 0.85em; + color: var(--github-text-light); +} + +/* Paragraphes */ +.markdown-preview p { + margin-top: 0; + margin-bottom: 16px; +} + +/* Listes style GitHub */ +.markdown-preview ul, +.markdown-preview ol { + margin-top: 0; + margin-bottom: 16px; + padding-left: 2em; +} + +.markdown-preview li { + word-wrap: break-all; + margin-bottom: 0; +} + +.markdown-preview li > p { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-preview li + li { + margin-top: 0.25em; +} + +/* Listes de tâches */ +.markdown-preview .task-list-item { + list-style-type: none; +} + +.markdown-preview .task-list-item input[type="checkbox"] { + margin: 0 0.2em 0.25em -1.4em; + vertical-align: middle; +} + +/* Citations */ +.markdown-preview blockquote { + margin: 0 0 16px 0; + padding: 0 1em; + color: var(--github-text-light); + border-left: 0.25em solid var(--github-border-color); +} + +.markdown-preview blockquote > :first-child { + margin-top: 0; +} + +.markdown-preview blockquote > :last-child { + margin-bottom: 0; +} + +/* Code inline */ +.markdown-preview code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--github-neutral-muted); + border-radius: 6px; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; +} + +body.dark-theme .markdown-preview code { + background-color: rgba(110, 118, 129, 0.4); +} + +/* Blocs de code */ +.markdown-preview pre { + padding: 16px; + margin-bottom: 16px; + background-color: var(--github-canvas-subtle); + border-radius: 6px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + word-wrap: normal; +} + +body.dark-theme .markdown-preview pre { + background-color: var(--github-canvas-subtle); +} + +.markdown-preview pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; + white-space: pre; + word-break: normal; + word-spacing: normal; + tab-size: 4; +} + +/* Tables style GitHub */ +.markdown-preview table { + border-spacing: 0; + border-collapse: collapse; + display: table; + width: 100%; + overflow: auto; + margin-top: 0; + margin-bottom: 16px; + border: 1px solid var(--github-border-color); +} + +.markdown-preview table th { + font-weight: 600; + background-color: var(--github-canvas-subtle); + border: 1px solid var(--github-border-color); + padding: 6px 13px; + text-align: left; + vertical-align: top; +} + +body.dark-theme .markdown-preview table th { + background-color: var(--github-canvas-subtle); +} + +.markdown-preview table td { + border: 1px solid var(--github-border-color); + padding: 6px 13px; + text-align: left; + vertical-align: top; +} + +.markdown-preview table tr { + background-color: var(--github-bg-color); + border-top: 1px solid var(--github-border-color); +} + +.markdown-preview table tr:nth-child(even) { + background-color: var(--github-canvas-subtle); +} + +body.dark-theme .markdown-preview table tr:nth-child(even) { + background-color: var(--github-canvas-subtle); +} + +.markdown-preview table thead tr { + background-color: var(--github-canvas-subtle); +} + +body.dark-theme .markdown-preview table thead tr { + background-color: var(--github-canvas-subtle); +} + +/* Liens style GitHub */ +.markdown-preview a { + color: var(--github-accent-fg); + text-decoration: none; +} + +.markdown-preview a:hover { + text-decoration: underline; +} + +.markdown-preview a:visited { + color: var(--github-done-fg); +} + +/* Images */ +.markdown-preview img { + max-width: 100%; + box-sizing: content-box; + background-color: var(--github-bg-color); +} + +.markdown-preview img[align=right] { + padding-left: 20px; +} + +.markdown-preview img[align=left] { + padding-right: 20px; +} + +/* Séparateurs horizontaux */ +.markdown-preview hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: var(--github-border-color); + border: 0; + border-radius: 6px; +} + +/* Emphasis */ +.markdown-preview strong { + font-weight: 600; +} + +.markdown-preview em { + font-style: italic; +} + +/* Strikethrough */ +.markdown-preview del { + text-decoration: line-through; +} + +/* Keyboard keys */ +.markdown-preview kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: var(--github-text-light); + vertical-align: middle; + background-color: var(--github-canvas-subtle); + border: solid 1px var(--github-neutral-muted); + border-bottom-color: var(--github-neutral-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--github-neutral-muted); + font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; +} + +/* Support pour les alertes GitHub (Notes, Tips, etc.) */ +.markdown-preview .markdown-alert { + padding: 0.5rem 1em; + margin-bottom: 16px; + color: inherit; + border-left: 0.25em solid var(--github-border-color); + background-color: var(--github-canvas-subtle); + border-radius: 0 6px 6px 0; +} + +.markdown-preview .markdown-alert > :first-child { + margin-top: 0; +} + +.markdown-preview .markdown-alert > :last-child { + margin-bottom: 0; +} + +.markdown-preview .markdown-alert .markdown-alert-title { + display: flex; + font-weight: 500; + align-items: center; + line-height: 1; +} + +.markdown-preview .markdown-alert.markdown-alert-note { + border-left-color: var(--github-accent-emphasis); +} + +.markdown-preview .markdown-alert.markdown-alert-tip { + border-left-color: var(--github-success-fg); +} + +.markdown-preview .markdown-alert.markdown-alert-important { + border-left-color: var(--github-done-fg); +} + +.markdown-preview .markdown-alert.markdown-alert-warning { + border-left-color: var(--github-attention-fg); +} + +.markdown-preview .markdown-alert.markdown-alert-caution { + border-left-color: var(--github-danger-fg); +} + +/* Support pour Mermaid avec style GitHub */ +.markdown-preview .mermaid { + text-align: center; + margin: 16px 0; + background: transparent; +} + +.markdown-preview .mermaid svg { + max-width: 100%; + height: auto; +} + +/* Support pour les footnotes */ +.markdown-preview .footnotes { + font-size: 0.875em; + color: var(--github-text-light); + border-top: 1px solid var(--github-border-color); + margin-top: 24px; + padding-top: 24px; +} + +.markdown-preview .footnotes ol { + padding-left: 16px; +} + +.markdown-preview .footnotes li { + margin: 0.25rem 0; +} + +/* Amélioration de l'espacement pour les éléments imbriqués */ +.markdown-preview li p { + margin-bottom: 0; + margin-top: 0; +} + +.markdown-preview li blockquote { + margin: 8px 0; +} + +.markdown-preview li ul, +.markdown-preview li ol { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-preview ul ul, +.markdown-preview ul ol, +.markdown-preview ol ol, +.markdown-preview ol ul { + margin-top: 0; + margin-bottom: 0; +} + +/* Scrollbars GitHub-like (webkit only) */ +.markdown-preview::-webkit-scrollbar { + width: 16px; + height: 16px; +} + +.markdown-preview::-webkit-scrollbar-corner, +.markdown-preview::-webkit-scrollbar-track { + background-color: transparent; +} + +.markdown-preview::-webkit-scrollbar-thumb { + background-color: var(--github-neutral-muted); + border-radius: 8px; + border: 4px solid transparent; + background-clip: content-box; +} + +.markdown-preview::-webkit-scrollbar-thumb:hover { + background-color: var(--github-text-light); +} + +/* Responsive pour mobile */ +@media (max-width: 768px) { + .markdown-preview { + padding: 24px 16px; + } +} \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js index 8b0bbe7..b14cf0c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -125,32 +125,42 @@ class ConceptionAssistant { this.showNotification('Retour en haut', 'success'); } - saveState() { + saveState(immediate = false) { + // Si immediate est true, sauvegarder immédiatement sans debounce + if (immediate) { + this.performSaveState(); + return; + } + // Éviter de sauvegarder trop souvent (debounce) if (this.saveStateTimer) { clearTimeout(this.saveStateTimer); } this.saveStateTimer = setTimeout(() => { - const currentContent = this.editor.innerText; - - // Ne pas sauvegarder si le contenu n'a pas changé - if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentContent) { - return; - } - - this.undoStack.push(currentContent); - - // Limiter la pile d'annulation à 50 éléments - if (this.undoStack.length > 50) { - this.undoStack.shift(); - } - - // Vider la pile de refaire car on a fait une nouvelle action - this.redoStack = []; + this.performSaveState(); }, 1000); // Sauvegarder après 1 seconde d'inactivité } + performSaveState() { + const currentContent = this.editor.innerText; + + // Ne pas sauvegarder si le contenu n'a pas changé + if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentContent) { + return; + } + + this.undoStack.push(currentContent); + + // Limiter la pile d'annulation à 50 éléments + if (this.undoStack.length > 50) { + this.undoStack.shift(); + } + + // Vider la pile de refaire car on a fait une nouvelle action + this.redoStack = []; + } + undo() { if (this.undoStack.length > 1) { const currentContent = this.undoStack.pop(); @@ -262,6 +272,14 @@ class ConceptionAssistant { this.currentJournalId = id; this.editor.innerText = journal.markdownContent; this.generateTOC(); + + // Réinitialiser l'historique pour le nouveau journal + this.undoStack = [journal.markdownContent]; + this.redoStack = []; + + // S'assurer que l'éditeur est en mode édition + this.ensureEditMode(); + this.showNotification('Journal chargé', 'success'); } } catch (error) { @@ -336,6 +354,14 @@ class ConceptionAssistant { this.editor.innerText = ''; this.generateTOC(); this.clearFeedback(); + + // Réinitialiser l'historique pour le nouveau journal + this.undoStack = ['']; + this.redoStack = []; + + // S'assurer que l'éditeur est en mode édition + this.ensureEditMode(); + this.showNotification('Nouveau journal créé', 'success'); } @@ -441,64 +467,134 @@ class ConceptionAssistant { } scrollToHeading(title) { - const content = this.editor.innerText; - const lines = content.split('\n'); + try { + // Méthode plus simple et robuste : chercher le texte directement dans l'éditeur + const content = this.editor.innerText; + const lines = content.split('\n'); - // Chercher la ligne qui correspond au titre - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { - try { - // Approche plus simple : utiliser un élément temporaire avec une position spécifique - const tempElement = document.createElement('div'); - tempElement.style.position = 'absolute'; - tempElement.style.visibility = 'hidden'; - tempElement.style.height = '1px'; - - // Calculer la position approximative en fonction de la ligne - const editorRect = this.editor.getBoundingClientRect(); - const computedStyle = window.getComputedStyle(this.editor); - const lineHeight = parseInt(computedStyle.lineHeight) || 20; - - // Insérer l'élément temporaire dans l'éditeur - this.editor.appendChild(tempElement); - - // Calculer la position de scroll basée sur le numéro de ligne - const targetPosition = i * lineHeight; - - // Scroller vers la position calculée - this.editor.scrollTo({ - top: targetPosition, - behavior: 'smooth' - }); - - // Si l'éditeur n'a pas de scroll, utiliser le parent - if (this.editor.scrollHeight <= this.editor.clientHeight) { - const scrollContainer = this.editor.parentElement || document.documentElement; - const editorTop = this.editor.offsetTop; - - scrollContainer.scrollTo({ - top: editorTop + targetPosition - 100, // -100 pour un padding - behavior: 'smooth' - }); - } - - // Nettoyer l'élément temporaire - setTimeout(() => { - if (tempElement.parentNode) { - tempElement.parentNode.removeChild(tempElement); - } - }, 100); - - this.showNotification('Navigation vers la section', 'success'); - return; - } catch (error) { - console.log('Erreur de scroll:', error); + // Trouver l'index de la ligne correspondant au titre + let targetLineIndex = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { + targetLineIndex = i; + break; } } - } - this.showNotification('Section non trouvée', 'warning'); + if (targetLineIndex === -1) { + this.showNotification('Section non trouvée', 'warning'); + return; + } + + // Calculer la position approximative de la ligne + const editorStyles = window.getComputedStyle(this.editor); + const lineHeight = parseFloat(editorStyles.lineHeight) || 20; + const paddingTop = parseFloat(editorStyles.paddingTop) || 0; + + // Calculer la position de scroll basée sur le numéro de ligne + const targetScrollPosition = (targetLineIndex * lineHeight) + paddingTop; + + // Déterminer si l'éditeur ou la fenêtre doit être scrollée + const editorRect = this.editor.getBoundingClientRect(); + const editorHasScroll = this.editor.scrollHeight > this.editor.clientHeight; + + if (editorHasScroll) { + // Scroller dans l'éditeur + this.editor.scrollTo({ + top: Math.max(0, targetScrollPosition - 60), + behavior: 'smooth' + }); + } else { + // Scroller la page entière + const editorTop = this.editor.offsetTop; + const windowScrollTarget = editorTop + targetScrollPosition - 100; + + window.scrollTo({ + top: Math.max(0, windowScrollTarget), + behavior: 'smooth' + }); + } + + // Optionnel : mettre en surbrillance temporairement le titre + this.highlightHeading(title); + + this.showNotification(`Navigation vers: ${title}`, 'success'); + + } catch (error) { + console.error('Erreur de scroll:', error); + this.showNotification('Erreur lors de la navigation', 'error'); + } + } + + highlightHeading(title) { + // Fonction pour mettre en surbrillance temporairement le titre trouvé + try { + const content = this.editor.innerText; + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { + // Créer un range pour sélectionner la ligne + const selection = window.getSelection(); + const range = document.createRange(); + + // Trouver le nœud texte et la position + const walker = document.createTreeWalker( + this.editor, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let currentLine = 0; + let textNode = walker.nextNode(); + + while (textNode && currentLine < i) { + const nodeText = textNode.textContent; + const newLines = (nodeText.match(/\n/g) || []).length; + currentLine += newLines; + + if (currentLine < i) { + textNode = walker.nextNode(); + } + } + + if (textNode) { + // Trouver le début de la ligne dans ce nœud + const nodeText = textNode.textContent; + const linesInNode = nodeText.split('\n'); + const targetLineInNode = i - (currentLine - linesInNode.length + 1); + + if (targetLineInNode >= 0 && targetLineInNode < linesInNode.length) { + let startPos = 0; + for (let j = 0; j < targetLineInNode; j++) { + startPos += linesInNode[j].length + 1; + } + + const endPos = startPos + linesInNode[targetLineInNode].length; + + range.setStart(textNode, startPos); + range.setEnd(textNode, endPos); + + // Sélectionner temporairement + selection.removeAllRanges(); + selection.addRange(range); + + // Retirer la sélection après un court délai + setTimeout(() => { + selection.removeAllRanges(); + }, 1000); + } + } + break; + } + } + } catch (error) { + // Ignorer les erreurs de surbrillance, ce n'est pas critique + console.log('Erreur surbrillance:', error); + } } exportMarkdown() { @@ -529,9 +625,18 @@ class ConceptionAssistant { const reader = new FileReader(); reader.onload = (e) => { - this.editor.innerText = e.target.result; + const importedContent = e.target.result; + this.editor.innerText = importedContent; this.generateTOC(); this.currentJournalId = null; // Nouveau journal + + // Réinitialiser l'historique pour le fichier importé + this.undoStack = [importedContent]; + this.redoStack = []; + + // S'assurer que l'éditeur est en mode édition + this.ensureEditMode(); + this.showNotification('Fichier Markdown importé', 'success'); }; reader.readAsText(file); @@ -633,6 +738,9 @@ class ConceptionAssistant { break; case 'liberty': + // Sauvegarder l'état avant les modifications de l'IA + this.saveState(true); + const count = document.getElementById('liberty-repeat-count')?.value || 3; // Mode liberté utilise toujours le document complet result = await this.callAI('/api/ai/liberty-mode', { content: fullContent, iterations: count, focus: 'conception' }); @@ -641,7 +749,8 @@ class ConceptionAssistant { if (result.finalContent) { this.editor.innerText = result.finalContent; this.generateTOC(); - this.saveState(); + // Sauvegarder l'état après les modifications de l'IA + this.saveState(true); } let libertyHTML = `Mode Liberté Intelligent (${result.iterations} itérations)

`; @@ -775,6 +884,9 @@ class ConceptionAssistant { if (!this.lastRephraseData) return; try { + // Sauvegarder l'état avant la reformulation pour permettre l'undo + this.saveState(true); + // Remplacer le texte dans l'éditeur const range = this.lastRephraseData.selection.getRangeAt(0); range.deleteContents(); @@ -784,6 +896,9 @@ class ConceptionAssistant { window.getSelection().removeAllRanges(); this.generateTOC(); + // Sauvegarder l'état après la reformulation + this.saveState(true); + // Afficher un message de succès this.showNotification('Reformulation appliquée avec succès', 'success'); this.clearFeedback(); @@ -806,6 +921,31 @@ class ConceptionAssistant { `; } + ensureEditMode() { + // Si on est en mode preview, forcer le retour en mode édition + if (this.isPreviewMode) { + const previewBtn = document.getElementById('preview-toggle'); + + // Revenir en mode édition sans utiliser originalContent car on veut le nouveau contenu + this.editor.contentEditable = true; + this.editor.style.background = ''; + this.editor.style.border = ''; + this.editor.style.borderRadius = ''; + + // Changer le bouton + if (previewBtn) { + previewBtn.innerHTML = 'Visualiser'; + previewBtn.classList.remove('secondary'); + previewBtn.classList.add('primary'); + } + + this.isPreviewMode = false; + } + + // S'assurer que l'éditeur est toujours éditable + this.editor.contentEditable = true; + } + showNotification(message, type = 'success') { const notification = document.createElement('div'); notification.className = `notification ${type}`; @@ -1059,6 +1199,13 @@ function initializeTemplateForm() { app.generateTOC(); app.currentJournalId = null; // Nouveau journal + // Réinitialiser l'historique pour le nouveau template + app.undoStack = [result.data.content]; + app.redoStack = []; + + // S'assurer que l'éditeur est en mode édition + app.ensureEditMode(); + app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success'); closeAllPanels(); } else { diff --git a/views/page.js b/views/page.js index a53ddf6..5cd8bb4 100644 --- a/views/page.js +++ b/views/page.js @@ -11,6 +11,7 @@ function getHead(){ Conception-Assistant +