From a09b73e4f1e8920d1bfa180f82bfa37f2dd2a1e1 Mon Sep 17 00:00:00 2001 From: Mathieu Aumont Date: Wed, 12 Nov 2025 20:17:43 +0100 Subject: [PATCH] Changement des ilink vers markdown pur --- cmd/server/main.go | 1 + frontend/src/daily-notes.js | 3 +- frontend/src/debug.js | 45 ++++ frontend/src/editor.js | 59 +++-- frontend/src/favorites.js | 158 ++++++------ frontend/src/file-tree.js | 103 ++++---- frontend/src/font-manager.js | 9 +- frontend/src/keyboard-shortcuts.js | 23 +- frontend/src/link-inserter.js | 25 +- frontend/src/search.js | 1 + frontend/src/sidebar-sections.js | 19 +- frontend/src/theme-manager.js | 9 +- frontend/src/ui.js | 1 + frontend/src/vim-mode-manager.js | 11 +- internal/api/handler.go | 320 +++++++++++++++++++++---- internal/indexer/indexer.go | 48 +++- notes/.favorites.json | 35 +-- notes/{tasks => }/bugs.md | 0 notes/meetings/2025/sprint-planning.md | 6 +- notes/scratch.md | 3 +- notes/un-dossier/test/Poppy-test.md | 9 +- static/theme.css | 174 ++++++++++++-- templates/editor.html | 12 +- templates/file-tree.html | 2 +- templates/index.html | 42 ++-- 25 files changed, 803 insertions(+), 315 deletions(-) create mode 100644 frontend/src/debug.js rename notes/{tasks => }/bugs.md (100%) diff --git a/cmd/server/main.go b/cmd/server/main.go index 9e1cdc8..6b8341a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -81,6 +81,7 @@ func main() { mux.Handle("/api/daily", apiHandler) // Daily notes mux.Handle("/api/daily/", apiHandler) // Daily notes mux.Handle("/api/favorites", apiHandler) // Favorites + mux.Handle("/api/folder/", apiHandler) // Folder view mux.Handle("/api/notes/", apiHandler) mux.Handle("/api/tree", apiHandler) diff --git a/frontend/src/daily-notes.js b/frontend/src/daily-notes.js index 9aff381..2dd2fdf 100644 --- a/frontend/src/daily-notes.js +++ b/frontend/src/daily-notes.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * DailyNotes - Gère les raccourcis et interactions pour les daily notes */ @@ -23,7 +24,7 @@ function initDailyNotesShortcut() { } }); - console.log('Daily notes shortcuts initialized (Ctrl/Cmd+D)'); + debug('Daily notes shortcuts initialized (Ctrl/Cmd+D)'); } /** diff --git a/frontend/src/debug.js b/frontend/src/debug.js new file mode 100644 index 0000000..ab5676e --- /dev/null +++ b/frontend/src/debug.js @@ -0,0 +1,45 @@ +/** + * Debug utility - Conditional logging + * Set DEBUG to true to enable console logs, false to disable + */ + +// Change this to false in production to disable all debug logs +export const DEBUG = false; + +/** + * Conditional console.log + * Only logs if DEBUG is true + */ +export function debug(...args) { + if (DEBUG) { + console.log(...args); + } +} + +/** + * Conditional console.warn + * Only logs if DEBUG is true + */ +export function debugWarn(...args) { + if (DEBUG) { + console.warn(...args); + } +} + +/** + * Conditional console.error + * Always logs errors regardless of DEBUG flag + */ +export function debugError(...args) { + console.error(...args); +} + +/** + * Conditional console.info + * Only logs if DEBUG is true + */ +export function debugInfo(...args) { + if (DEBUG) { + console.info(...args); + } +} diff --git a/frontend/src/editor.js b/frontend/src/editor.js index 41ac306..f25168b 100644 --- a/frontend/src/editor.js +++ b/frontend/src/editor.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; import { EditorState } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { basicSetup } from '@codemirror/basic-setup'; @@ -13,7 +14,7 @@ let vimExtension = null; try { const { vim } = await import('@replit/codemirror-vim'); vimExtension = vim; - console.log('✅ Vim extension loaded and ready'); + debug('✅ Vim extension loaded and ready'); } catch (error) { console.warn('⚠️ Vim extension not available:', error.message); } @@ -118,7 +119,7 @@ class MarkdownEditor { if (window.vimModeManager && window.vimModeManager.isEnabled()) { if (vimExtension) { extensions.push(vimExtension()); - console.log('✅ Vim mode enabled in editor'); + debug('✅ Vim mode enabled in editor'); } else { console.warn('⚠️ Vim mode requested but extension not loaded yet'); } @@ -246,10 +247,28 @@ class MarkdownEditor { const html = marked.parse(contentWithoutFrontMatter); // Permettre les attributs HTMX et onclick dans DOMPurify const cleanHtml = DOMPurify.sanitize(html, { - ADD_ATTR: ['hx-get', 'hx-target', 'hx-swap', 'onclick'] + ADD_ATTR: ['hx-get', 'hx-target', 'hx-swap', 'hx-push-url', 'onclick'] }); this.preview.innerHTML = cleanHtml; + // Post-processing : convertir les liens Markdown vers .md en liens HTMX cliquables + this.preview.querySelectorAll('a[href$=".md"]').forEach(link => { + const href = link.getAttribute('href'); + // Ne traiter que les liens relatifs (pas les URLs complètes http://) + if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//')) { + debug('[Preview] Converting Markdown link to HTMX:', href); + + // Transformer en lien HTMX interne + link.setAttribute('hx-get', `/api/notes/${href}`); + link.setAttribute('hx-target', '#editor-container'); + link.setAttribute('hx-swap', 'innerHTML'); + link.setAttribute('hx-push-url', 'true'); + link.setAttribute('href', '#'); + link.setAttribute('onclick', 'return false;'); + link.classList.add('internal-link'); + } + }); + // Traiter les nouveaux éléments HTMX if (typeof htmx !== 'undefined') { htmx.process(this.preview); @@ -289,7 +308,7 @@ class MarkdownEditor { const targetElement = link.getAttribute('hx-target') || '#editor-container'; const swapMethod = link.getAttribute('hx-swap') || 'innerHTML'; - console.log('[InternalLink] Clicked:', target); + debug('[InternalLink] Clicked:', target); if (target && typeof htmx !== 'undefined') { htmx.ajax('GET', target, { @@ -300,7 +319,7 @@ class MarkdownEditor { }); }); - console.log('[Preview] Setup', freshLinks.length, 'internal link handlers'); + debug('[Preview] Setup', freshLinks.length, 'internal link handlers'); } syncToTextarea() { @@ -334,7 +353,7 @@ class MarkdownEditor { } async reloadWithVimMode() { - console.log('Reloading editor with Vim mode...'); + debug('Reloading editor with Vim mode...'); await this.initEditor(); } } @@ -654,7 +673,7 @@ class SlashCommands { // Commande spéciale avec modal (comme /ilink) if (command.isModal && command.handler) { - console.log('Executing modal command:', command.name); + debug('Executing modal command:', command.name); // NE PAS cacher la palette tout de suite car le handler a besoin de slashPos // La palette sera cachée par le handler lui-même command.handler(); @@ -685,7 +704,7 @@ class SlashCommands { // Sauvegarder la position du slash IMMÉDIATEMENT avant toute autre opération const savedSlashPos = this.slashPos; - console.log('[SlashCommands] openLinkInserter - savedSlashPos:', savedSlashPos); + debug('[SlashCommands] openLinkInserter - savedSlashPos:', savedSlashPos); if (!savedSlashPos) { console.error('[SlashCommands] No slash position available!'); @@ -698,7 +717,7 @@ class SlashCommands { // S'assurer que le LinkInserter global existe, le créer si nécessaire if (!window.linkInserter) { - console.log('Initializing LinkInserter...'); + debug('Initializing LinkInserter...'); window.linkInserter = new LinkInserter(); } @@ -706,14 +725,14 @@ class SlashCommands { window.linkInserter.open({ editorView: this.editorView, onSelect: ({ title, path }) => { - console.log('[SlashCommands] onSelect callback received:', { title, path }); - console.log('[SlashCommands] savedSlashPos:', savedSlashPos); + debug('[SlashCommands] onSelect callback received:', { title, path }); + debug('[SlashCommands] savedSlashPos:', savedSlashPos); - // Créer un lien HTMX cliquable dans le preview - // Format : Title - // Le onclick="return false;" empêche le comportement par défaut du # qui pourrait rediriger - const linkHtml = `${title}`; - console.log('[SlashCommands] Inserting:', linkHtml); + // Créer un lien Markdown standard + // Format : [Title](path/to/note.md) + // Le post-processing dans updatePreview() le rendra cliquable avec HTMX + const linkMarkdown = `[${title}](${path})`; + debug('[SlashCommands] Inserting Markdown link:', linkMarkdown); const { state, dispatch } = this.editorView; const { from } = state.selection.main; @@ -721,15 +740,15 @@ class SlashCommands { // Remplacer depuis le "/" jusqu'au curseur actuel const replaceFrom = savedSlashPos.absolutePos; - console.log('[SlashCommands] Replacing from', replaceFrom, 'to', from); + debug('[SlashCommands] Replacing from', replaceFrom, 'to', from); dispatch(state.update({ - changes: { from: replaceFrom, to: from, insert: linkHtml }, - selection: { anchor: replaceFrom + linkHtml.length } + changes: { from: replaceFrom, to: from, insert: linkMarkdown }, + selection: { anchor: replaceFrom + linkMarkdown.length } })); this.editorView.focus(); - console.log('[SlashCommands] Link inserted successfully'); + debug('[SlashCommands] Markdown link inserted successfully'); } }); } diff --git a/frontend/src/favorites.js b/frontend/src/favorites.js index 3e2f226..f216c16 100644 --- a/frontend/src/favorites.js +++ b/frontend/src/favorites.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * Favorites - Gère le système de favoris */ @@ -8,33 +9,33 @@ class FavoritesManager { } init() { - console.log('FavoritesManager: Initialisation...'); + debug('FavoritesManager: Initialisation...'); // Charger les favoris au démarrage this.refreshFavorites(); // Écouter les événements HTMX pour mettre à jour les boutons document.body.addEventListener('htmx:afterSwap', (event) => { - console.log('HTMX afterSwap:', event.detail.target.id); + debug('HTMX afterSwap:', event.detail.target.id); if (event.detail.target.id === 'file-tree') { - console.log('File-tree chargé, ajout des boutons favoris...'); + debug('File-tree chargé, ajout des boutons favoris...'); setTimeout(() => this.attachFavoriteButtons(), 100); } if (event.detail.target.id === 'favorites-list') { - console.log('Favoris rechargés, mise à jour des boutons...'); + debug('Favoris rechargés, mise à jour des boutons...'); setTimeout(() => this.attachFavoriteButtons(), 100); } }); // Attacher les boutons après un délai pour laisser HTMX charger le file-tree setTimeout(() => { - console.log('Tentative d\'attachement des boutons favoris après délai...'); + debug('Tentative d\'attachement des boutons favoris après délai...'); this.attachFavoriteButtons(); }, 1000); - console.log('FavoritesManager: Initialisé'); + debug('FavoritesManager: Initialisé'); } refreshFavorites() { @@ -47,7 +48,7 @@ class FavoritesManager { } async addFavorite(path, isDir, title) { - console.log('addFavorite appelé avec:', { path, isDir, title }); + debug('addFavorite appelé avec:', { path, isDir, title }); try { // Utiliser URLSearchParams au lieu de FormData pour le format application/x-www-form-urlencoded @@ -56,7 +57,7 @@ class FavoritesManager { params.append('is_dir', isDir ? 'true' : 'false'); params.append('title', title || ''); - console.log('Params créés:', { + debug('Params créés:', { path: params.get('path'), is_dir: params.get('is_dir'), title: params.get('title') @@ -74,9 +75,9 @@ class FavoritesManager { const html = await response.text(); document.getElementById('favorites-list').innerHTML = html; this.attachFavoriteButtons(); - console.log('Favori ajouté:', path); + debug('Favori ajouté:', path); } else if (response.status === 409) { - console.log('Déjà en favoris'); + debug('Déjà en favoris'); } else { const errorText = await response.text(); console.error('Erreur ajout favori:', response.status, response.statusText, errorText); @@ -103,7 +104,7 @@ class FavoritesManager { const html = await response.text(); document.getElementById('favorites-list').innerHTML = html; this.attachFavoriteButtons(); - console.log('Favori retiré:', path); + debug('Favori retiré:', path); } else { console.error('Erreur retrait favori:', response.statusText); } @@ -130,96 +131,95 @@ class FavoritesManager { } attachFavoriteButtons() { - console.log('attachFavoriteButtons: Début...'); - + debug('attachFavoriteButtons: Début...'); + + // Supprimer tous les boutons favoris existants pour les recréer avec le bon état + document.querySelectorAll('.add-to-favorites').forEach(btn => btn.remove()); + // Ajouter des boutons étoile aux éléments du file tree this.getFavoritesPaths().then(favoritePaths => { - console.log('Chemins favoris:', favoritePaths); - + debug('Chemins favoris:', favoritePaths); + // Dossiers const folderHeaders = document.querySelectorAll('.folder-header'); - console.log('Nombre de folder-header trouvés:', folderHeaders.length); - + debug('Nombre de folder-header trouvés:', folderHeaders.length); + folderHeaders.forEach(header => { - if (!header.querySelector('.add-to-favorites')) { - const folderItem = header.closest('.folder-item'); - const path = folderItem?.getAttribute('data-path'); - - console.log('Dossier trouvé:', path); - - if (path) { - const button = document.createElement('button'); - button.className = 'add-to-favorites'; - button.innerHTML = '⭐'; - button.title = 'Ajouter aux favoris'; - - // Extraire le nom avant d'ajouter le bouton - const name = header.querySelector('.folder-name')?.textContent?.trim() || path.split('/').pop(); - + const folderItem = header.closest('.folder-item'); + const path = folderItem?.getAttribute('data-path'); + + debug('Dossier trouvé:', path); + + if (path) { + const button = document.createElement('button'); + button.className = 'add-to-favorites'; + button.innerHTML = '⭐'; + button.title = 'Ajouter aux favoris'; + + // Extraire le nom avant d'ajouter le bouton + const name = header.querySelector('.folder-name')?.textContent?.trim() || path.split('/').pop(); + + button.onclick = (e) => { + e.stopPropagation(); + debug('Ajout dossier aux favoris:', path, name); + this.addFavorite(path, true, name); + }; + + if (favoritePaths.includes(path)) { + button.classList.add('is-favorite'); + button.title = 'Retirer des favoris'; button.onclick = (e) => { e.stopPropagation(); - console.log('Ajout dossier aux favoris:', path, name); - this.addFavorite(path, true, name); + debug('Retrait dossier des favoris:', path); + this.removeFavorite(path); }; - - if (favoritePaths.includes(path)) { - button.classList.add('is-favorite'); - button.title = 'Retirer des favoris'; - button.onclick = (e) => { - e.stopPropagation(); - console.log('Retrait dossier des favoris:', path); - this.removeFavorite(path); - }; - } - - header.appendChild(button); } + + header.appendChild(button); } }); // Fichiers const fileItems = document.querySelectorAll('.file-item'); - console.log('Nombre de file-item trouvés:', fileItems.length); - + debug('Nombre de file-item trouvés:', fileItems.length); + fileItems.forEach(fileItem => { - if (!fileItem.querySelector('.add-to-favorites')) { - const path = fileItem.getAttribute('data-path'); - - console.log('Fichier trouvé:', path); - - if (path) { - const button = document.createElement('button'); - button.className = 'add-to-favorites'; - button.innerHTML = '⭐'; - button.title = 'Ajouter aux favoris'; - - // Extraire le nom avant d'ajouter le bouton - const name = fileItem.textContent.trim().replace('📄', '').trim().replace('.md', ''); - + const path = fileItem.getAttribute('data-path'); + + debug('Fichier trouvé:', path); + + if (path) { + const button = document.createElement('button'); + button.className = 'add-to-favorites'; + button.innerHTML = '⭐'; + button.title = 'Ajouter aux favoris'; + + // Extraire le nom avant d'ajouter le bouton + const name = fileItem.textContent.trim().replace('📄', '').trim().replace('.md', ''); + + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + debug('Ajout fichier aux favoris:', path, name); + this.addFavorite(path, false, name); + }; + + if (favoritePaths.includes(path)) { + button.classList.add('is-favorite'); + button.title = 'Retirer des favoris'; button.onclick = (e) => { e.preventDefault(); e.stopPropagation(); - console.log('Ajout fichier aux favoris:', path, name); - this.addFavorite(path, false, name); + debug('Retrait fichier des favoris:', path); + this.removeFavorite(path); }; - - if (favoritePaths.includes(path)) { - button.classList.add('is-favorite'); - button.title = 'Retirer des favoris'; - button.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log('Retrait fichier des favoris:', path); - this.removeFavorite(path); - }; - } - - fileItem.appendChild(button); } + + fileItem.appendChild(button); } }); - - console.log('attachFavoriteButtons: Terminé'); + + debug('attachFavoriteButtons: Terminé'); }); } } diff --git a/frontend/src/file-tree.js b/frontend/src/file-tree.js index f2f886e..86b5ff2 100644 --- a/frontend/src/file-tree.js +++ b/frontend/src/file-tree.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * FileTree - Gère l'arborescence hiérarchique avec drag & drop * Utilise la délégation d'événements pour éviter les problèmes de listeners perdus @@ -12,7 +13,7 @@ class FileTree { init() { this.setupEventListeners(); - console.log('FileTree initialized with event delegation'); + debug('FileTree initialized with event delegation'); } setupEventListeners() { @@ -112,27 +113,33 @@ class FileTree { } }; - // Drag over - délégué sur les folder-headers + // Drag over - délégué sur les folder-headers et la racine this.dragOverHandler = (e) => { const folderHeader = e.target.closest('.folder-header'); - if (folderHeader) { - this.handleDragOver(e, folderHeader); + const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]'); + const target = folderHeader || rootHeader; + if (target) { + this.handleDragOver(e, target); } }; // Drag leave - délégué this.dragLeaveHandler = (e) => { const folderHeader = e.target.closest('.folder-header'); - if (folderHeader) { - this.handleDragLeave(e, folderHeader); + const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]'); + const target = folderHeader || rootHeader; + if (target) { + this.handleDragLeave(e, target); } }; // Drop - délégué this.dropHandler = (e) => { const folderHeader = e.target.closest('.folder-header'); - if (folderHeader) { - this.handleDrop(e, folderHeader); + const rootHeader = e.target.closest('.sidebar-section-header[data-section="notes"]'); + const target = folderHeader || rootHeader; + if (target) { + this.handleDrop(e, target); } }; @@ -152,7 +159,7 @@ class FileTree { // Vérifier si le swap concerne le file-tree const target = event.detail?.target; if (target && (target.id === 'file-tree' || target.closest('#file-tree'))) { - console.log('FileTree: afterSwap detected, updating attributes...'); + debug('FileTree: afterSwap detected, updating attributes...'); this.updateDraggableAttributes(); } }); @@ -162,14 +169,14 @@ class FileTree { const target = event.detail?.target; // Ignorer les swaps de statut (auto-save-status, save-status) if (target && target.id === 'file-tree') { - console.log('FileTree: oobAfterSwap detected, updating attributes...'); + debug('FileTree: oobAfterSwap detected, updating attributes...'); this.updateDraggableAttributes(); } }); // Écouter les restaurations d'historique (bouton retour du navigateur) document.body.addEventListener('htmx:historyRestore', () => { - console.log('FileTree: History restored, re-initializing event listeners...'); + debug('FileTree: History restored, re-initializing event listeners...'); // Réinitialiser complètement les event listeners après restauration de l'historique setTimeout(() => { this.setupEventListeners(); @@ -214,7 +221,7 @@ class FileTree { this.draggedPath = path; this.draggedType = type; - console.log('Drag start:', { type, path, name }); + debug('Drag start:', { type, path, name }); } handleDragEnd(e) { @@ -242,20 +249,23 @@ class FileTree { this.draggedType = null; } - handleDragOver(e, folderHeader) { + handleDragOver(e, target) { e.preventDefault(); e.stopPropagation(); - const folderItem = folderHeader.closest('.folder-item'); - if (!folderItem) return; + // Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header) + const isRoot = target.classList.contains('sidebar-section-header'); + const targetElement = isRoot ? target : target.closest('.folder-item'); - const targetPath = folderItem.dataset.path; + if (!targetElement) return; + + const targetPath = targetElement.dataset.path; // Empêcher de déplacer un dossier dans lui-même ou dans ses enfants if (this.draggedType === 'folder' && this.draggedPath) { if (targetPath === this.draggedPath || targetPath.startsWith(this.draggedPath + '/')) { e.dataTransfer.dropEffect = 'none'; - folderItem.classList.remove('drag-over'); + targetElement.classList.remove('drag-over'); this.removeDestinationIndicator(); return; } @@ -263,34 +273,37 @@ class FileTree { e.dataTransfer.dropEffect = 'move'; - if (folderItem && !folderItem.classList.contains('drag-over')) { - // Retirer la classe des autres dossiers - document.querySelectorAll('.folder-item.drag-over').forEach(f => { - if (f !== folderItem) { + if (targetElement && !targetElement.classList.contains('drag-over')) { + // Retirer la classe des autres dossiers et de la racine + document.querySelectorAll('.folder-item.drag-over, .sidebar-section-header.drag-over').forEach(f => { + if (f !== targetElement) { f.classList.remove('drag-over'); } }); - folderItem.classList.add('drag-over'); + targetElement.classList.add('drag-over'); // Afficher l'indicateur de destination - this.showDestinationIndicator(folderItem, targetPath); + this.showDestinationIndicator(targetElement, targetPath, isRoot); } } - handleDragLeave(e, folderHeader) { - const folderItem = folderHeader.closest('.folder-item'); - if (!folderItem) return; + handleDragLeave(e, target) { + // Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header) + const isRoot = target.classList.contains('sidebar-section-header'); + const targetElement = isRoot ? target : target.closest('.folder-item'); - // Vérifier que la souris a vraiment quitté le dossier - const rect = folderHeader.getBoundingClientRect(); + if (!targetElement) return; + + // Vérifier que la souris a vraiment quitté l'élément + const rect = target.getBoundingClientRect(); if (e.clientX < rect.left || e.clientX >= rect.right || e.clientY < rect.top || e.clientY >= rect.bottom) { - folderItem.classList.remove('drag-over'); + targetElement.classList.remove('drag-over'); this.removeDestinationIndicator(); } } - showDestinationIndicator(folderItem, targetPath) { + showDestinationIndicator(targetElement, targetPath, isRoot) { let indicator = document.getElementById('drag-destination-indicator'); if (!indicator) { indicator = document.createElement('div'); @@ -299,8 +312,7 @@ class FileTree { document.body.appendChild(indicator); } - const folderName = folderItem.querySelector('.folder-name').textContent.trim(); - const isRoot = folderItem.dataset.isRoot === 'true'; + const folderName = targetElement.querySelector('.folder-name').textContent.trim(); const displayPath = isRoot ? 'notes/' : targetPath; indicator.innerHTML = ` @@ -318,14 +330,17 @@ class FileTree { } } - handleDrop(e, folderHeader) { + handleDrop(e, target) { e.preventDefault(); e.stopPropagation(); - const folderItem = folderHeader.closest('.folder-item'); - if (!folderItem) return; + // Gérer soit un folder-header dans un folder-item, soit la racine (sidebar-section-header) + const isRoot = target.classList.contains('sidebar-section-header'); + const targetElement = isRoot ? target : target.closest('.folder-item'); - folderItem.classList.remove('drag-over'); + if (!targetElement) return; + + targetElement.classList.remove('drag-over'); // Supprimer l'indicateur de destination this.removeDestinationIndicator(); @@ -333,9 +348,9 @@ class FileTree { const sourcePath = e.dataTransfer.getData('application/note-path') || e.dataTransfer.getData('text/plain'); const sourceType = e.dataTransfer.getData('application/note-type'); - const targetFolderPath = folderItem.dataset.path; + const targetFolderPath = targetElement.dataset.path; - console.log('Drop event:', { + debug('Drop event:', { sourcePath, sourceType, targetFolderPath, @@ -364,7 +379,7 @@ class FileTree { const sourceDir = sourcePath.includes('/') ? sourcePath.substring(0, sourcePath.lastIndexOf('/')) : ''; if (sourceDir === targetFolderPath) { - console.log('Déjà dans le même dossier parent, rien à faire'); + debug('Déjà dans le même dossier parent, rien à faire'); return; } @@ -377,12 +392,12 @@ class FileTree { // Si targetFolderPath est vide (racine), ne pas ajouter de slash const destinationPath = targetFolderPath === '' ? itemName : targetFolderPath + '/' + itemName; - console.log(`Déplacement: ${sourcePath} → ${destinationPath}`); + debug(`Déplacement: ${sourcePath} → ${destinationPath}`); this.moveFile(sourcePath, destinationPath); } async moveFile(sourcePath, destinationPath) { - console.log('moveFile called:', { sourcePath, destinationPath }); + debug('moveFile called:', { sourcePath, destinationPath }); try { // Utiliser htmx.ajax() au lieu de fetch() manuel @@ -392,7 +407,7 @@ class FileTree { values: { source: sourcePath, destination: destinationPath }, swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob }).then(() => { - console.log(`Fichier déplacé: ${sourcePath} -> ${destinationPath}`); + debug(`Fichier déplacé: ${sourcePath} -> ${destinationPath}`); }).catch((error) => { console.error('Erreur lors du déplacement:', error); alert('Erreur lors du déplacement du fichier'); @@ -504,7 +519,7 @@ window.handleNewFolder = async function(event) { swap: 'none' // On ne swap rien directement, le serveur utilise hx-swap-oob }).then(() => { window.hideNewFolderModal(); - console.log(`Dossier créé: ${folderName}`); + debug(`Dossier créé: ${folderName}`); }).catch((error) => { console.error('Erreur lors de la création du dossier:', error); alert('Erreur lors de la création du dossier'); @@ -722,7 +737,7 @@ class SelectionManager { } }); - console.log(`${paths.length} élément(s) supprimé(s)`); + debug(`${paths.length} élément(s) supprimé(s)`); // Fermer la modale this.hideDeleteConfirmationModal(); diff --git a/frontend/src/font-manager.js b/frontend/src/font-manager.js index 37194c7..e7ecce9 100644 --- a/frontend/src/font-manager.js +++ b/frontend/src/font-manager.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * Font Manager - Gère le changement de polices */ @@ -67,7 +68,7 @@ class FontManager { const savedSize = localStorage.getItem('fontSize') || 'medium'; this.applyFontSize(savedSize); - console.log('FontManager initialized with font:', savedFont, 'size:', savedSize); + debug('FontManager initialized with font:', savedFont, 'size:', savedSize); } applyFont(fontId) { @@ -88,7 +89,7 @@ class FontManager { // Sauvegarder le choix localStorage.setItem('selectedFont', fontId); - console.log('Police appliquée:', font.name); + debug('Police appliquée:', font.name); } applyFontSize(sizeId) { @@ -109,7 +110,7 @@ class FontManager { // Sauvegarder le choix localStorage.setItem('fontSize', sizeId); - console.log('Taille de police appliquée:', sizeId, size); + debug('Taille de police appliquée:', sizeId, size); } getCurrentSize() { @@ -130,7 +131,7 @@ class FontManager { link.href = `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`; document.head.appendChild(link); - console.log('Google Font chargée:', fontParam); + debug('Google Font chargée:', fontParam); } getCurrentFont() { diff --git a/frontend/src/keyboard-shortcuts.js b/frontend/src/keyboard-shortcuts.js index 22d0e4c..8a81fa6 100644 --- a/frontend/src/keyboard-shortcuts.js +++ b/frontend/src/keyboard-shortcuts.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * Keyboard Shortcuts Manager - Gère tous les raccourcis clavier de l'application */ @@ -25,7 +26,7 @@ class KeyboardShortcutsManager { this.handleKeydown(event); }); - console.log('Keyboard shortcuts initialized:', this.shortcuts.length, 'shortcuts'); + debug('Keyboard shortcuts initialized:', this.shortcuts.length, 'shortcuts'); } handleKeydown(event) { @@ -59,13 +60,13 @@ class KeyboardShortcutsManager { if (searchInput) { searchInput.focus(); searchInput.select(); - console.log('Search opened via Ctrl+K'); + debug('Search opened via Ctrl+K'); } } saveNote() { // Déclencher la sauvegarde de la note (géré par CodeMirror) - console.log('Save triggered via Ctrl+S'); + debug('Save triggered via Ctrl+S'); // La sauvegarde est déjà gérée dans editor.js } @@ -74,14 +75,14 @@ class KeyboardShortcutsManager { const dailyBtn = document.querySelector('button[hx-get="/api/daily/today"]'); if (dailyBtn) { dailyBtn.click(); - console.log('Daily note opened via Ctrl+D'); + debug('Daily note opened via Ctrl+D'); } } createNewNote() { if (typeof showNewNoteModal === 'function') { showNewNoteModal(); - console.log('New note modal opened via Ctrl+N'); + debug('New note modal opened via Ctrl+N'); } } @@ -89,35 +90,35 @@ class KeyboardShortcutsManager { const homeBtn = document.querySelector('button[hx-get="/api/home"]'); if (homeBtn) { homeBtn.click(); - console.log('Home opened via Ctrl+H'); + debug('Home opened via Ctrl+H'); } } toggleSidebar() { if (typeof toggleSidebar === 'function') { toggleSidebar(); - console.log('Sidebar toggled via Ctrl+B'); + debug('Sidebar toggled via Ctrl+B'); } } openSettings() { if (typeof openThemeModal === 'function') { openThemeModal(); - console.log('Settings opened via Ctrl+,'); + debug('Settings opened via Ctrl+,'); } } togglePreview() { if (typeof togglePreview === 'function') { togglePreview(); - console.log('Preview toggled via Ctrl+/'); + debug('Preview toggled via Ctrl+/'); } } createNewFolder() { if (typeof showNewFolderModal === 'function') { showNewFolderModal(); - console.log('New folder modal opened via Ctrl+Shift+F'); + debug('New folder modal opened via Ctrl+Shift+F'); } } @@ -147,7 +148,7 @@ class KeyboardShortcutsManager { } } - console.log('Escape pressed'); + debug('Escape pressed'); } getShortcuts() { diff --git a/frontend/src/link-inserter.js b/frontend/src/link-inserter.js index 19306e5..0d236e5 100644 --- a/frontend/src/link-inserter.js +++ b/frontend/src/link-inserter.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * LinkInserter - Modal de recherche pour insérer des liens vers d'autres notes * Intégré dans l'éditeur CodeMirror 6 @@ -137,7 +138,7 @@ class LinkInserter { } handleKeyNavigation(event) { - console.log('[LinkInserter] Key pressed:', event.key, 'Results:', this.results.length); + debug('[LinkInserter] Key pressed:', event.key, 'Results:', this.results.length); if (this.results.length === 0) { if (event.key === 'Escape') { @@ -149,27 +150,27 @@ class LinkInserter { switch (event.key) { case 'ArrowDown': - console.log('[LinkInserter] Arrow Down - moving to index:', this.selectedIndex + 1); + debug('[LinkInserter] Arrow Down - moving to index:', this.selectedIndex + 1); event.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1); this.updateSelection(); break; case 'ArrowUp': - console.log('[LinkInserter] Arrow Up - moving to index:', this.selectedIndex - 1); + debug('[LinkInserter] Arrow Up - moving to index:', this.selectedIndex - 1); event.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, 0); this.updateSelection(); break; case 'Enter': - console.log('[LinkInserter] Enter pressed - calling selectResult()'); + debug('[LinkInserter] Enter pressed - calling selectResult()'); event.preventDefault(); this.selectResult(); break; case 'Escape': - console.log('[LinkInserter] Escape pressed - closing modal'); + debug('[LinkInserter] Escape pressed - closing modal'); event.preventDefault(); this.close(); break; @@ -189,7 +190,7 @@ class LinkInserter { } selectResult() { - console.log('[LinkInserter] selectResult called, results:', this.results.length); + debug('[LinkInserter] selectResult called, results:', this.results.length); if (this.results.length === 0) { console.warn('[LinkInserter] No results to select'); @@ -197,11 +198,11 @@ class LinkInserter { } const selected = this.results[this.selectedIndex]; - console.log('[LinkInserter] Selected:', selected); - console.log('[LinkInserter] Callback exists:', !!this.callback); + debug('[LinkInserter] Selected:', selected); + debug('[LinkInserter] Callback exists:', !!this.callback); if (selected && this.callback) { - console.log('[LinkInserter] Calling callback with:', { title: selected.title, path: selected.path }); + debug('[LinkInserter] Calling callback with:', { title: selected.title, path: selected.path }); // Sauvegarder le callback localement avant de fermer const callback = this.callback; @@ -211,7 +212,7 @@ class LinkInserter { // Puis appeler le callback après un petit délai pour que le modal se ferme proprement setTimeout(() => { - console.log('[LinkInserter] Executing callback now...'); + debug('[LinkInserter] Executing callback now...'); callback({ title: selected.title, path: selected.path @@ -254,7 +255,7 @@ class LinkInserter { // Click handler item.addEventListener('click', (e) => { - console.log('[LinkInserter] Item clicked, index:', index); + debug('[LinkInserter] Item clicked, index:', index); e.preventDefault(); e.stopPropagation(); this.selectedIndex = index; @@ -336,7 +337,7 @@ class LinkInserter { * @param {Function} options.onSelect - Callback appelé avec {title, path} */ open({ editorView, onSelect }) { - console.log('[LinkInserter] open() called with callback:', !!onSelect); + debug('[LinkInserter] open() called with callback:', !!onSelect); if (this.isOpen) return; diff --git a/frontend/src/search.js b/frontend/src/search.js index f0d6cdf..965cc73 100644 --- a/frontend/src/search.js +++ b/frontend/src/search.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * SearchModal - Système de recherche modale avec raccourcis clavier * Inspiré des Command Palettes modernes (VSCode, Notion, etc.) diff --git a/frontend/src/sidebar-sections.js b/frontend/src/sidebar-sections.js index 3970baf..7ce6df6 100644 --- a/frontend/src/sidebar-sections.js +++ b/frontend/src/sidebar-sections.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * SidebarSections - Gère les sections rétractables de la sidebar * Permet de replier/déplier les favoris et le répertoire de notes @@ -12,7 +13,7 @@ class SidebarSections { } init() { - console.log('SidebarSections: Initialisation...'); + debug('SidebarSections: Initialisation...'); // Restaurer l'état sauvegardé au démarrage this.restoreStates(); @@ -22,12 +23,12 @@ class SidebarSections { const targetId = event.detail?.target?.id; if (targetId === 'favorites-list') { - console.log('Favoris rechargés, restauration de l\'état...'); + debug('Favoris rechargés, restauration de l\'état...'); setTimeout(() => this.restoreSectionState('favorites'), 50); } if (targetId === 'file-tree') { - console.log('File-tree rechargé, restauration de l\'état...'); + debug('File-tree rechargé, restauration de l\'état...'); setTimeout(() => this.restoreSectionState('notes'), 50); } }); @@ -38,14 +39,14 @@ class SidebarSections { // Ne restaurer l'état que pour les swaps du file-tree complet // Les swaps de statut (auto-save-status) ne doivent pas déclencher la restauration if (targetId === 'file-tree') { - console.log('File-tree rechargé (oob), restauration de l\'état...'); + debug('File-tree rechargé (oob), restauration de l\'état...'); setTimeout(() => this.restoreSectionState('notes'), 50); } }); // Écouter les restaurations d'historique (bouton retour du navigateur) document.body.addEventListener('htmx:historyRestore', () => { - console.log('SidebarSections: History restored, restoring section states...'); + debug('SidebarSections: History restored, restoring section states...'); // Restaurer les états des sections après restauration de l'historique setTimeout(() => { this.restoreSectionState('favorites'); @@ -53,7 +54,7 @@ class SidebarSections { }, 100); }); - console.log('SidebarSections: Initialisé'); + debug('SidebarSections: Initialisé'); } /** @@ -75,7 +76,7 @@ class SidebarSections { if (!section) return; localStorage.setItem(section.key, isExpanded.toString()); - console.log(`État sauvegardé: ${sectionName} = ${isExpanded}`); + debug(`État sauvegardé: ${sectionName} = ${isExpanded}`); } /** @@ -113,7 +114,7 @@ class SidebarSections { } this.setSectionState(sectionName, newState); - console.log(`Section ${sectionName} ${newState ? 'ouverte' : 'fermée'}`); + debug(`Section ${sectionName} ${newState ? 'ouverte' : 'fermée'}`); } /** @@ -148,7 +149,7 @@ class SidebarSections { } } - console.log(`État restauré: ${sectionName} = ${isExpanded ? 'ouvert' : 'fermé'}`); + debug(`État restauré: ${sectionName} = ${isExpanded ? 'ouvert' : 'fermé'}`); } /** diff --git a/frontend/src/theme-manager.js b/frontend/src/theme-manager.js index 4fbfd0f..712ddd9 100644 --- a/frontend/src/theme-manager.js +++ b/frontend/src/theme-manager.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * ThemeManager - Gère le système de thèmes de l'application * Permet de changer entre différents thèmes et persiste le choix dans localStorage @@ -70,7 +71,7 @@ class ThemeManager { } }); - console.log('ThemeManager initialized with theme:', this.currentTheme); + debug('ThemeManager initialized with theme:', this.currentTheme); } loadTheme() { @@ -91,7 +92,7 @@ class ThemeManager { // Mettre à jour les cartes de thème si la modale est ouverte this.updateThemeCards(); - console.log('Theme applied:', themeId); + debug('Theme applied:', themeId); } openThemeModal() { @@ -163,7 +164,7 @@ window.selectTheme = function(themeId) { }; window.switchSettingsTab = function(tabName) { - console.log('Switching to tab:', tabName); + debug('Switching to tab:', tabName); // Désactiver tous les onglets const tabs = document.querySelectorAll('.settings-tab'); @@ -191,7 +192,7 @@ window.switchSettingsTab = function(tabName) { const section = document.getElementById(sectionId); if (section) { section.style.display = 'block'; - console.log('Showing section:', sectionId); + debug('Showing section:', sectionId); } else { console.error('Section not found:', sectionId); } diff --git a/frontend/src/ui.js b/frontend/src/ui.js index 435d225..c0658e2 100644 --- a/frontend/src/ui.js +++ b/frontend/src/ui.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; // Fonction pour détecter si on est sur mobile function isMobileDevice() { return window.innerWidth <= 768; diff --git a/frontend/src/vim-mode-manager.js b/frontend/src/vim-mode-manager.js index 2d262b0..a08ea2a 100644 --- a/frontend/src/vim-mode-manager.js +++ b/frontend/src/vim-mode-manager.js @@ -1,3 +1,4 @@ +import { debug, debugError } from './debug.js'; /** * Vim Mode Manager - Gère l'activation/désactivation du mode Vim dans CodeMirror */ @@ -8,7 +9,7 @@ class VimModeManager { this.vim = null; // Extension Vim de CodeMirror this.editorView = null; // Instance EditorView actuelle - console.log('VimModeManager initialized, enabled:', this.enabled); + debug('VimModeManager initialized, enabled:', this.enabled); } /** @@ -60,7 +61,7 @@ class VimModeManager { // Import dynamique du package Vim const { vim } = await import('@replit/codemirror-vim'); this.vim = vim; - console.log('✅ Vim extension loaded successfully'); + debug('✅ Vim extension loaded successfully'); return this.vim; } catch (error) { console.warn('⚠️ Vim mode is not available. The @replit/codemirror-vim package is not installed.'); @@ -118,14 +119,14 @@ if (typeof window !== 'undefined') { // Afficher un message const message = enabled ? '✅ Mode Vim activé' : '❌ Mode Vim désactivé'; - console.log(message); + debug(message); // Recharger l'éditeur actuel si il existe if (window.currentMarkdownEditor && window.currentMarkdownEditor.reloadWithVimMode) { await window.currentMarkdownEditor.reloadWithVimMode(); - console.log('Editor reloaded with Vim mode:', enabled); + debug('Editor reloaded with Vim mode:', enabled); } else { - console.log('No editor to reload. Vim mode will be applied when opening a note.'); + debug('No editor to reload. Vim mode will be applied when opening a note.'); } }; diff --git a/internal/api/handler.go b/internal/api/handler.go index eda71d8..76fb9fb 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -114,6 +114,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handleFavorites(w, r) return } + if strings.HasPrefix(path, "/api/folder/") { + h.handleFolderView(w, r) + return + } http.NotFound(w, r) } @@ -278,15 +282,17 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) { // Utiliser le template editor.html pour afficher la page d'accueil data := struct { - Filename string - Content string - IsHome bool - Backlinks []BacklinkInfo + Filename string + Content string + IsHome bool + Backlinks []BacklinkInfo + Breadcrumb template.HTML }{ - Filename: "🏠 Accueil - Index", - Content: content, - IsHome: true, - Backlinks: nil, // Pas de backlinks pour la page d'accueil + Filename: "🏠 Accueil - Index", + Content: content, + IsHome: true, + Backlinks: nil, // Pas de backlinks pour la page d'accueil + Breadcrumb: h.generateBreadcrumb(""), } err := h.templates.ExecuteTemplate(w, "editor.html", data) @@ -338,12 +344,19 @@ func (h *Handler) generateHomeMarkdown() string { // Section des notes récemment modifiées (après les favoris) h.generateRecentNotesSection(&sb) - // Titre de l'arborescence avec le nombre de notes - sb.WriteString(fmt.Sprintf("## 📂 Toutes les notes (%d)\n\n", noteCount)) + // Section de toutes les notes avec accordéon + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString(fmt.Sprintf("

📂 Toutes les notes (%d)

\n", noteCount)) + sb.WriteString("
\n") + sb.WriteString("
\n") // Générer l'arborescence en Markdown h.generateMarkdownTree(&sb, tree, 0) + sb.WriteString("
\n") + sb.WriteString("
\n") + return sb.String() } @@ -355,18 +368,24 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) { return } - sb.WriteString("## 🏷️ Tags\n\n") - sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("

🏷️ Tags

\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") for _, tc := range tags { // Créer un lien HTML discret et fonctionnel sb.WriteString(fmt.Sprintf( - `#%s %d`, + ` #%s %d`, tc.Tag, tc.Tag, tc.Count, )) sb.WriteString("\n") } + sb.WriteString("
\n") + sb.WriteString("
\n") sb.WriteString("
\n\n") } @@ -377,36 +396,42 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) { return } - sb.WriteString("## ⭐ Favoris\n\n") - sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("

⭐ Favoris

\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") for _, fav := range favorites.Items { safeID := "fav-" + strings.ReplaceAll(strings.ReplaceAll(fav.Path, "/", "-"), "\\", "-") - + if fav.IsDir { // Dossier - avec accordéon - sb.WriteString(fmt.Sprintf("
\n")) - sb.WriteString(fmt.Sprintf("
\n", safeID)) - sb.WriteString(fmt.Sprintf(" 📁\n", safeID)) - sb.WriteString(fmt.Sprintf(" %s\n", fav.Title)) - sb.WriteString(fmt.Sprintf("
\n")) - sb.WriteString(fmt.Sprintf("
\n", safeID)) - + sb.WriteString(fmt.Sprintf("
\n")) + sb.WriteString(fmt.Sprintf("
\n", safeID)) + sb.WriteString(fmt.Sprintf(" 📁\n", safeID)) + sb.WriteString(fmt.Sprintf(" %s\n", fav.Title)) + sb.WriteString(fmt.Sprintf("
\n")) + sb.WriteString(fmt.Sprintf("
\n", safeID)) + // Lister le contenu du dossier - h.generateFavoriteFolderContent(sb, fav.Path, 2) - - sb.WriteString(fmt.Sprintf("
\n")) - sb.WriteString(fmt.Sprintf("
\n")) + h.generateFavoriteFolderContent(sb, fav.Path, 3) + + sb.WriteString(fmt.Sprintf("
\n")) + sb.WriteString(fmt.Sprintf("
\n")) } else { // Fichier - sb.WriteString(fmt.Sprintf("
\n")) - sb.WriteString(fmt.Sprintf(" ", fav.Path)) + sb.WriteString(fmt.Sprintf(" \n")) + sb.WriteString(fmt.Sprintf("
\n")) } } + sb.WriteString("
\n") + sb.WriteString("
\n") sb.WriteString("
\n\n") } @@ -418,8 +443,12 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) { return } - sb.WriteString("## 🕒 Récemment modifiés\n\n") - sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("
\n") + sb.WriteString("

🕒 Récemment modifiés

\n") + sb.WriteString("
\n") + sb.WriteString(" \n") sb.WriteString("
\n\n") } @@ -788,15 +819,17 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename backlinkData := h.buildBacklinkData(backlinks) data := struct { - Filename string - Content string - IsHome bool - Backlinks []BacklinkInfo + Filename string + Content string + IsHome bool + Backlinks []BacklinkInfo + Breadcrumb template.HTML }{ - Filename: filename, - Content: string(content), - IsHome: false, - Backlinks: backlinkData, + Filename: filename, + Content: string(content), + IsHome: false, + Backlinks: backlinkData, + Breadcrumb: h.generateBreadcrumb(filename), } err = h.templates.ExecuteTemplate(w, "editor.html", data) @@ -1264,3 +1297,190 @@ func (h *Handler) buildBacklinkData(paths []string) []BacklinkInfo { return result } + +// handleFolderView affiche le contenu d'un dossier +func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) + return + } + + // Si ce n'est pas une requête HTMX, rediriger vers la page principale + if r.Header.Get("HX-Request") == "" { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Extraire le chemin du dossier depuis l'URL + folderPath := strings.TrimPrefix(r.URL.Path, "/api/folder/") + folderPath = strings.TrimPrefix(folderPath, "/") + + // Sécurité : vérifier le chemin + cleanPath := filepath.Clean(folderPath) + if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) { + http.Error(w, "Chemin invalide", http.StatusBadRequest) + return + } + + // Construire le chemin absolu + absPath := filepath.Join(h.notesDir, cleanPath) + + // Vérifier que c'est bien un dossier + info, err := os.Stat(absPath) + if err != nil || !info.IsDir() { + http.Error(w, "Dossier non trouvé", http.StatusNotFound) + return + } + + // Générer le contenu de la page + content := h.generateFolderViewMarkdown(cleanPath) + + // Utiliser le template editor.html + data := struct { + Filename string + Content string + IsHome bool + Backlinks []BacklinkInfo + Breadcrumb template.HTML + }{ + Filename: cleanPath, + Content: content, + IsHome: true, // Pas d'édition pour une vue de dossier + Backlinks: nil, + Breadcrumb: h.generateBreadcrumb(cleanPath), + } + + err = h.templates.ExecuteTemplate(w, "editor.html", data) + if err != nil { + h.logger.Printf("Erreur d'exécution du template folder view: %v", err) + http.Error(w, "Erreur interne", http.StatusInternalServerError) + } +} + +// generateBreadcrumb génère un fil d'Ariane HTML cliquable +func (h *Handler) generateBreadcrumb(path string) template.HTML { + if path == "" { + return template.HTML(`📁 Racine`) + } + + parts := strings.Split(filepath.ToSlash(path), "/") + var sb strings.Builder + + sb.WriteString(``) + + // Lien racine + sb.WriteString(`📁 Racine`) + + // Construire les liens pour chaque partie + currentPath := "" + for i, part := range parts { + sb.WriteString(` `) + + if currentPath == "" { + currentPath = part + } else { + currentPath = currentPath + "/" + part + } + + // Le dernier élément (fichier) n'est pas cliquable + if i == len(parts)-1 && strings.HasSuffix(part, ".md") { + // C'est un fichier, pas cliquable + displayName := strings.TrimSuffix(part, ".md") + sb.WriteString(fmt.Sprintf(`%s`, displayName)) + } else { + // C'est un dossier, cliquable + sb.WriteString(fmt.Sprintf( + `📂 %s`, + currentPath, part, + )) + } + } + + sb.WriteString(``) + return template.HTML(sb.String()) +} + +// generateFolderViewMarkdown génère le contenu Markdown pour l'affichage d'un dossier +func (h *Handler) generateFolderViewMarkdown(folderPath string) string { + var sb strings.Builder + + // En-tête + if folderPath == "" { + sb.WriteString("# 📁 Racine\n\n") + } else { + folderName := filepath.Base(folderPath) + sb.WriteString(fmt.Sprintf("# 📂 %s\n\n", folderName)) + } + + sb.WriteString("_Contenu du dossier_\n\n") + + // Lister le contenu + absPath := filepath.Join(h.notesDir, folderPath) + entries, err := os.ReadDir(absPath) + if err != nil { + sb.WriteString("❌ Erreur lors de la lecture du dossier\n") + return sb.String() + } + + // Séparer dossiers et fichiers + var folders []os.DirEntry + var files []os.DirEntry + + for _, entry := range entries { + // Ignorer les fichiers cachés + if strings.HasPrefix(entry.Name(), ".") { + continue + } + + if entry.IsDir() { + folders = append(folders, entry) + } else if strings.HasSuffix(entry.Name(), ".md") { + files = append(files, entry) + } + } + + // Afficher les dossiers + if len(folders) > 0 { + sb.WriteString("## 📁 Dossiers\n\n") + sb.WriteString("
\n") + for _, folder := range folders { + subPath := filepath.Join(folderPath, folder.Name()) + sb.WriteString(fmt.Sprintf( + ``, + filepath.ToSlash(subPath), folder.Name(), + )) + sb.WriteString("\n") + } + sb.WriteString("
\n\n") + } + + // Afficher les fichiers + if len(files) > 0 { + sb.WriteString(fmt.Sprintf("## 📄 Notes (%d)\n\n", len(files))) + sb.WriteString("
\n") + for _, file := range files { + filePath := filepath.Join(folderPath, file.Name()) + displayName := strings.TrimSuffix(file.Name(), ".md") + + // Lire le titre du front matter si possible + fullPath := filepath.Join(h.notesDir, filePath) + fm, _, err := indexer.ExtractFrontMatterAndBody(fullPath) + if err == nil && fm.Title != "" { + displayName = fm.Title + } + + sb.WriteString(fmt.Sprintf( + ``, + filepath.ToSlash(filePath), displayName, + )) + sb.WriteString("\n") + } + sb.WriteString("
\n\n") + } + + if len(folders) == 0 && len(files) == 0 { + sb.WriteString("_Ce dossier est vide_\n") + } + + return sb.String() +} diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go index 4d65d5e..6955848 100644 --- a/internal/indexer/indexer.go +++ b/internal/indexer/indexer.go @@ -33,6 +33,7 @@ type Document struct { LastModified string Body string Summary string + Links []string // Liens Markdown vers d'autres notes lowerTitle string lowerBody string @@ -115,11 +116,11 @@ func (i *Indexer) Load(root string) error { indexed[tag] = list } - // Build backlinks index + // Build backlinks index from Markdown links backlinksMap := make(map[string][]string) for sourcePath, doc := range documents { - links := extractInternalLinks(doc.Body) - for _, targetPath := range links { + // Use the Links field which contains extracted Markdown links + for _, targetPath := range doc.Links { // Add sourcePath to the backlinks of targetPath if _, ok := backlinksMap[targetPath]; !ok { backlinksMap[targetPath] = make([]string, 0) @@ -169,6 +170,45 @@ func normalizeTags(tags []string) []string { return result } +// extractMarkdownLinks extrait tous les liens Markdown du body +// Format : [texte](chemin/vers/note.md) +// Retourne une liste de chemins vers d'autres notes +func extractMarkdownLinks(body string) []string { + // Regex pour capturer [texte](chemin.md) + // Groupe 1 : texte du lien, Groupe 2 : chemin + re := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+\.md)\)`) + matches := re.FindAllStringSubmatch(body, -1) + + links := make([]string, 0, len(matches)) + seen := make(map[string]bool) // Éviter les doublons + + for _, match := range matches { + if len(match) < 3 { + continue + } + + linkPath := strings.TrimSpace(match[2]) + + // Ignorer les URLs absolues (http://, https://, //) + if strings.HasPrefix(linkPath, "http://") || + strings.HasPrefix(linkPath, "https://") || + strings.HasPrefix(linkPath, "//") { + continue + } + + // Normaliser le chemin (convertir \ en / pour Windows) + linkPath = filepath.ToSlash(linkPath) + + // Éviter les doublons + if !seen[linkPath] { + seen[linkPath] = true + links = append(links, linkPath) + } + } + + return links +} + func buildDocument(path string, fm FullFrontMatter, body string, tags []string) *Document { title := strings.TrimSpace(fm.Title) if title == "" { @@ -176,6 +216,7 @@ func buildDocument(path string, fm FullFrontMatter, body string, tags []string) } summary := buildSummary(body) + links := extractMarkdownLinks(body) lowerTags := make([]string, len(tags)) for idx, tag := range tags { @@ -190,6 +231,7 @@ func buildDocument(path string, fm FullFrontMatter, body string, tags []string) LastModified: strings.TrimSpace(fm.LastModified), Body: body, Summary: summary, + Links: links, lowerTitle: strings.ToLower(title), lowerBody: strings.ToLower(body), lowerTags: lowerTags, diff --git a/notes/.favorites.json b/notes/.favorites.json index db273a3..838b57e 100644 --- a/notes/.favorites.json +++ b/notes/.favorites.json @@ -14,54 +14,33 @@ "added_at": "2025-11-11T14:20:49.985321698+01:00", "order": 1 }, - { - "path": "research/tech/websockets.md", - "is_dir": false, - "title": "websockets", - "added_at": "2025-11-11T14:20:55.347335695+01:00", - "order": 2 - }, - { - "path": "tasks/backlog.md", - "is_dir": false, - "title": "backlog", - "added_at": "2025-11-11T14:20:57.762787363+01:00", - "order": 3 - }, { "path": "ideas/client-feedback.md", "is_dir": false, "title": "client-feedback", "added_at": "2025-11-11T14:22:16.497953232+01:00", - "order": 4 + "order": 2 }, { "path": "ideas/collaboration.md", "is_dir": false, "title": "collaboration", "added_at": "2025-11-11T14:22:18.012032002+01:00", - "order": 5 + "order": 3 }, { "path": "ideas/mobile-app.md", "is_dir": false, "title": "mobile-app", "added_at": "2025-11-11T14:22:19.048311608+01:00", - "order": 6 + "order": 4 }, { - "path": "meetings/2025", + "path": "documentation/guides", "is_dir": true, - "title": "2025", - "added_at": "2025-11-11T14:22:21.531283601+01:00", - "order": 7 - }, - { - "path": "meetings/outscale.md", - "is_dir": false, - "title": "outscale", - "added_at": "2025-11-11T14:22:22.519332518+01:00", - "order": 8 + "title": "guides", + "added_at": "2025-11-12T18:18:20.53353467+01:00", + "order": 5 } ] } \ No newline at end of file diff --git a/notes/tasks/bugs.md b/notes/bugs.md similarity index 100% rename from notes/tasks/bugs.md rename to notes/bugs.md diff --git a/notes/meetings/2025/sprint-planning.md b/notes/meetings/2025/sprint-planning.md index 2c10444..0db4487 100644 --- a/notes/meetings/2025/sprint-planning.md +++ b/notes/meetings/2025/sprint-planning.md @@ -1,7 +1,7 @@ --- title: Sprint Planning January date: 10-11-2025 -last_modified: 12-11-2025:10:26 +last_modified: 12-11-2025:19:55 tags: - meeting - planning @@ -22,6 +22,8 @@ tags: ## Vélocité +Poppy Test + 20 story points pour ce sprint. ## Risques @@ -30,4 +32,4 @@ tags: - Tests E2E à mettre en place -/il +C'est une note pour être sur que c'est bien la dernière note éditée. \ No newline at end of file diff --git a/notes/scratch.md b/notes/scratch.md index 605beb0..62fcaf2 100644 --- a/notes/scratch.md +++ b/notes/scratch.md @@ -1,7 +1,7 @@ --- title: Scratch Pad date: 10-11-2025 -last_modified: 10-11-2025:20:05 +last_modified: 12-11-2025:20:13 tags: - default --- @@ -26,3 +26,4 @@ const hello = () => { console.log('Hello World'); }; ``` +API Design \ No newline at end of file diff --git a/notes/un-dossier/test/Poppy-test.md b/notes/un-dossier/test/Poppy-test.md index 6e45462..4bdc302 100644 --- a/notes/un-dossier/test/Poppy-test.md +++ b/notes/un-dossier/test/Poppy-test.md @@ -1,7 +1,7 @@ --- title: Poppy Test date: 10-11-2025 -last_modified: 11-11-2025:18:41 +last_modified: 12-11-2025:20:16 --- # Poppy Test @@ -14,4 +14,9 @@ On verra bien à la fin. Poppy Test -Typography Research \ No newline at end of file +Typography Research + + +[UI Design Inspiration](research/design/ui-inspiration.md) + +/i \ No newline at end of file diff --git a/static/theme.css b/static/theme.css index 6d75243..811d3f5 100644 --- a/static/theme.css +++ b/static/theme.css @@ -1261,6 +1261,22 @@ body, html { box-shadow: var(--shadow-glow); } +/* Style pour la racine en drag-over */ +.sidebar-section-header.drag-over { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important; + color: white !important; + box-shadow: var(--shadow-glow); + border: 2px solid var(--accent-primary); + border-radius: var(--radius-md); + animation: pulse 1s ease-in-out infinite; +} + +.sidebar-section-header.drag-over .folder-name, +.sidebar-section-header.drag-over .root-hint, +.sidebar-section-header.drag-over .folder-icon { + color: white !important; +} + /* Indicateur de destination pendant le drag */ .drag-destination-indicator { position: fixed; @@ -3400,25 +3416,14 @@ body, html { overflow-y: auto; padding: var(--spacing-sm); margin-bottom: var(--spacing-lg); - scrollbar-width: thin; - scrollbar-color: var(--border-primary) transparent; + /* Masquer la scrollbar mais garder le scroll */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE et Edge */ } +/* Masquer la scrollbar pour Chrome, Safari et Opera */ .recent-notes-container::-webkit-scrollbar { - width: 6px; -} - -.recent-notes-container::-webkit-scrollbar-track { - background: transparent; -} - -.recent-notes-container::-webkit-scrollbar-thumb { - background: var(--border-primary); - border-radius: 3px; -} - -.recent-notes-container::-webkit-scrollbar-thumb:hover { - background: var(--border-secondary); + display: none; } .recent-note-card { @@ -3482,3 +3487,140 @@ body, html { -webkit-box-orient: vertical; overflow: hidden; } + +/* ======================================== + Home Page Sections with Accordions + ======================================== */ +.home-section { + margin-bottom: var(--spacing-lg); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + background: var(--bg-secondary); + overflow: hidden; + transition: all var(--transition-fast); +} + +.home-section:hover { + border-color: var(--border-secondary); + box-shadow: var(--shadow-sm); +} + +.home-section-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + cursor: pointer; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); + transition: all var(--transition-fast); + user-select: none; +} + +.home-section-header:hover { + background: var(--bg-secondary); +} + +.home-section-header:active { + background: var(--bg-primary); +} + +.home-section-title { + margin: 0; + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + flex: 1; +} + +.home-section-content { + padding: var(--spacing-lg); + overflow-y: auto; + transition: all var(--transition-medium); + max-height: 600px; + /* Masquer la scrollbar mais garder le scroll */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE et Edge */ +} + +/* Masquer la scrollbar pour Chrome, Safari et Opera */ +.home-section-content::-webkit-scrollbar { + display: none; +} + +/* Adjust nested containers for accordion layout */ +.home-section .recent-notes-container { + padding: 0; + margin-bottom: 0; +} + +/* ======================================== + Breadcrumb Navigation + ======================================== */ +.breadcrumb { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 0.25rem; + font-size: 0.95rem; +} + +.breadcrumb-link { + color: var(--accent-primary); + text-decoration: none; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.breadcrumb-link:hover { + background: var(--bg-tertiary); + color: var(--accent-secondary); +} + +.breadcrumb-separator { + color: var(--text-muted); + font-size: 0.9rem; +} + +/* ======================================== + Folder and File Lists (Folder View) + ======================================== */ +/* Styles spécifiques pour la vue de dossier dans l'éditeur */ +#editor-content .folder-list, +#editor-content .file-list { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); +} + +#editor-content .folder-list .folder-item, +#editor-content .file-list .file-item { + padding: var(--spacing-md); + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +#editor-content .folder-list .folder-item:hover, +#editor-content .file-list .file-item:hover { + border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); + transform: translateX(4px); +} + +#editor-content .folder-list .folder-item a, +#editor-content .file-list .file-item a { + color: var(--text-primary); + text-decoration: none; + display: block; + font-weight: 500; +} + +#editor-content .folder-list .folder-item:hover a, +#editor-content .file-list .file-item:hover a { + color: var(--accent-primary); +} + diff --git a/templates/editor.html b/templates/editor.html index 72937c3..580f49d 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -3,9 +3,17 @@
diff --git a/templates/file-tree.html b/templates/file-tree.html index 00ec649..817670c 100644 --- a/templates/file-tree.html +++ b/templates/file-tree.html @@ -1,5 +1,5 @@ -