Des tonnes de modifications notamment VIM / Couleurs / typos
This commit is contained in:
137
frontend/package-lock.json
generated
137
frontend/package-lock.json
generated
@ -13,7 +13,8 @@
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.6"
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@replit/codemirror-vim": "^6.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.2.2"
|
||||
@ -59,6 +60,18 @@
|
||||
"@lezer/common": "^0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz",
|
||||
"integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^0.20.0",
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.0",
|
||||
"@lezer/common": "^0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
|
||||
@ -84,6 +97,17 @@
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz",
|
||||
"integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
|
||||
@ -126,70 +150,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz",
|
||||
"integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
|
||||
"integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^0.20.0",
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.0",
|
||||
"@lezer/common": "^0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@codemirror/language": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
|
||||
"integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.0",
|
||||
"@lezer/common": "^0.16.0",
|
||||
"@lezer/highlight": "^0.16.0",
|
||||
"@lezer/lr": "^0.16.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@codemirror/state": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
|
||||
"integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@codemirror/view": {
|
||||
"version": "0.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
|
||||
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@lezer/common": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
|
||||
"integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@lezer/highlight": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
|
||||
"integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands/node_modules/@lezer/lr": {
|
||||
"version": "0.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
|
||||
"integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^0.16.0"
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
@ -278,33 +248,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz",
|
||||
"integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==",
|
||||
"version": "6.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
|
||||
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search/node_modules/@codemirror/state": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
|
||||
"integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/search/node_modules/@codemirror/view": {
|
||||
"version": "0.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
|
||||
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
@ -853,6 +807,19 @@
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@replit/codemirror-vim": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
|
||||
"integrity": "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@codemirror/commands": "6.x.x",
|
||||
"@codemirror/language": "6.x.x",
|
||||
"@codemirror/search": "6.x.x",
|
||||
"@codemirror/state": "6.x.x",
|
||||
"@codemirror/view": "6.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz",
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.6"
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@replit/codemirror-vim": "^6.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
93
frontend/src/daily-notes.js
Normal file
93
frontend/src/daily-notes.js
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* DailyNotes - Gère les raccourcis et interactions pour les daily notes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialise le raccourci clavier pour la note du jour
|
||||
* Ctrl/Cmd+D ouvre la note du jour
|
||||
*/
|
||||
function initDailyNotesShortcut() {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Ctrl+D (Windows/Linux) ou Cmd+D (Mac)
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'd') {
|
||||
event.preventDefault();
|
||||
|
||||
// Utiliser HTMX pour charger la note du jour
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', '/api/daily/today', {
|
||||
target: '#editor-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Daily notes shortcuts initialized (Ctrl/Cmd+D)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le calendrier et les notes récentes
|
||||
* Appelé après la création ou modification d'une daily note
|
||||
*/
|
||||
window.refreshDailyNotes = function() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const calendarUrl = `/api/daily/calendar/${year}-${month}`;
|
||||
|
||||
// Recharger le calendrier
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', calendarUrl, {
|
||||
target: '#daily-calendar-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
|
||||
// Recharger les notes récentes
|
||||
htmx.ajax('GET', '/api/daily/recent', {
|
||||
target: '#daily-recent-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Écouter les événements HTMX pour rafraîchir le calendrier
|
||||
* après sauvegarde d'une daily note
|
||||
*/
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
const target = event.detail?.target;
|
||||
|
||||
// Si on a chargé l'éditeur avec une URL contenant /api/daily/
|
||||
if (target && target.id === 'editor-container') {
|
||||
const request = event.detail?.requestConfig;
|
||||
if (request && request.path && request.path.includes('/api/daily/')) {
|
||||
// On vient de charger une daily note, pas besoin de rafraîchir
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Écouter les soumissions de formulaire pour rafraîchir
|
||||
* le calendrier après sauvegarde d'une daily note
|
||||
*/
|
||||
document.body.addEventListener('htmx:afterRequest', (event) => {
|
||||
const target = event.detail?.target;
|
||||
|
||||
// Vérifier si c'est une soumission de formulaire d'édition
|
||||
if (event.detail?.successful && target) {
|
||||
// Vérifier si le chemin sauvegardé est une daily note
|
||||
const pathInput = target.querySelector('input[name="path"]');
|
||||
if (pathInput && pathInput.value.startsWith('daily/')) {
|
||||
// Rafraîchir le calendrier et les notes récentes
|
||||
window.refreshDailyNotes();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialisation automatique
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initDailyNotesShortcut();
|
||||
});
|
||||
@ -6,6 +6,18 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
|
||||
// Import du mode Vim
|
||||
let vimExtension = null;
|
||||
(async () => {
|
||||
try {
|
||||
const { vim } = await import('@replit/codemirror-vim');
|
||||
vimExtension = vim;
|
||||
console.log('✅ Vim extension loaded and ready');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Vim extension not available:', error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* MarkdownEditor - Éditeur Markdown avec preview en temps réel
|
||||
*/
|
||||
@ -48,56 +60,88 @@ class MarkdownEditor {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialiser CodeMirror 6
|
||||
const startState = EditorState.create({
|
||||
doc: this.textarea.value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
markdown(),
|
||||
oneDark,
|
||||
keymap.of([indentWithTab]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
// Debounce la mise à jour du preview
|
||||
if (this._updateTimeout) {
|
||||
clearTimeout(this._updateTimeout);
|
||||
}
|
||||
this._updateTimeout = setTimeout(() => {
|
||||
this.updatePreview();
|
||||
}, 150);
|
||||
// Initialiser l'éditeur (avec ou sans Vim)
|
||||
this.initEditor();
|
||||
}
|
||||
|
||||
// Auto-save logic
|
||||
if (this._autoSaveTimeout) {
|
||||
clearTimeout(this._autoSaveTimeout);
|
||||
}
|
||||
this._autoSaveTimeout = setTimeout(() => {
|
||||
const form = this.textarea.closest('form');
|
||||
if (form) {
|
||||
const saveStatus = document.getElementById('auto-save-status');
|
||||
if (saveStatus) {
|
||||
saveStatus.textContent = 'Sauvegarde...';
|
||||
}
|
||||
// Synchroniser le contenu de CodeMirror vers le textarea
|
||||
this.syncToTextarea();
|
||||
form.requestSubmit();
|
||||
}
|
||||
}, 2000); // Auto-save after 2 seconds of inactivity
|
||||
getExtensions() {
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
markdown(),
|
||||
oneDark,
|
||||
keymap.of([indentWithTab]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
// Debounce la mise à jour du preview
|
||||
if (this._updateTimeout) {
|
||||
clearTimeout(this._updateTimeout);
|
||||
}
|
||||
}),
|
||||
// Keymap for Ctrl/Cmd+S
|
||||
keymap.of([{
|
||||
key: "Mod-s",
|
||||
run: () => {
|
||||
this._updateTimeout = setTimeout(() => {
|
||||
this.updatePreview();
|
||||
}, 150);
|
||||
|
||||
// Auto-save logic
|
||||
if (this._autoSaveTimeout) {
|
||||
clearTimeout(this._autoSaveTimeout);
|
||||
}
|
||||
this._autoSaveTimeout = setTimeout(() => {
|
||||
const form = this.textarea.closest('form');
|
||||
if (form) {
|
||||
const saveStatus = document.getElementById('auto-save-status');
|
||||
if (saveStatus) {
|
||||
saveStatus.textContent = 'Sauvegarde...';
|
||||
}
|
||||
// Synchroniser le contenu de CodeMirror vers le textarea
|
||||
this.syncToTextarea();
|
||||
form.requestSubmit();
|
||||
}
|
||||
return true;
|
||||
}, 2000); // Auto-save after 2 seconds of inactivity
|
||||
}
|
||||
}),
|
||||
// Keymap for Ctrl/Cmd+S
|
||||
keymap.of([{
|
||||
key: "Mod-s",
|
||||
run: () => {
|
||||
const form = this.textarea.closest('form');
|
||||
if (form) {
|
||||
// Synchroniser le contenu de CodeMirror vers le textarea
|
||||
this.syncToTextarea();
|
||||
form.requestSubmit();
|
||||
}
|
||||
}])
|
||||
]
|
||||
return true;
|
||||
}
|
||||
}])
|
||||
];
|
||||
|
||||
// Ajouter l'extension Vim si activée et disponible
|
||||
if (window.vimModeManager && window.vimModeManager.isEnabled()) {
|
||||
if (vimExtension) {
|
||||
extensions.push(vimExtension());
|
||||
console.log('✅ Vim mode enabled in editor');
|
||||
} else {
|
||||
console.warn('⚠️ Vim mode requested but extension not loaded yet');
|
||||
}
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
initEditor() {
|
||||
const currentContent = this.editorView
|
||||
? this.editorView.state.doc.toString()
|
||||
: this.textarea.value;
|
||||
|
||||
const extensions = this.getExtensions();
|
||||
|
||||
// Détruire l'ancien éditeur si il existe
|
||||
if (this.editorView) {
|
||||
this.editorView.destroy();
|
||||
}
|
||||
|
||||
// Initialiser CodeMirror 6
|
||||
const startState = EditorState.create({
|
||||
doc: currentContent,
|
||||
extensions
|
||||
});
|
||||
|
||||
this.editorView = new EditorView({
|
||||
@ -160,6 +204,13 @@ class MarkdownEditor {
|
||||
|
||||
// Initial preview update
|
||||
this.updatePreview();
|
||||
|
||||
// Initialiser les SlashCommands si ce n'est pas déjà fait
|
||||
if (this.editorView && !window.currentSlashCommands) {
|
||||
window.currentSlashCommands = new SlashCommands({
|
||||
editorView: this.editorView
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stripFrontMatter(markdownContent) {
|
||||
@ -242,6 +293,11 @@ class MarkdownEditor {
|
||||
this.textarea = null;
|
||||
this.preview = null;
|
||||
}
|
||||
|
||||
async reloadWithVimMode() {
|
||||
console.log('Reloading editor with Vim mode...');
|
||||
await this.initEditor();
|
||||
}
|
||||
}
|
||||
|
||||
// Global instances
|
||||
@ -360,9 +416,9 @@ class SlashCommands {
|
||||
this.palette.id = 'slash-commands-palette';
|
||||
this.palette.style.cssText = `
|
||||
position: fixed;
|
||||
background: #161b22;
|
||||
background-color: #161b22 !important;
|
||||
border: 1px solid #58a6ff;
|
||||
background: var(--bg-secondary);
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border: 1px solid var(--border-primary);
|
||||
list-style: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
@ -372,7 +428,7 @@ class SlashCommands {
|
||||
min-width: 220px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.3), 0 0 20px rgba(88, 166, 255, 0.2);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
||||
opacity: 1 !important;
|
||||
`;
|
||||
|
||||
@ -477,14 +533,14 @@ class SlashCommands {
|
||||
|
||||
filteredCommands.forEach((cmd, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<span style="color: #7d8590; margin-right: 0.5rem;">⌘</span>/${cmd.name}`;
|
||||
li.innerHTML = `<span style="color: var(--text-muted); margin-right: 0.5rem;">⌘</span>/${cmd.name}`;
|
||||
|
||||
const isSelected = index === this.selectedIndex;
|
||||
li.style.cssText = `
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
color: ${isSelected ? 'white' : '#e6edf3'};
|
||||
background: ${isSelected ? 'linear-gradient(135deg, #58a6ff, #8b5cf6)' : 'transparent'};
|
||||
color: ${isSelected ? 'var(--text-primary)' : 'var(--text-secondary)'};
|
||||
background: ${isSelected ? 'var(--accent-primary)' : 'transparent'};
|
||||
border-radius: 4px;
|
||||
margin: 4px 0;
|
||||
transition: all 150ms ease;
|
||||
@ -632,12 +688,7 @@ function initializeMarkdownEditor(context) {
|
||||
const markdownEditor = new MarkdownEditor(textarea, preview);
|
||||
window.currentMarkdownEditor = markdownEditor;
|
||||
|
||||
if (markdownEditor.editorView) {
|
||||
const slashCommands = new SlashCommands({
|
||||
editorView: markdownEditor.editorView
|
||||
});
|
||||
window.currentSlashCommands = slashCommands;
|
||||
}
|
||||
// Note: SlashCommands sera créé automatiquement dans initEditor() qui est async
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
246
frontend/src/favorites.js
Normal file
246
frontend/src/favorites.js
Normal file
@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Favorites - Gère le système de favoris
|
||||
*/
|
||||
|
||||
class FavoritesManager {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('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);
|
||||
|
||||
if (event.detail.target.id === 'file-tree') {
|
||||
console.log('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...');
|
||||
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...');
|
||||
this.attachFavoriteButtons();
|
||||
}, 1000);
|
||||
|
||||
console.log('FavoritesManager: Initialisé');
|
||||
}
|
||||
|
||||
refreshFavorites() {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', '/api/favorites', {
|
||||
target: '#favorites-list',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async addFavorite(path, isDir, title) {
|
||||
console.log('addFavorite appelé avec:', { path, isDir, title });
|
||||
|
||||
try {
|
||||
// Utiliser URLSearchParams au lieu de FormData pour le format application/x-www-form-urlencoded
|
||||
const params = new URLSearchParams();
|
||||
params.append('path', path);
|
||||
params.append('is_dir', isDir ? 'true' : 'false');
|
||||
params.append('title', title || '');
|
||||
|
||||
console.log('Params créés:', {
|
||||
path: params.get('path'),
|
||||
is_dir: params.get('is_dir'),
|
||||
title: params.get('title')
|
||||
});
|
||||
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: params.toString()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
document.getElementById('favorites-list').innerHTML = html;
|
||||
this.attachFavoriteButtons();
|
||||
console.log('Favori ajouté:', path);
|
||||
} else if (response.status === 409) {
|
||||
console.log('Déjà en favoris');
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('Erreur ajout favori:', response.status, response.statusText, errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur ajout favori:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async removeFavorite(path) {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('path', path);
|
||||
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: params.toString()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
document.getElementById('favorites-list').innerHTML = html;
|
||||
this.attachFavoriteButtons();
|
||||
console.log('Favori retiré:', path);
|
||||
} else {
|
||||
console.error('Erreur retrait favori:', response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur retrait favori:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getFavoritesPaths() {
|
||||
try {
|
||||
const response = await fetch('/api/favorites');
|
||||
const html = await response.text();
|
||||
|
||||
// Parser le HTML pour extraire les chemins
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const items = doc.querySelectorAll('.favorite-item');
|
||||
|
||||
return Array.from(items).map(item => item.getAttribute('data-path'));
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement favoris:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
attachFavoriteButtons() {
|
||||
console.log('attachFavoriteButtons: Début...');
|
||||
|
||||
// Ajouter des boutons étoile aux éléments du file tree
|
||||
this.getFavoritesPaths().then(favoritePaths => {
|
||||
console.log('Chemins favoris:', favoritePaths);
|
||||
|
||||
// Dossiers
|
||||
const folderHeaders = document.querySelectorAll('.folder-header');
|
||||
console.log('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();
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
console.log('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('Retrait dossier des favoris:', path);
|
||||
this.removeFavorite(path);
|
||||
};
|
||||
}
|
||||
|
||||
header.appendChild(button);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fichiers
|
||||
const fileItems = document.querySelectorAll('.file-item');
|
||||
console.log('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', '');
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('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('Retrait fichier des favoris:', path);
|
||||
this.removeFavorite(path);
|
||||
};
|
||||
}
|
||||
|
||||
fileItem.appendChild(button);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('attachFavoriteButtons: Terminé');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonctions globales pour les templates
|
||||
*/
|
||||
window.removeFavorite = function(path) {
|
||||
if (window.favoritesManager) {
|
||||
window.favoritesManager.removeFavorite(path);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialisation automatique
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.favoritesManager = new FavoritesManager();
|
||||
});
|
||||
} else {
|
||||
// DOM déjà chargé
|
||||
window.favoritesManager = new FavoritesManager();
|
||||
}
|
||||
@ -20,6 +20,11 @@ class FileTree {
|
||||
|
||||
// Event listener délégué pour les clics sur les folder-headers
|
||||
sidebar.addEventListener('click', (e) => {
|
||||
// Ignorer les clics sur les checkboxes
|
||||
if (e.target.classList.contains('selection-checkbox')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier d'abord si c'est un folder-header ou un de ses enfants
|
||||
const folderHeader = e.target.closest('.folder-header');
|
||||
if (folderHeader && !e.target.closest('.file-item')) {
|
||||
@ -489,4 +494,234 @@ document.addEventListener('keydown', (event) => {
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.fileTree = new FileTree();
|
||||
});
|
||||
window.selectionManager = new SelectionManager();
|
||||
});
|
||||
|
||||
/**
|
||||
* SelectionManager - Gère le mode sélection et la suppression en masse
|
||||
*/
|
||||
class SelectionManager {
|
||||
constructor() {
|
||||
this.isSelectionMode = false;
|
||||
this.selectedPaths = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Écouter les événements HTMX pour réinitialiser les listeners après les swaps
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.detail.target.id === 'file-tree' || event.detail.target.closest('#file-tree')) {
|
||||
this.attachCheckboxListeners();
|
||||
if (this.isSelectionMode) {
|
||||
this.showCheckboxes();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:oobAfterSwap', (event) => {
|
||||
if (event.detail.target.id === 'file-tree') {
|
||||
this.attachCheckboxListeners();
|
||||
if (this.isSelectionMode) {
|
||||
this.showCheckboxes();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Attacher les listeners initiaux
|
||||
setTimeout(() => this.attachCheckboxListeners(), 500);
|
||||
}
|
||||
|
||||
attachCheckboxListeners() {
|
||||
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
// Retirer l'ancien listener s'il existe
|
||||
checkbox.removeEventListener('change', this.handleCheckboxChange);
|
||||
// Ajouter le nouveau listener
|
||||
checkbox.addEventListener('change', (e) => this.handleCheckboxChange(e));
|
||||
});
|
||||
}
|
||||
|
||||
handleCheckboxChange(e) {
|
||||
const checkbox = e.target;
|
||||
const path = checkbox.dataset.path;
|
||||
|
||||
if (checkbox.checked) {
|
||||
window.selectionManager.selectedPaths.add(path);
|
||||
} else {
|
||||
window.selectionManager.selectedPaths.delete(path);
|
||||
}
|
||||
|
||||
window.selectionManager.updateToolbar();
|
||||
}
|
||||
|
||||
toggleSelectionMode() {
|
||||
this.isSelectionMode = !this.isSelectionMode;
|
||||
|
||||
if (this.isSelectionMode) {
|
||||
this.showCheckboxes();
|
||||
document.getElementById('toggle-selection-mode')?.classList.add('active');
|
||||
} else {
|
||||
this.hideCheckboxes();
|
||||
this.clearSelection();
|
||||
document.getElementById('toggle-selection-mode')?.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
showCheckboxes() {
|
||||
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.style.display = 'inline-block';
|
||||
});
|
||||
}
|
||||
|
||||
hideCheckboxes() {
|
||||
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedPaths.clear();
|
||||
this.updateToolbar();
|
||||
}
|
||||
|
||||
updateToolbar() {
|
||||
const toolbar = document.getElementById('selection-toolbar');
|
||||
const countSpan = document.getElementById('selection-count');
|
||||
|
||||
if (this.selectedPaths.size > 0) {
|
||||
toolbar.style.display = 'flex';
|
||||
countSpan.textContent = `${this.selectedPaths.size} élément(s) sélectionné(s)`;
|
||||
} else {
|
||||
toolbar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showDeleteConfirmationModal() {
|
||||
const modal = document.getElementById('delete-confirmation-modal');
|
||||
const countSpan = document.getElementById('delete-count');
|
||||
const itemsList = document.getElementById('delete-items-list');
|
||||
|
||||
countSpan.textContent = this.selectedPaths.size;
|
||||
|
||||
// Générer la liste des éléments à supprimer
|
||||
itemsList.innerHTML = '';
|
||||
const ul = document.createElement('ul');
|
||||
ul.style.margin = '0';
|
||||
ul.style.padding = '0 0 0 1.5rem';
|
||||
ul.style.color = 'var(--text-primary)';
|
||||
|
||||
this.selectedPaths.forEach(path => {
|
||||
const li = document.createElement('li');
|
||||
li.style.marginBottom = '0.5rem';
|
||||
|
||||
// Déterminer si c'est un dossier
|
||||
const checkbox = document.querySelector(`.selection-checkbox[data-path="${path}"]`);
|
||||
const isDir = checkbox?.dataset.isDir === 'true';
|
||||
|
||||
li.innerHTML = `${isDir ? '📁' : '📄'} <code>${path}</code>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
itemsList.appendChild(ul);
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
hideDeleteConfirmationModal() {
|
||||
const modal = document.getElementById('delete-confirmation-modal');
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
async deleteSelectedItems() {
|
||||
const paths = Array.from(this.selectedPaths);
|
||||
|
||||
if (paths.length === 0) {
|
||||
alert('Aucun élément sélectionné');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Construire le corps de la requête au format query string
|
||||
// Le backend attend: paths[]=path1&paths[]=path2
|
||||
const params = new URLSearchParams();
|
||||
paths.forEach(path => {
|
||||
params.append('paths[]', path);
|
||||
});
|
||||
|
||||
// Utiliser fetch() avec le corps en query string
|
||||
const response = await fetch('/api/files/delete-multiple', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: params.toString()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Parser le HTML pour trouver les éléments avec hx-swap-oob
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// Traiter les swaps out-of-band manuellement
|
||||
doc.querySelectorAll('[hx-swap-oob]').forEach(element => {
|
||||
const targetId = element.id;
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
target.innerHTML = element.innerHTML;
|
||||
// Déclencher l'événement htmx pour que les listeners se réattachent
|
||||
htmx.process(target);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`${paths.length} élément(s) supprimé(s)`);
|
||||
|
||||
// Fermer la modale
|
||||
this.hideDeleteConfirmationModal();
|
||||
|
||||
// Réinitialiser la sélection et garder le mode sélection actif
|
||||
this.clearSelection();
|
||||
|
||||
// Réattacher les listeners sur les nouvelles checkboxes
|
||||
setTimeout(() => {
|
||||
this.attachCheckboxListeners();
|
||||
if (this.isSelectionMode) {
|
||||
this.showCheckboxes();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error);
|
||||
alert('Erreur lors de la suppression des éléments: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonctions globales pour les boutons
|
||||
*/
|
||||
window.toggleSelectionMode = function() {
|
||||
window.selectionManager.toggleSelectionMode();
|
||||
};
|
||||
|
||||
window.deleteSelected = function() {
|
||||
window.selectionManager.showDeleteConfirmationModal();
|
||||
};
|
||||
|
||||
window.cancelSelection = function() {
|
||||
window.selectionManager.toggleSelectionMode();
|
||||
};
|
||||
|
||||
window.hideDeleteConfirmationModal = function() {
|
||||
window.selectionManager.hideDeleteConfirmationModal();
|
||||
};
|
||||
|
||||
window.confirmDelete = function() {
|
||||
window.selectionManager.deleteSelectedItems();
|
||||
};
|
||||
184
frontend/src/font-manager.js
Normal file
184
frontend/src/font-manager.js
Normal file
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Font Manager - Gère le changement de polices
|
||||
*/
|
||||
|
||||
class FontManager {
|
||||
constructor() {
|
||||
this.fonts = [
|
||||
{
|
||||
id: 'fira-code',
|
||||
name: 'Fira Code',
|
||||
family: "'Fira Code', 'Courier New', monospace",
|
||||
googleFont: 'Fira+Code:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'sans-serif',
|
||||
name: 'Sans-serif',
|
||||
family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
googleFont: null
|
||||
},
|
||||
{
|
||||
id: 'inter',
|
||||
name: 'Inter',
|
||||
family: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
googleFont: 'Inter:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'poppins',
|
||||
name: 'Poppins',
|
||||
family: "'Poppins', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
googleFont: 'Poppins:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'public-sans',
|
||||
name: 'Public Sans',
|
||||
family: "'Public Sans', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
googleFont: 'Public+Sans:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'jetbrains-mono',
|
||||
name: 'JetBrains Mono',
|
||||
family: "'JetBrains Mono', 'Courier New', monospace",
|
||||
googleFont: 'JetBrains+Mono:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'cascadia-code',
|
||||
name: 'Cascadia Code',
|
||||
family: "'Cascadia Code', 'Courier New', monospace",
|
||||
googleFont: 'Cascadia+Code:wght@300;400;500;600;700'
|
||||
},
|
||||
{
|
||||
id: 'source-code-pro',
|
||||
name: 'Source Code Pro',
|
||||
family: "'Source Code Pro', 'Courier New', monospace",
|
||||
googleFont: 'Source+Code+Pro:wght@300;400;500;600;700'
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Charger la police sauvegardée
|
||||
const savedFont = localStorage.getItem('selectedFont') || 'jetbrains-mono';
|
||||
this.applyFont(savedFont);
|
||||
|
||||
// Charger la taille sauvegardée
|
||||
const savedSize = localStorage.getItem('fontSize') || 'medium';
|
||||
this.applyFontSize(savedSize);
|
||||
|
||||
console.log('FontManager initialized with font:', savedFont, 'size:', savedSize);
|
||||
}
|
||||
|
||||
applyFont(fontId) {
|
||||
const font = this.fonts.find(f => f.id === fontId);
|
||||
if (!font) {
|
||||
console.error('Police non trouvée:', fontId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Charger la police Google Fonts si nécessaire
|
||||
if (font.googleFont) {
|
||||
this.loadGoogleFont(font.googleFont);
|
||||
}
|
||||
|
||||
// Appliquer la police au body
|
||||
document.body.style.fontFamily = font.family;
|
||||
|
||||
// Sauvegarder le choix
|
||||
localStorage.setItem('selectedFont', fontId);
|
||||
|
||||
console.log('Police appliquée:', font.name);
|
||||
}
|
||||
|
||||
applyFontSize(sizeId) {
|
||||
// Définir les tailles en utilisant une variable CSS root
|
||||
const sizes = {
|
||||
'small': '14px',
|
||||
'medium': '16px',
|
||||
'large': '18px',
|
||||
'x-large': '20px'
|
||||
};
|
||||
|
||||
const size = sizes[sizeId] || sizes['medium'];
|
||||
|
||||
// Appliquer la taille via une variable CSS sur :root
|
||||
// Cela affectera tous les éléments qui utilisent rem
|
||||
document.documentElement.style.fontSize = size;
|
||||
|
||||
// Sauvegarder le choix
|
||||
localStorage.setItem('fontSize', sizeId);
|
||||
|
||||
console.log('Taille de police appliquée:', sizeId, size);
|
||||
}
|
||||
|
||||
getCurrentSize() {
|
||||
return localStorage.getItem('fontSize') || 'medium';
|
||||
}
|
||||
|
||||
loadGoogleFont(fontParam) {
|
||||
// Vérifier si la police n'est pas déjà chargée
|
||||
const linkId = 'google-font-' + fontParam.split(':')[0].replace(/\+/g, '-');
|
||||
if (document.getElementById(linkId)) {
|
||||
return; // Déjà chargé
|
||||
}
|
||||
|
||||
// Créer un nouveau link pour Google Fonts
|
||||
const link = document.createElement('link');
|
||||
link.id = linkId;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`;
|
||||
document.head.appendChild(link);
|
||||
|
||||
console.log('Google Font chargée:', fontParam);
|
||||
}
|
||||
|
||||
getCurrentFont() {
|
||||
return localStorage.getItem('selectedFont') || 'jetbrains-mono';
|
||||
}
|
||||
|
||||
getFonts() {
|
||||
return this.fonts;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation automatique
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.fontManager = new FontManager();
|
||||
});
|
||||
} else {
|
||||
window.fontManager = new FontManager();
|
||||
}
|
||||
|
||||
// Fonction globale pour changer la police
|
||||
window.selectFont = function(fontId) {
|
||||
if (window.fontManager) {
|
||||
window.fontManager.applyFont(fontId);
|
||||
|
||||
// Mettre à jour l'interface (marquer comme active)
|
||||
document.querySelectorAll('.font-card').forEach(card => {
|
||||
card.classList.remove('active');
|
||||
});
|
||||
const selectedCard = document.querySelector(`.font-card[data-font="${fontId}"]`);
|
||||
if (selectedCard) {
|
||||
selectedCard.classList.add('active');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction globale pour changer la taille de police
|
||||
window.selectFontSize = function(sizeId) {
|
||||
if (window.fontManager) {
|
||||
window.fontManager.applyFontSize(sizeId);
|
||||
|
||||
// Mettre à jour l'interface (marquer comme active)
|
||||
document.querySelectorAll('.font-size-option').forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
const selectedOption = document.querySelector(`.font-size-option[data-size="${sizeId}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('active');
|
||||
}
|
||||
}
|
||||
};
|
||||
165
frontend/src/keyboard-shortcuts.js
Normal file
165
frontend/src/keyboard-shortcuts.js
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Keyboard Shortcuts Manager - Gère tous les raccourcis clavier de l'application
|
||||
*/
|
||||
|
||||
class KeyboardShortcutsManager {
|
||||
constructor() {
|
||||
this.shortcuts = [
|
||||
{ key: 'k', ctrl: true, description: 'Ouvrir la recherche', action: () => this.openSearch() },
|
||||
{ key: 's', ctrl: true, description: 'Sauvegarder la note', action: () => this.saveNote() },
|
||||
{ key: 'd', ctrl: true, description: 'Ouvrir la note du jour', action: () => this.openDailyNote() },
|
||||
{ key: 'n', ctrl: true, description: 'Créer une nouvelle note', action: () => this.createNewNote() },
|
||||
{ key: 'h', ctrl: true, description: 'Retour à la page d\'accueil', action: () => this.goHome() },
|
||||
{ key: 'b', ctrl: true, description: 'Afficher/Masquer la sidebar', action: () => this.toggleSidebar() },
|
||||
{ key: ',', ctrl: true, description: 'Ouvrir les paramètres', action: () => this.openSettings() },
|
||||
{ key: 'p', ctrl: true, description: 'Afficher/Masquer la prévisualisation', action: () => this.togglePreview() },
|
||||
{ key: 'f', ctrl: true, shift: true, description: 'Créer un nouveau dossier', action: () => this.createNewFolder() },
|
||||
{ key: 'Escape', ctrl: false, description: 'Fermer les modales/dialogs', action: () => this.closeModals() }
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
this.handleKeydown(event);
|
||||
});
|
||||
|
||||
console.log('Keyboard shortcuts initialized:', this.shortcuts.length, 'shortcuts');
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
// Ignorer si on tape dans un input/textarea (sauf pour les raccourcis système comme Ctrl+S)
|
||||
const isInputField = event.target.tagName === 'INPUT' ||
|
||||
event.target.tagName === 'TEXTAREA' ||
|
||||
event.target.isContentEditable;
|
||||
|
||||
// Chercher un raccourci correspondant
|
||||
for (const shortcut of this.shortcuts) {
|
||||
const ctrlMatch = shortcut.ctrl ? (event.ctrlKey || event.metaKey) : !event.ctrlKey && !event.metaKey;
|
||||
const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey;
|
||||
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
|
||||
|
||||
if (ctrlMatch && shiftMatch && keyMatch) {
|
||||
// Certains raccourcis fonctionnent même dans les champs de saisie
|
||||
const allowInInput = ['s', 'k', 'd', 'h', 'b', ',', '/'].includes(shortcut.key.toLowerCase());
|
||||
|
||||
if (!isInputField || allowInInput) {
|
||||
event.preventDefault();
|
||||
shortcut.action();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openSearch() {
|
||||
// Déclencher le focus sur le champ de recherche
|
||||
const searchInput = document.querySelector('header input[type="search"]');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
console.log('Search opened via Ctrl+K');
|
||||
}
|
||||
}
|
||||
|
||||
saveNote() {
|
||||
// Déclencher la sauvegarde de la note (géré par CodeMirror)
|
||||
console.log('Save triggered via Ctrl+S');
|
||||
// La sauvegarde est déjà gérée dans editor.js
|
||||
}
|
||||
|
||||
openDailyNote() {
|
||||
// Ouvrir la note du jour
|
||||
const dailyBtn = document.querySelector('button[hx-get="/api/daily/today"]');
|
||||
if (dailyBtn) {
|
||||
dailyBtn.click();
|
||||
console.log('Daily note opened via Ctrl+D');
|
||||
}
|
||||
}
|
||||
|
||||
createNewNote() {
|
||||
if (typeof showNewNoteModal === 'function') {
|
||||
showNewNoteModal();
|
||||
console.log('New note modal opened via Ctrl+N');
|
||||
}
|
||||
}
|
||||
|
||||
goHome() {
|
||||
const homeBtn = document.querySelector('button[hx-get="/api/home"]');
|
||||
if (homeBtn) {
|
||||
homeBtn.click();
|
||||
console.log('Home opened via Ctrl+H');
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
if (typeof toggleSidebar === 'function') {
|
||||
toggleSidebar();
|
||||
console.log('Sidebar toggled via Ctrl+B');
|
||||
}
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
if (typeof openThemeModal === 'function') {
|
||||
openThemeModal();
|
||||
console.log('Settings opened via Ctrl+,');
|
||||
}
|
||||
}
|
||||
|
||||
togglePreview() {
|
||||
if (typeof togglePreview === 'function') {
|
||||
togglePreview();
|
||||
console.log('Preview toggled via Ctrl+/');
|
||||
}
|
||||
}
|
||||
|
||||
createNewFolder() {
|
||||
if (typeof showNewFolderModal === 'function') {
|
||||
showNewFolderModal();
|
||||
console.log('New folder modal opened via Ctrl+Shift+F');
|
||||
}
|
||||
}
|
||||
|
||||
closeModals() {
|
||||
// Fermer les modales ouvertes
|
||||
if (typeof hideNewNoteModal === 'function') {
|
||||
const noteModal = document.getElementById('new-note-modal');
|
||||
if (noteModal && noteModal.style.display !== 'none') {
|
||||
hideNewNoteModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof hideNewFolderModal === 'function') {
|
||||
const folderModal = document.getElementById('new-folder-modal');
|
||||
if (folderModal && folderModal.style.display !== 'none') {
|
||||
hideNewFolderModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof closeThemeModal === 'function') {
|
||||
const themeModal = document.getElementById('theme-modal');
|
||||
if (themeModal && themeModal.style.display !== 'none') {
|
||||
closeThemeModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Escape pressed');
|
||||
}
|
||||
|
||||
getShortcuts() {
|
||||
return this.shortcuts;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation automatique
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.keyboardShortcuts = new KeyboardShortcutsManager();
|
||||
});
|
||||
} else {
|
||||
window.keyboardShortcuts = new KeyboardShortcutsManager();
|
||||
}
|
||||
@ -2,3 +2,4 @@ import './editor.js';
|
||||
import './file-tree.js';
|
||||
import './ui.js';
|
||||
import './search.js';
|
||||
import './daily-notes.js';
|
||||
|
||||
205
frontend/src/theme-manager.js
Normal file
205
frontend/src/theme-manager.js
Normal file
@ -0,0 +1,205 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themes = [
|
||||
{
|
||||
id: 'material-dark',
|
||||
name: 'Material Dark',
|
||||
icon: '🌙',
|
||||
description: 'Thème professionnel inspiré de Material Design'
|
||||
},
|
||||
{
|
||||
id: 'monokai-dark',
|
||||
name: 'Monokai Dark',
|
||||
icon: '🎨',
|
||||
description: 'Palette Monokai classique pour les développeurs'
|
||||
},
|
||||
{
|
||||
id: 'dracula',
|
||||
name: 'Dracula',
|
||||
icon: '🧛',
|
||||
description: 'Thème sombre élégant avec des accents violets et cyan'
|
||||
},
|
||||
{
|
||||
id: 'one-dark',
|
||||
name: 'One Dark',
|
||||
icon: '⚡',
|
||||
description: 'Thème populaire d\'Atom avec des couleurs douces'
|
||||
},
|
||||
{
|
||||
id: 'solarized-dark',
|
||||
name: 'Solarized Dark',
|
||||
icon: '☀️',
|
||||
description: 'Palette scientifiquement optimisée pour réduire la fatigue oculaire'
|
||||
},
|
||||
{
|
||||
id: 'nord',
|
||||
name: 'Nord',
|
||||
icon: '❄️',
|
||||
description: 'Palette arctique apaisante avec des tons bleus froids'
|
||||
},
|
||||
{
|
||||
id: 'catppuccin',
|
||||
name: 'Catppuccin',
|
||||
icon: '🌸',
|
||||
description: 'Thème pastel doux et chaleureux avec des accents roses et bleus'
|
||||
},
|
||||
{
|
||||
id: 'everforest',
|
||||
name: 'Everforest',
|
||||
icon: '🌲',
|
||||
description: 'Palette naturelle inspirée de la forêt avec des tons verts et beiges'
|
||||
}
|
||||
];
|
||||
|
||||
this.currentTheme = this.loadTheme();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Appliquer le thème sauvegardé
|
||||
this.applyTheme(this.currentTheme);
|
||||
|
||||
// Écouter les événements HTMX pour réinitialiser les listeners
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.detail.target.id === 'sidebar' || event.detail.target.closest('#sidebar')) {
|
||||
this.attachModalListeners();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('ThemeManager initialized with theme:', this.currentTheme);
|
||||
}
|
||||
|
||||
loadTheme() {
|
||||
// Charger le thème depuis localStorage, par défaut 'material-dark'
|
||||
return localStorage.getItem('app-theme') || 'material-dark';
|
||||
}
|
||||
|
||||
saveTheme(themeId) {
|
||||
localStorage.setItem('app-theme', themeId);
|
||||
}
|
||||
|
||||
applyTheme(themeId) {
|
||||
// Appliquer le thème sur l'élément racine
|
||||
document.documentElement.setAttribute('data-theme', themeId);
|
||||
this.currentTheme = themeId;
|
||||
this.saveTheme(themeId);
|
||||
|
||||
// Mettre à jour les cartes de thème si la modale est ouverte
|
||||
this.updateThemeCards();
|
||||
|
||||
console.log('Theme applied:', themeId);
|
||||
}
|
||||
|
||||
openThemeModal() {
|
||||
const modal = document.getElementById('theme-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
this.updateThemeCards();
|
||||
}
|
||||
}
|
||||
|
||||
closeThemeModal() {
|
||||
const modal = document.getElementById('theme-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updateThemeCards() {
|
||||
// Mettre à jour l'état actif des cartes de thème
|
||||
const cards = document.querySelectorAll('.theme-card');
|
||||
cards.forEach(card => {
|
||||
const themeId = card.dataset.theme;
|
||||
if (themeId === this.currentTheme) {
|
||||
card.classList.add('active');
|
||||
} else {
|
||||
card.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
attachModalListeners() {
|
||||
// Ré-attacher les listeners après un swap HTMX
|
||||
const settingsBtn = document.getElementById('theme-settings-btn');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.replaceWith(settingsBtn.cloneNode(true));
|
||||
const newBtn = document.getElementById('theme-settings-btn');
|
||||
newBtn.addEventListener('click', () => this.openThemeModal());
|
||||
}
|
||||
}
|
||||
|
||||
getThemes() {
|
||||
return this.themes;
|
||||
}
|
||||
|
||||
getCurrentTheme() {
|
||||
return this.currentTheme;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonctions globales pour les boutons
|
||||
*/
|
||||
window.openThemeModal = function() {
|
||||
if (window.themeManager) {
|
||||
window.themeManager.openThemeModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.closeThemeModal = function() {
|
||||
if (window.themeManager) {
|
||||
window.themeManager.closeThemeModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.selectTheme = function(themeId) {
|
||||
if (window.themeManager) {
|
||||
window.themeManager.applyTheme(themeId);
|
||||
}
|
||||
};
|
||||
|
||||
window.switchSettingsTab = function(tabName) {
|
||||
console.log('Switching to tab:', tabName);
|
||||
|
||||
// Désactiver tous les onglets
|
||||
const tabs = document.querySelectorAll('.settings-tab');
|
||||
tabs.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Cacher toutes les sections
|
||||
document.getElementById('themes-section').style.display = 'none';
|
||||
document.getElementById('fonts-section').style.display = 'none';
|
||||
document.getElementById('editor-section').style.display = 'none';
|
||||
|
||||
// Activer l'onglet cliqué
|
||||
const activeTab = Array.from(tabs).find(tab => {
|
||||
const text = tab.textContent.toLowerCase();
|
||||
if (tabName === 'themes') return text.includes('thème');
|
||||
if (tabName === 'fonts') return text.includes('police');
|
||||
if (tabName === 'editor') return text.includes('éditeur');
|
||||
return false;
|
||||
});
|
||||
if (activeTab) {
|
||||
activeTab.classList.add('active');
|
||||
}
|
||||
|
||||
// Afficher la section correspondante
|
||||
const sectionId = tabName + '-section';
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
section.style.display = 'block';
|
||||
console.log('Showing section:', sectionId);
|
||||
} else {
|
||||
console.error('Section not found:', sectionId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialisation automatique
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.themeManager = new ThemeManager();
|
||||
});
|
||||
139
frontend/src/vim-mode-manager.js
Normal file
139
frontend/src/vim-mode-manager.js
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Vim Mode Manager - Gère l'activation/désactivation du mode Vim dans CodeMirror
|
||||
*/
|
||||
|
||||
class VimModeManager {
|
||||
constructor() {
|
||||
this.enabled = this.loadPreference();
|
||||
this.vim = null; // Extension Vim de CodeMirror
|
||||
this.editorView = null; // Instance EditorView actuelle
|
||||
|
||||
console.log('VimModeManager initialized, enabled:', this.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge la préférence du mode Vim depuis localStorage
|
||||
*/
|
||||
loadPreference() {
|
||||
const saved = localStorage.getItem('vimModeEnabled');
|
||||
return saved === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde la préférence du mode Vim
|
||||
*/
|
||||
savePreference(enabled) {
|
||||
localStorage.setItem('vimModeEnabled', enabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'état actuel du mode Vim
|
||||
*/
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active ou désactive le mode Vim
|
||||
*/
|
||||
async toggle() {
|
||||
this.enabled = !this.enabled;
|
||||
this.savePreference(this.enabled);
|
||||
|
||||
// Recharger l'éditeur si il existe
|
||||
if (window.currentEditor && window.currentEditor.reloadWithVimMode) {
|
||||
await window.currentEditor.reloadWithVimMode(this.enabled);
|
||||
}
|
||||
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge l'extension Vim de façon asynchrone
|
||||
*/
|
||||
async loadVimExtension() {
|
||||
if (this.vim) {
|
||||
return this.vim;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import dynamique du package Vim
|
||||
const { vim } = await import('@replit/codemirror-vim');
|
||||
this.vim = vim;
|
||||
console.log('✅ Vim extension loaded successfully');
|
||||
return this.vim;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Vim mode is not available. The @replit/codemirror-vim package is not installed.');
|
||||
console.info('To install it, run: cd frontend && npm install && npm run build');
|
||||
this.vim = false; // Marquer comme échoué
|
||||
this.enabled = false; // Désactiver automatiquement
|
||||
this.savePreference(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'extension Vim pour CodeMirror (si activée)
|
||||
*/
|
||||
async getVimExtension() {
|
||||
if (!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si déjà essayé et échoué
|
||||
if (this.vim === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.vim) {
|
||||
return this.vim();
|
||||
}
|
||||
|
||||
const vimModule = await this.loadVimExtension();
|
||||
return vimModule ? vimModule() : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Instance globale
|
||||
const vimModeManager = new VimModeManager();
|
||||
|
||||
// Export pour utilisation dans d'autres modules
|
||||
if (typeof window !== 'undefined') {
|
||||
window.vimModeManager = vimModeManager;
|
||||
|
||||
// Fonction globale pour le toggle dans la modale
|
||||
window.toggleVimMode = async function() {
|
||||
const checkbox = document.getElementById('vim-mode-toggle');
|
||||
if (!checkbox) return;
|
||||
|
||||
const enabled = await vimModeManager.toggle();
|
||||
checkbox.checked = enabled;
|
||||
|
||||
// Vérifier si le package est disponible
|
||||
if (enabled && vimModeManager.vim === false) {
|
||||
alert('❌ Le mode Vim n\'est pas disponible.\n\nLe package @replit/codemirror-vim n\'est pas installé.\n\nPour l\'installer, exécutez :\ncd frontend\nnpm install\nnpm run build');
|
||||
checkbox.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher un message
|
||||
const message = enabled ? '✅ Mode Vim activé' : '❌ Mode Vim désactivé';
|
||||
console.log(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);
|
||||
} else {
|
||||
console.log('No editor to reload. Vim mode will be applied when opening a note.');
|
||||
}
|
||||
};
|
||||
|
||||
// Initialiser l'état du checkbox au chargement
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const checkbox = document.getElementById('vim-mode-toggle');
|
||||
if (checkbox) {
|
||||
checkbox.checked = vimModeManager.isEnabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user