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); }
|
.textarea::placeholder { color: var(--text3); }
|
||||||
|
|
||||||
/* Quick insert + */
|
/* 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); }
|
.qi-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-bg); }
|
||||||
|
|
||||||
/* Preview pane */
|
/* 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>Type <strong>/</strong> for commands | <strong>Alt+I</strong> Insert</span>
|
||||||
<span>Ctrl+J AI panel</span>
|
<span>Ctrl+J AI panel</span>
|
||||||
<span>Ctrl+S save</span>
|
<span>Ctrl+S save</span>
|
||||||
|
<span>Ctrl+K link</span>
|
||||||
<span class="stat-ai" id="stat-ai">✨ AI processing...</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>
|
||||||
<div class="stat-right">
|
<div class="stat-right">
|
||||||
<span class="stat-file" id="stat-file"></span>
|
<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*/, ''));
|
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 ----
|
// ---- Dirty / Save ----
|
||||||
function markDirty() {
|
function markDirty() {
|
||||||
if (!isDirty) { isDirty = true; updateSaveDot(); }
|
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;
|
isDirty = false; originalContent = content;
|
||||||
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
|
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
|
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
|
||||||
showToast('Save failed: ' + e.message, 'err');
|
showToast('Echec sauvegarde: ' + e.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceSave() {
|
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);
|
clearTimeout(saveTimer);
|
||||||
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
|
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
|
||||||
var content = val();
|
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';
|
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
|
||||||
showToast('Saved', 'ok');
|
showToast('Saved', 'ok');
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
|
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
|
||||||
showToast('Save failed: ' + e.message, 'err');
|
showToast('Echec sauvegarde: ' + e.message, 'err');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,8 +544,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
schedulePreview();
|
schedulePreview();
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
ta.value = '# Error\n\nCould not load: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
|
ta.value = '# Erreur\n\nImpossible de charger: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
|
||||||
statFile.textContent = 'Load error';
|
statFile.textContent = 'Erreur chargement';
|
||||||
originalContent = ta.value;
|
originalContent = ta.value;
|
||||||
updateStats();
|
updateStats();
|
||||||
});
|
});
|
||||||
@@ -538,8 +559,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
aiConfigured = s.configured === true;
|
aiConfigured = s.configured === true;
|
||||||
updateAIButton();
|
updateAIButton();
|
||||||
if (aiConfigured) aiCall(endpoint, text, extra, callback);
|
if (aiConfigured) aiCall(endpoint, text, extra, callback);
|
||||||
else { showToast('AI not configured', 'err'); hideAIProcessing(); }
|
else { showToast('AI non configuree', 'err'); hideAIProcessing(); }
|
||||||
}).catch(function() { showToast('AI status check failed', 'err'); hideAIProcessing(); });
|
}).catch(function() { showToast('Verification AI echouee', 'err'); hideAIProcessing(); });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAIProcessing();
|
showAIProcessing();
|
||||||
@@ -570,8 +591,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAIButton() {
|
function updateAIButton() {
|
||||||
if (aiConfigured) { btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)'; }
|
if (aiConfigured) {
|
||||||
else { btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured'; }
|
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'); }
|
function showAIProcessing() { statAI.classList.add('on'); }
|
||||||
@@ -646,7 +672,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
processPreviewBlocks();
|
processPreviewBlocks();
|
||||||
_previewLoading = false;
|
_previewLoading = false;
|
||||||
}).catch(function(e) {
|
}).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;
|
_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 === 's') { e.preventDefault(); forceSave(); }
|
||||||
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
||||||
if (e.ctrlKey && e.key === 'i') { 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
|
// Quick Insert: Alt+I
|
||||||
if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); }
|
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();
|
loadFile();
|
||||||
checkAIStatus();
|
checkAIStatus();
|
||||||
updateStats();
|
updateStats();
|
||||||
|
// Auto-focus after short delay (wait for iframe to fully render)
|
||||||
|
setTimeout(function() { ta.focus(); }, 200);
|
||||||
|
|
||||||
// ---- Process preview blocks (Mermaid + highlight.js) ----
|
// ---- Process preview blocks (Mermaid + highlight.js) ----
|
||||||
function processPreviewBlocks() {
|
function processPreviewBlocks() {
|
||||||
|
|||||||
Reference in New Issue
Block a user