feat: finalize Forge editor — Tab indent, Ctrl+K, auto-focus, AI status, French errors
Tab/Shift+Tab: indent/dedent list items. Ctrl+K: link insertion prompt. Qi button: position:fixed (always visible). Auto-focus textarea on load. AI ready indicator in status bar. All error messages in French. Force-save shows 'Sauvegarde' toast.
This commit is contained in:
+47
-13
@@ -56,7 +56,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
.textarea::placeholder { color: var(--text3); }
|
||||
|
||||
/* Quick insert + */
|
||||
.qi-btn { position: absolute; left: 14px; top: 24px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg2); color: var(--text3); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 100ms; }
|
||||
.qi-btn { position: fixed; top: 56px; left: 48px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg2); color: var(--text3); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 50; transition: all 100ms; }
|
||||
.qi-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-bg); }
|
||||
|
||||
/* Preview pane */
|
||||
@@ -331,7 +331,9 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
<span>Type <strong>/</strong> for commands | <strong>Alt+I</strong> Insert</span>
|
||||
<span>Ctrl+J AI panel</span>
|
||||
<span>Ctrl+S save</span>
|
||||
<span>Ctrl+K link</span>
|
||||
<span class="stat-ai" id="stat-ai">✨ AI processing...</span>
|
||||
<span id="stat-ai-ready" style="display:none;color:var(--ok)">✨ AI ready</span>
|
||||
</div>
|
||||
<div class="stat-right">
|
||||
<span class="stat-file" id="stat-file"></span>
|
||||
@@ -436,6 +438,25 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
replaceLine(l.text.replace(/^\/\s*/, ''));
|
||||
}
|
||||
|
||||
// ---- Tab indentation ----
|
||||
function indentLine() {
|
||||
var l = getLine();
|
||||
if (/^(\s*)([-*]|\d+\.)\s/.test(l.text)) { replaceLine(' ' + l.text); }
|
||||
else { insertAtCursor(' '); }
|
||||
}
|
||||
function dedentLine() {
|
||||
var l = getLine();
|
||||
if (/^ /.test(l.text)) { replaceLine(l.text.replace(/^ /, '')); }
|
||||
}
|
||||
function promptLink() {
|
||||
var sel = val().substring(ta.selectionStart, ta.selectionEnd);
|
||||
var url = prompt('URL:', 'https://');
|
||||
if (url) {
|
||||
if (sel) { wrapSelection('[', '](' + url + ')'); }
|
||||
else { insertAtCursor('[text](' + url + ')', 1); }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Dirty / Save ----
|
||||
function markDirty() {
|
||||
if (!isDirty) { isDirty = true; updateSaveDot(); }
|
||||
@@ -465,13 +486,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
isDirty = false; originalContent = content;
|
||||
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
|
||||
}).catch(function(e) {
|
||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
|
||||
showToast('Save failed: ' + e.message, 'err');
|
||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
|
||||
showToast('Echec sauvegarde: ' + e.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
function forceSave() {
|
||||
if (!fileVault || !filePath) { showToast('No file loaded. Use ?vault=X&path=Y', 'err'); return; }
|
||||
if (!fileVault || !filePath) { showToast('Aucun fichier charge.', 'err'); return; }
|
||||
clearTimeout(saveTimer);
|
||||
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
|
||||
var content = val();
|
||||
@@ -484,8 +505,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
|
||||
showToast('Saved', 'ok');
|
||||
}).catch(function(e) {
|
||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
|
||||
showToast('Save failed: ' + e.message, 'err');
|
||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
|
||||
showToast('Echec sauvegarde: ' + e.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -523,8 +544,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
schedulePreview();
|
||||
})
|
||||
.catch(function(e) {
|
||||
ta.value = '# Error\n\nCould not load: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
|
||||
statFile.textContent = 'Load error';
|
||||
ta.value = '# Erreur\n\nImpossible de charger: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
|
||||
statFile.textContent = 'Erreur chargement';
|
||||
originalContent = ta.value;
|
||||
updateStats();
|
||||
});
|
||||
@@ -538,8 +559,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
aiConfigured = s.configured === true;
|
||||
updateAIButton();
|
||||
if (aiConfigured) aiCall(endpoint, text, extra, callback);
|
||||
else { showToast('AI not configured', 'err'); hideAIProcessing(); }
|
||||
}).catch(function() { showToast('AI status check failed', 'err'); hideAIProcessing(); });
|
||||
else { showToast('AI non configuree', 'err'); hideAIProcessing(); }
|
||||
}).catch(function() { showToast('Verification AI echouee', 'err'); hideAIProcessing(); });
|
||||
return;
|
||||
}
|
||||
showAIProcessing();
|
||||
@@ -570,8 +591,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
}
|
||||
|
||||
function updateAIButton() {
|
||||
if (aiConfigured) { btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)'; }
|
||||
else { btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured'; }
|
||||
if (aiConfigured) {
|
||||
btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)';
|
||||
document.getElementById('stat-ai-ready').style.display = 'inline';
|
||||
} else {
|
||||
btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured';
|
||||
document.getElementById('stat-ai-ready').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showAIProcessing() { statAI.classList.add('on'); }
|
||||
@@ -646,7 +672,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
processPreviewBlocks();
|
||||
_previewLoading = false;
|
||||
}).catch(function(e) {
|
||||
previewContent.innerHTML = '<div class="preview-loading">Render error: ' + e.message + '</div>';
|
||||
previewContent.innerHTML = '<div class="preview-loading">Erreur de rendu</div>';
|
||||
_previewLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -1048,6 +1074,12 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
if (e.ctrlKey && e.key === 's') { e.preventDefault(); forceSave(); }
|
||||
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
||||
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrapSelection('*'); }
|
||||
if (e.ctrlKey && e.key === 'k') { e.preventDefault(); promptLink(); }
|
||||
// Tab: indent list items
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) { dedentLine(); } else { indentLine(); }
|
||||
}
|
||||
// Quick Insert: Alt+I
|
||||
if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); }
|
||||
});
|
||||
@@ -1177,6 +1209,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
loadFile();
|
||||
checkAIStatus();
|
||||
updateStats();
|
||||
// Auto-focus after short delay (wait for iframe to fully render)
|
||||
setTimeout(function() { ta.focus(); }, 200);
|
||||
|
||||
// ---- Process preview blocks (Mermaid + highlight.js) ----
|
||||
function processPreviewBlocks() {
|
||||
|
||||
Reference in New Issue
Block a user